2021-04-26

java 基于DelayQueue延时队列的使用

 

1.需求,通过数据库时间字段指定日期提前1小时给用户发送一条短信

 

2.解决方案,

1,jdk延时队列 DelayQueue       

  JDK自带的,随时可用,不需要引入外部组件啥的,使用起来比较方便,缺点,如果项目突然挂了,也队列中的数据也会消失。

2,  通过MQ的延时队列       

  优点,性能比较好,但是代价比较大,需要单独的部署MQ组件

3,  直接通过定时器每一分钟读取数据(如Quartz)

  比较耗费性能,代码比较简单

4,redis 过期提醒

  这块是利用Reids 过期通知 redis 2.8.0版本之后才支持

  到达过期时间后,会自动的去调用java的监听事件从而实现到达指定时间通知

5,redis sorted set

  这块主要利用它的score属性,redis通过score来为集合中的成员进行从大到小的排序。

  后端实现一个线程单独去处理,轮询去匹配是否到达了指定日期,到了就发送

 

好了,开始贴代码

这块其实就是jdk自带的DelayQueue 奈何jdk只能查找object的元素,以及对object进行删除,无法根据实体类中字段进行删除和查找,所以MeetingDelayQueue 去添加了几个方法

indexOfKeyByValue  通过 传入的key 和value查找是否匹配
removeByKeyValue   通过 传入的key value去匹配然后删除队列中的任务
MeetingDelayQueue  
package com.isoftstone.bussiness.expertService.task;import java.util.AbstractQueue;import java.util.Collection;import java.util.Iterator;import java.util.NoSuchElementException;import java.util.concurrent.BlockingQueue;import java.util.concurrent.Delayed;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.ReentrantLock;import static java.util.concurrent.TimeUnit.NANOSECONDS;/** * @Author: Fan ZhiWei * @Description: 会议的队列 * @Date: Create in 16:33 2021/4/23 */public class MeetingDelayQueue <E extends Delayed> extends AbstractQueue<E>  implements BlockingQueue<E> { private final transient ReentrantLock lock = new ReentrantLock(); private final PriorityMeetingQueue<E> q = new PriorityMeetingQueue<E>(); /**  * Thread designated to wait for the element at the head of  * the queue. This variant of the Leader-Follower pattern  * serves to  * minimize unnecessary timed waiting. When a thread becomes  * the leader, it waits only for the next delay to elapse, but  * other threads await indefinitely. The leader thread must  * signal some other thread before returning from take() or  * poll(...), unless some other thread becomes leader in the  * interim. Whenever the head of the queue is replaced with  * an element with an earlier expiration time, the leader  * field is invalidated by being reset to null, and some  * waiting thread, but not necessarily the current leader, is  * signalled. So waiting threads must be prepared to acquire  * and lose leadership while waiting.  */ private Thread leader = null; /**  * Condition signalled when a newer element becomes available  * at the head of the queue or a new thread may need to  * become leader.  */ private final Condition available = lock.newCondition(); /**  * Creates a new {@code DelayQueue} that is initially empty.  */ public MeetingDelayQueue() {} /**  * Creates a {@code DelayQueue} initially containing the elements of the  * given collection of {@link Delayed} instances.  *  * @param c the collection of elements to initially contain  * @throws NullPointerException if the specified collection or any  *   of its elements are null  */ public MeetingDelayQueue(Collection<? extends E> c) {  this.addAll(c); } /**  * Inserts the specified element into this delay queue.  *  * @param e the element to add  * @return {@code true} (as specified by {@link Collection#add})  * @throws NullPointerException if the specified element is null  */ public boolean add(E e) {  return offer(e); } /**  * Inserts the specified element into this delay queue.  *  * @param e the element to add  * @return {@code true}  * @throws NullPointerException if the specified element is null  */ public boolean offer(E e) {  final ReentrantLock lock = this.lock;  lock.lock();  try {   q.offer(e);   if (q.peek() == e) {    leader = null;    available.signal();   }   return true;  } finally {   lock.unlock();  } } /**  * Inserts the specified element into this delay queue. As the queue is  * unbounded this method will never block.  *  * @param e the element to add  * @throws NullPointerException {@inheritDoc}  */ public void put(E e) {  offer(e); } /**  * Inserts the specified element into this delay queue. As the queue is  * unbounded this method will never block.  *  * @param e the element to add  * @param timeout This parameter is ignored as the method never blocks  * @param unit This parameter is ignored as the method never blocks  * @return {@code true}  * @throws NullPointerException {@inheritDoc}  */ public boolean offer(E e, long timeout, TimeUnit unit) {  return offer(e); } /**  * Retrieves and removes the head of this queue, or returns {@code null}  * if this queue has no elements with an expired delay.  *  * @return the head of this queue, or {@code null} if this  *   queue has no elements with an expired delay  */ public E poll() {  final ReentrantLock lock = this.lock;  lock.lock();  try {   E first = q.peek();   if (first == null || first.getDelay(NANOSECONDS) > 0)    return null;   else    return q.poll();  } finally {   lock.unlock();  } } /**  * Retrieves and removes the head of this queue, waiting if necessary  * until an element with an expired delay is available on this queue.  *  * @return the head of this queue  * @throws InterruptedException {@inheritDoc}  */ public E take() throws InterruptedException {  final ReentrantLock lock = this.lock;  lock.lockInterruptibly();  try {   for (;;) {    E first = q.peek();    if (first == null)     available.await();    else {     long delay = first.getDelay(NANOSECONDS);     if (delay <= 0)      return q.poll();     first = null; // don't retain ref while waiting     if (leader != null)      available.await();     else {      Thread thisThread = Thread.currentThread();      leader = thisThread;      try {       available.awaitNanos(delay);      } finally {       if (leader == thisThread)        leader = null;      }     }    }   }  } finally {   if (leader == null && q.peek() != null)    available.signal();   lock.unlock();  } } /**  * Retrieves and removes the head of this queue, waiting if necessary  * until an element with an expired delay is available on this queue,  * or the specified wait time expires.  *  * @return the head of this queue, or {@code null} if the  *   specified waiting time elapses before an element with  *   an expired delay becomes available  * @throws InterruptedException {@inheritDoc}  */ public E poll(long timeout, TimeUnit unit) throws InterruptedException {  long nanos = unit.toNanos(timeout);  final ReentrantLock lock = this.lock;  lock.lockInterruptibly();  try {   for (;;) {    E first = q.peek();    if (first == null) {     if (nanos <= 0)      return null;     else      nanos = available.awaitNanos(nanos);    } else {     long delay = first.getDelay(NANOSECONDS);     if (delay <= 0)      return q.poll();     if (nanos <= 0)      return null;     first = null; // don't retain ref while waiting     if (nanos < delay || leader != null)      nanos = available.awaitNanos(nanos);     else {      Thread thisThread = Thread.currentThread();      leader = thisThread;      try {       long timeLeft = available.awaitNanos(delay);       nanos -= delay - timeLeft;      } finally {       if (leader == thisThread)        leader = null;      }     }    }   }  } finally {   if (leader == null && q.peek() != null)    available.signal();   lock.unlock();  } } /**  * Retrieves, but does not remove, the head of this queue, or  * returns {@code null} if this queue is empty. Unlike  * {@code poll}, if no expired elements are available in the queue,  * this method returns the element that will expire next,  * if one exists.  *  * @return the head of this queue, or {@code null} if this  *   queue is empty  */ public E peek() {  final ReentrantLock lock = this.lock;  lock.lock();  try {   return q.peek();  } finally {   lock.unlock();  } } public int size() {  final ReentrantLock lock = this.lock;  lock.lock();  try {   return q.size();  } finally {   lock.unlock();  } } /**  * Returns first element only if it is expired.  * Used only by drainTo. Call only when holding lock.  */ private E peekExpired() {  // assert lock.isHeldByCurrentThread();  E first = q.peek();  return (first == null || first.getDelay(NANOSECONDS) > 0) ?    null : first; } /**  * @throws UnsupportedOperationException {@inheritDoc}  * @throws ClassCastException   {@inheritDoc}  * @throws NullPointerException   {@inheritDoc}  * @throws IllegalArgumentException  {@inheritDoc}  */ public int drainTo(Collection<? super E> c) {  if (c == null)   throw new NullPointerException();  if (c == this)   throw new IllegalArgumentException();  final ReentrantLock lock = this.lock;  lock.lock();  try {   int n = 0;   for (E e; (e = peekExpired()) != null;) {    c.add(e);  // In this order, in case add() throws.    q.poll();    ++n;   }   return n;  } finally {   lock.unlock();  } } /**  * @throws UnsupportedOperationException {@inheritDoc}  * @throws ClassCastException   {@inheritDoc}  * @throws NullPointerException   {@inheritDoc}  * @throws IllegalArgumentException  {@inheritDoc}  */ public int drainTo(Collection<? super E> c, int maxElements) {  if (c == null)   throw new NullPointerException();  if (c == this)   throw new IllegalArgumentException();  if (maxElements <= 0)   return 0;  final ReentrantLock lock = this.lock;  lock.lock();  try {   int n = 0;   for (E e; n < maxElements && (e = peekExpired()) != null;) {    c.add(e);  // In this order, in case add() throws.    q.poll();    ++n;   }   return n;  } finally {   lock.unlock();  } } /**  * Atomically removes all of the elements from this delay queue.  * The queue will be empty after this call returns.  * Elements with an unexpired delay are not waited for; they are  * simply discarded from the queue.  */ public void clear() {  final ReentrantLock lock = this.lock;  lock.lock();  try {   q.clear();  } finally {   lock.unlock();  } } /**  * Always returns {@code Integer.MAX_VALUE} because  * a {@code DelayQueue} is not capacity constrained.  *  * @return {@code Integer.MAX_VALUE}  */ public int remainingCapacity() {  return Integer.MAX_VALUE; } /**  * Returns an array containing all of the elements in this queue.  * The returned array elements are in no particular order.  *  * <p>The returned array will be "safe" in that no references to it are  * maintained by this queue. (In other words, this method must allocate  * a new array). The caller is thus free to modify the returned array.  *  * <p>This method acts as bridge between array-based and collection-based  * APIs.  *  * @return an array containing all of the elements in this queue  */ public Object[] toArray() {  final ReentrantLock lock = this.lock;  lock.lock();  try {   return q.toArray();  } finally {   lock.unlock();  } } /**  * Returns an array containing all of the elements in this queue; the  * runtime type of the returned array is that of the specified array.  * The returned array elements are in no particular order.  * If the queue fits in the specified array, it is returned therein.  * Otherwise, a new array is allocated with the runtime type of the  * specified array and the size of this queue.  *  * <p>If this queue fits in the specified array with room to spare  * (i.e., the array has more elements than this queue), the element in  * the array immediately following the end of the queue is set to  * {@code null}.  *  * <p>Like the {@link #toArray()} method, this method acts as bridge between  * array-based and collection-based APIs. Further, this method allows  * precise control over the runtime type of the output array, and may,  * under certain circumstances, be used to save allocation costs.  *  * <p>The following code can be used to dump a delay queue into a newly  * allocated array of {@code Delayed}:  *  * <pre> {@code Delayed[] a = q.toArray(new Delayed[0]);}</pre>  *  * Note that {@code toArray(new Object[0])} is identical in function to  * {@code toArray()}.  *  * @param a the array into which the elements of the queue are to  *   be stored, if it is big enough; otherwise, a new array of the  *   same runtime type is allocated for this purpose  * @return an array containing all of the elements in this queue  * @throws ArrayStoreException if the runtime type of the specified array  *   is not a supertype of the runtime type of every element in  *   this queue  * @throws NullPointerException if the specified array is null  */ public <T> T[] toArray(T[] a) {  final ReentrantLock lock = this.lock;  lock.lock();  try {   return q.toArray(a);  } finally {   lock.unlock();  } } public int indexOfKeyByValue(String key1,String value1,String key2,String value2) {  final ReentrantLock lock = this.lock;  lock.lock();  try {   return q.indexOfKeyByValue(key1,value1,key2,value2);  } finally {   lock.unlock();  } } //fzw public int indexOfKeyByValue(String key,String value) {  final ReentrantLock lock = this.lock;  lock.lock();  try {   return q.indexOfKeyByValue(key,value);  } finally {   lock.unlock();  } } //fzw public boolean removeByKeyValue(String key,String value) {  final ReentrantLock lock = this.lock;  lock.lock();  try {   return q.removeKeyValue(key,value);  } finally {   lock.unlock();  } } /**  * Removes a single instance of the specified element from this  * queue, if it is present, whether or not it has expired.  */ public boolean remove(Object o) {  final ReentrantLock lock = this.lock;  lock.lock();  try {   return q.remove(o);  } finally {   lock.unlock();  } } /**  * Identity-based version for use in Itr.remove  */ void removeEQ(Object o) {  final ReentrantLock lock = this.lock;  lock.lock();  try {   for (Iterator<E> it = q.iterator(); it.hasNext(); ) {    if (o == it.next()) {     it.remove();     break;    }   }  } finally {   lock.unlock();  } } /**  * Returns an iterator over all the elements (both expired and  * unexpired) in this queue. The iterator does not return the  * elements in any particular order.  *  * <p>The returned iterator is  * <a href="package-summary.html#Weakly"><i>weakly consistent</i></a>.  *  * @return an iterator over the elements in this queue  */ public Iterator<E> iterator() {  return new MeetingDelayQueue.Itr(toArray()); } /**  * Snapshot iterator that works off copy of underlying q array.  */ private class Itr implements Iterator<E> {  final Object[] array; // Array of all elements  int cursor;   // index of next element to return  int lastRet;   // index of last element, or -1 if no such  Itr(Object[] array) {   lastRet = -1;   this.array = array;  }  public boolean hasNext() {   return cursor < array.length;  }  @SuppressWarnings("unchecked")  public E next() {   if (cursor >= array.length)    throw new NoSuchElementException();   lastRet = cursor;   return (E)array[cursor++];  }  public void remove() {   if (lastRet < 0)    throw new IllegalStateException();   removeEQ(array[lastRet]);   lastRet = -1;  } }}

 


PriorityMeetingQueue 这个是重写的PriorityQueue 主要是MeetingDelayQueue 当中就是用这个进行保存的,删除,和干啥其实就是对这个对象的操

package com.isoftstone.bussiness.expertService.task;import org.apache.commons.lang.StringUtils;import sun.misc.SharedSecrets;import java.lang.reflect.Field;import java.util.*;import java.util.function.Consumer;/** * @Author: Fan ZhiWei * @Description: * @Date: Create in 16:50 2021/4/23 */public class PriorityMeetingQueue<E> extends AbstractQueue<E>  implements java.io.Serializable { private static final long serialVersionUID = -7720805057305804111L; private static final int DEFAULT_INITIAL_CAPACITY = 11; /**  * Priority queue represented as a balanced binary heap: the two  * children of queue[n] are queue[2*n+1] and queue[2*(n+1)]. The  * priority queue is ordered by comparator, or by the elements'  * natural ordering, if comparator is null: For each node n in the  * heap and each descendant d of n, n <= d. The element with the  * lowest value is in queue[0], assuming the queue is nonempty.  */ transient Object[] queue; // non-private to simplify nested class access /**  * The number of elements in the priority queue.  */ private int size = 0; /**  * The comparator, or null if priority queue uses elements'  * natural ordering.  */ private final Comparator<? super E> comparator; /**  * The number of times this priority queue has been  * <i>structurally modified</i>. See AbstractList for gory details.  */ transient int modCount = 0; // non-private to simplify nested class access /**  * Creates a {@code PriorityMeetingQueue} with the default initial  * capacity (11) that orders its elements according to their  * {@linkplain Comparable natural ordering}.  */ public PriorityMeetingQueue() {  this(DEFAULT_INITIAL_CAPACITY, null); } /**  * Creates a {@code PriorityMeetingQueue} with the specified initial  * capacity that orders its elements according to their  * {@linkplain Comparable natural ordering}.  *  * @param initialCapacity the initial capacity for this priority queue  * @throws IllegalArgumentException if {@code initialCapacity} is less  *   than 1  */ public PriorityMeetingQueue(int initialCapacity) {  this(initialCapacity, null); } /**  * Creates a {@code PriorityMeetingQueue} with the default initial capacity and  * whose elements are ordered according to the specified comparator.  *  * @param comparator the comparator that will be used to order this  *   priority queue. If {@code null}, the {@linkplain Comparable  *   natural ordering} of the elements will be used.  * @since 1.8  */ public PriorityMeetingQueue(Comparator<? super E> comparator) {  this(DEFAULT_INITIAL_CAPACITY, comparator); } /**  * Creates a {@code PriorityMeetingQueue} with the specified initial capacity  * that orders its elements according to the specified comparator.  *  * @param initialCapacity the initial capacity for this priority queue  * @param comparator the comparator that will be used to order this  *   priority queue. If {@code null}, the {@linkplain Comparable  *   natural ordering} of the elements will be used.  * @throws IllegalArgumentException if {@code initialCapacity} is  *   less than 1  */ public PriorityMeetingQueue(int initialCapacity,        Comparator<? super E> comparator) {  // Note: This restriction of at least one is not actually needed,  // but continues for 1.5 compatibility  if (initialCapacity < 1)   throw new IllegalArgumentException();  this.queue = new Object[initialCapacity];  this.comparator = comparator; } /**  * Creates a {@code PriorityMeetingQueue} containing the elements in the  * specified collection. If the specified collection is an instance of  * a {@link SortedSet} or is another {@code PriorityMeetingQueue}, this  * priority queue will be ordered according to the same ordering.  * Otherwise, this priority queue will be ordered according to the  * {@linkplain Comparable natural ordering} of its elements.  *  * @param c the collection whose elements are to be placed  *   into this priority queue  * @throws ClassCastException if elements of the specified collection  *   cannot be compared to one another according to the priority  *   queue's ordering  * @throws NullPointerException if the specified collection or any  *   of its elements are null  */ @SuppressWarnings("unchecked") public PriorityMeetingQueue(Collection<? extends E> c) {  if (c instanceof SortedSet<?>) {   SortedSet<? extends E> ss = (SortedSet<? extends E>) c;   this.comparator = (Comparator<? super E>) ss.comparator();   initElementsFromCollection(ss);  }  else if (c instanceof PriorityMeetingQueue<?>) {   PriorityMeetingQueue<? extends E> pq = (PriorityMeetingQueue<? extends E>) c;   this.comparator = (Comparator<? super E>) pq.comparator();   initFromPriorityMeetingQueue(pq);  }  else {   this.comparator = null;   initFromCollection(c);  } } /**  * Creates a {@code PriorityMeetingQueue} containing the elements in the  * specified priority queue. This priority queue will be  * ordered according to the same ordering as the given priority  * queue.  *  * @param c the priority queue whose elements are to be placed  *   into this priority queue  * @throws ClassCastException if elements of {@code c} cannot be  *   compared to one another according to {@code c}'s  *   ordering  * @throws NullPointerException if the specified priority queue or any  *   of its elements are null  */ @SuppressWarnings("unchecked") public PriorityMeetingQueue(PriorityMeetingQueue<? extends E> c) {  this.comparator = (Comparator<? super E>) c.comparator();  initFromPriorityMeetingQueue(c); } /**  * Creates a {@code PriorityMeetingQueue} containing the elements in the  * specified sorted set. This priority queue will be ordered  * according to the same ordering as the given sorted set.  *  * @param c the sorted set whose elements are to be placed  *   into this priority queue  * @throws ClassCastException if elements of the specified sorted  *   set cannot be compared to one another according to the  *   sorted set's ordering  * @throws NullPointerException if the specified sorted set or any  *   of its elements are null  */ @SuppressWarnings("unchecked") public PriorityMeetingQueue(SortedSet<? extends E> c) {  this.comparator = (Comparator<? super E>) c.comparator();  initElementsFromCollection(c); } private void initFromPriorityMeetingQueue(PriorityMeetingQueue<? extends E> c) {  if (c.getClass() == PriorityMeetingQueue.class) {   this.queue = c.toArray();   this.size = c.size();  } else {   initFromCollection(c);  } } private void initElementsFromCollection(Collection<? extends E> c) {  Object[] a = c.toArray();  // If c.toArray incorrectly doesn't return Object[], copy it.  if (a.getClass() != Object[].class)   a = Arrays.copyOf(a, a.length, Object[].class);  int len = a.length;  if (len == 1 || this.comparator != null)   for (int i = 0; i < len; i++)    if (a[i] == null)     throw new NullPointerException();  this.queue = a;  this.size = a.length; } /**  * Initializes queue array with elements from the given Collection.  *  * @param c the collection  */ private void initFromCollection(Collection<? extends E> c) {  initElementsFromCollection(c);  heapify(); } /**  * The maximum size of array to allocate.  * Some VMs reserve some header words in an array.  * Attempts to allocate larger arrays may result in  * OutOfMemoryError: Requested array size exceeds VM limit  */ private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; /**  * Increases the capacity of the array.  *  * @param minCapacity the desired minimum capacity  */ private void grow(int minCapacity) {  int oldCapacity = queue.length;  // Double size if small; else grow by 50%  int newCapacity = oldCapacity + ((oldCapacity < 64) ?    (oldCapacity + 2) :    (oldCapacity >> 1));  // overflow-conscious code  if (newCapacity - MAX_ARRAY_SIZE > 0)   newCapacity = hugeCapacity(minCapacity);  queue = Arrays.copyOf(queue, newCapacity); } private static int hugeCapacity(int minCapacity) {  if (minCapacity < 0) // overflow   throw new OutOfMemoryError();  return (minCapacity > MAX_ARRAY_SIZE) ?    Integer.MAX_VALUE :    MAX_ARRAY_SIZE; } /**  * Inserts the specified element into this priority queue.  *  * @return {@code true} (as specified by {@link Collection#add})  * @throws ClassCastException if the specified element cannot be  *   compared with elements currently in this priority queue  *   according to the priority queue's ordering  * @throws NullPointerException if the specified element is null  */ public boolean add(E e) {  return offer(e); } /**  * Inserts the specified element into this priority queue.  *  * @return {@code true} (as specified by {@link Queue#offer})  * @throws ClassCastException if the specified element cannot be  *   compared with elements currently in this priority queue  *   according to the priority queue's ordering  * @throws NullPointerException if the specified element is null  */ public boolean offer(E e) {  if (e == null)   throw new NullPointerException();  modCount++;  int i = size;  if (i >= queue.length)   grow(i + 1);  size = i + 1;  if (i == 0)   queue[0] = e;  else   siftUp(i, e);  return true; } @SuppressWarnings("unchecked") public E peek() {  return (size == 0) ? null : (E) queue[0]; } private int indexOf(Object o) {  if (o != null) {   for (int i = 0; i < size; i++)    if (o.equals(queue[i]))     return i;  }  return -1; } //根据实体类中的键值对来找到索引 public int indexOfKeyByValue(String key, String value) {  if (StringUtils.isNotEmpty(key)&&StringUtils.isNotEmpty(value)) {   for (int i = 0; i < size; i++) {    try {     Class aClass1 = queue[i].getClass();     Field field = aClass1.getDeclaredField(key);     field.setAccessible(true);     if(value.equals(field.get(queue[i]))){      return i;     }    } catch (Exception e) {     e.printStackTrace();    }   }  }  return -1; } //根据实体类中的键值对来找到索引 public int indexOfKeyByValue(String key1,String value1,String key2,String value2) {  if (StringUtils.isNotEmpty(key1)&&StringUtils.isNotEmpty(value1)&&StringUtils.isNotEmpty(key2)&&StringUtils.isNotEmpty(value2)) {   for (int i = 0; i < size; i++) {    try {     Class aClass1 = queue[i].getClass();     Field field1 = aClass1.getDeclaredField(key1);     Field field2 = aClass1.getDeclaredField(key2);     field1.setAccessible(true);     field2.setAccessible(true);     if(value1.equals(field1.get(queue[i]))&&value2.equals(field2.get(queue[i]))){      return i;     }    } catch (Exception e) {     e.printStackTrace();    }   }  }  return -1; } /*根据实体类中的建值来*/ /**  * Removes a single instance of the specified element from this queue,  * if it is present. More formally, removes an element {@code e} such  * that {@code o.equals(e)}, if this queue contains one or more such  * elements. Returns {@code true} if and only if this queue contained  * the specified element (or equivalently, if this queue changed as a  * result of the call).  *  * @param o element to be removed from this queue, if present  * @return {@code true} if this queue changed as a result of the call  */ public boolean remove(Object o) {  int i = indexOf(o);  if (i == -1)   return false;  else {   removeAt(i);   return true;  } } public boolean removeKeyValue(String key,String value) {  int i = indexOfKeyByValue(key,value);  if (i == -1)   return false;  else {   removeAt(i);   return true;  } } /**  * Version of remove using reference equality, not equals.  * Needed by iterator.remove.  *  * @param o element to be removed from this queue, if present  * @return {@code true} if removed  */ boolean removeEq(Object o) {  for (int i = 0; i < size; i++) {   if (o == queue[i]) {    removeAt(i);    return true;   }  }  return false; } /**  * Returns {@code true} if this queue contains the specified element.  * More formally, returns {@code true} if and only if this queue contains  * at least one element {@code e} such that {@code o.equals(e)}.  *  * @param o object to be checked for containment in this queue  * @return {@code true} if this queue contains the specified element  */ public boolean contains(Object o) {  return indexOf(o) != -1; } /**  * Returns an array containing all of the elements in this queue.  * The elements are in no particular order.  *  * <p>The returned array will be "safe" in that no references to it are  * maintained by this queue. (In other words, this method must allocate  * a new array). The caller is thus free to modify the returned array.  *  * <p>This method acts as bridge between array-based and collection-based  * APIs.  *  * @return an array containing all of the elements in this queue  */ public Object[] toArray() {  return Arrays.copyOf(queue, size); } /**  * Returns an array containing all of the elements in this queue; the  * runtime type of the returned array is that of the specified array.  * The returned array elements are in no particular order.  * If the queue fits in the specified array, it is returned therein.  * Otherwise, a new array is allocated with the runtime type of the  * specified array and the size of this queue.  *  * <p>If the queue fits in the specified array with room to spare  * (i.e., the array has more elements than the queue), the element in  * the array immediately following the end of the collection is set to  * {@code null}.  *  * <p>Like the {@link #toArray()} method, this method acts as bridge between  * array-based and collection-based APIs. Further, this method allows  * precise control over the runtime type of the output array, and may,  * under certain circumstances, be used to save allocation costs.  *  * <p>Suppose {@code x} is a queue known to contain only strings.  * The following code can be used to dump the queue into a newly  * allocated array of {@code String}:  *  * <pre> {@code String[] y = x.toArray(new String[0]);}</pre>  *  * Note that {@code toArray(new Object[0])} is identical in function to  * {@code toArray()}.  *  * @param a the array into which the elements of the queue are to  *   be stored, if it is big enough; otherwise, a new array of the  *   same runtime type is allocated for this purpose.  * @return an array containing all of the elements in this queue  * @throws ArrayStoreException if the runtime type of the specified array  *   is not a supertype of the runtime type of every element in  *   this queue  * @throws NullPointerException if the specified array is null  */ @SuppressWarnings("unchecked") public <T> T[] toArray(T[] a) {  final int size = this.size;  if (a.length < size)   // Make a new array of a's runtime type, but my contents:   return (T[]) Arrays.copyOf(queue, size, a.getClass());  System.arraycopy(queue, 0, a, 0, size);  if (a.length > size)   a[size] = null;  return a; } /**  * Returns an iterator over the elements in this queue. The iterator  * does not return the elements in any particular order.  *  * @return an iterator over the elements in this queue  */ public Iterator<E> iterator() {  return new Itr(); } private final class Itr implements Iterator<E> {  /**   * Index (into queue array) of element to be returned by   * subsequent call to next.   */  private int cursor = 0;  /**   * Index of element returned by most recent call to next,   * unless that element came from the forgetMeNot list.   * Set to -1 if element is deleted by a call to remove.   */  private int lastRet = -1;  /**   * A queue of elements that were moved from the unvisited portion of   * the heap into the visited portion as a result of "unlucky" element   * removals during the iteration. (Unlucky element removals are those   * that require a siftup instead of a siftdown.) We must visit all of   * the elements in this list to complete the iteration. We do this   * after we've completed the "normal" iteration.   *   * We expect that most iterations, even those involving removals,   * will not need to store elements in this field.   */  private ArrayDeque<E> forgetMeNot = null;  /**   * Element returned by the most recent call to next iff that   * element was drawn from the forgetMeNot list.   */  private E lastRetElt = null;  /**   * The modCount value that the iterator believes that the backing   * Queue should have. If this expectation is violated, the iterator   * has detected concurrent modification.   */  private int expectedModCount = modCount;  public boolean hasNext() {   return cursor < size ||     (forgetMeNot != null && !forgetMeNot.isEmpty());  }  @SuppressWarnings("unchecked")  public E next() {   if (expectedModCount != modCount)    throw new ConcurrentModificationException();   if (cursor < size)    return (E) queue[lastRet = cursor++];   if (forgetMeNot != null) {    lastRet = -1;    lastRetElt = forgetMeNot.poll();    if (lastRetElt != null)     return lastRetElt;   }   throw new NoSuchElementException();  }  public void remove() {   if (expectedModCount != modCount)    throw new ConcurrentModificationException();   if (lastRet != -1) {    E moved = PriorityMeetingQueue.this.removeAt(lastRet);    lastRet = -1;    if (moved == null)     cursor--;    else {     if (forgetMeNot == null)      forgetMeNot = new ArrayDeque<>();     forgetMeNot.add(moved);    }   } else if (lastRetElt != null) {    PriorityMeetingQueue.this.removeEq(lastRetElt);    lastRetElt = null;   } else {    throw new IllegalStateException();   }   expectedModCount = modCount;  } } public int size() {  return size; } /**  * Removes all of the elements from this priority queue.  * The queue will be empty after this call returns.  */ public void clear() {  modCount++;  for (int i = 0; i < size; i++)   queue[i] = null;  size = 0; } @SuppressWarnings("unchecked") public E poll() {  if (size == 0)   return null;  int s = --size;  modCount++;  E result = (E) queue[0];  E x = (E) queue[s];  queue[s] = null;  if (s != 0)   siftDown(0, x);  return result; } /**  * Removes the ith element from queue.  *  * Normally this method leaves the elements at up to i-1,  * inclusive, untouched. Under these circumstances, it returns  * null. Occasionally, in order to maintain the heap invariant,  * it must swap a later element of the list with one earlier than  * i. Under these circumstances, this method returns the element  * that was previously at the end of the list and is now at some  * position before i. This fact is used by iterator.remove so as to  * avoid missing traversing elements.  */ @SuppressWarnings("unchecked") private E removeAt(int i) {  // assert i >= 0 && i < size;  modCount++;  int s = --size;  if (s == i) // removed last element   queue[i] = null;  else {   E moved = (E) queue[s];   queue[s] = null;   siftDown(i, moved);   if (queue[i] == moved) {    siftUp(i, moved);    if (queue[i] != moved)     return moved;   }  }  return null; } /**  * Inserts item x at position k, maintaining heap invariant by  * promoting x up the tree until it is greater than or equal to  * its parent, or is the root.  *  * To simplify and speed up coercions and comparisons. the  * Comparable and Comparator versions are separated into different  * methods that are otherwise identical. (Similarly for siftDown.)  *  * @param k the position to fill  * @param x the item to insert  */ private void siftUp(int k, E x) {  if (comparator != null)   siftUpUsingComparator(k, x);  else   siftUpComparable(k, x); } @SuppressWarnings("unchecked") private void siftUpComparable(int k, E x) {  Comparable<? super E> key = (Comparable<? super E>) x;  while (k > 0) {   int parent = (k - 1) >>> 1;   Object e = queue[parent];   if (key.compareTo((E) e) >= 0)    break;   queue[k] = e;   k = parent;  }  queue[k] = key; } @SuppressWarnings("unchecked") private void siftUpUsingComparator(int k, E x) {  while (k > 0) {   int parent = (k - 1) >>> 1;   Object e = queue[parent];   if (comparator.compare(x, (E) e) >= 0)    break;   queue[k] = e;   k = parent;  }  queue[k] = x; } /**  * Inserts item x at position k, maintaining heap invariant by  * demoting x down the tree repeatedly until it is less than or  * equal to its children or is a leaf.  *  * @param k the position to fill  * @param x the item to insert  */ private void siftDown(int k, E x) {  if (comparator != null)   siftDownUsingComparator(k, x);  else   siftDownComparable(k, x); } @SuppressWarnings("unchecked") private void siftDownComparable(int k, E x) {  Comparable<? super E> key = (Comparable<? super E>)x;  int half = size >>> 1;  // loop while a non-leaf  while (k < half) {   int child = (k << 1) + 1; // assume left child is least   Object c = queue[child];   int right = child + 1;   if (right < size &&     ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)    c = queue[child = right];   if (key.compareTo((E) c) <= 0)    break;   queue[k] = c;   k = child;  }  queue[k] = key; } @SuppressWarnings("unchecked") private void siftDownUsingComparator(int k, E x) {  int half = size >>> 1;  while (k < half) {   int child = (k << 1) + 1;   Object c = queue[child];   int right = child + 1;   if (right < size &&     comparator.compare((E) c, (E) queue[right]) > 0)    c = queue[child = right];   if (comparator.compare(x, (E) c) <= 0)    break;   queue[k] = c;   k = child;  }  queue[k] = x; } /**  * Establishes the heap invariant (described above) in the entire tree,  * assuming nothing about the order of the elements prior to the call.  */ @SuppressWarnings("unchecked") private void heapify() {  for (int i = (size >>> 1) - 1; i >= 0; i--)   siftDown(i, (E) queue[i]); } /**  * Returns the comparator used to order the elements in this  * queue, or {@code null} if this queue is sorted according to  * the {@linkplain Comparable natural ordering} of its elements.  *  * @return the comparator used to order this queue, or  *   {@code null} if this queue is sorted according to the  *   natural ordering of its elements  */ public Comparator<? super E> comparator() {  return comparator; } /**  * Saves this queue to a stream (that is, serializes it).  *  * @serialData The length of the array backing the instance is  *    emitted (int), followed by all of its elements  *    (each an {@code Object}) in the proper order.  * @param s the stream  */ private void writeObject(java.io.ObjectOutputStream s)   throws java.io.IOException {  // Write out element count, and any hidden stuff  s.defaultWriteObject();  // Write out array length, for compatibility with 1.5 version  s.writeInt(Math.max(2, size + 1));  // Write out all elements in the "proper order".  for (int i = 0; i < size; i++)   s.writeObject(queue[i]); } /**  * Reconstitutes the {@code PriorityMeetingQueue} instance from a stream  * (that is, deserializes it).  *  * @param s the stream  */ private void readObject(java.io.ObjectInputStream s)   throws java.io.IOException, ClassNotFoundException {  // Read in size, and any hidden stuff  s.defaultReadObject();  // Read in (and discard) array length  s.readInt();  SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, size);  queue = new Object[size];  // Read in all elements.  for (int i = 0; i < size; i++)   queue[i] = s.readObject();  // Elements are guaranteed to be in "proper order", but the  // spec has never explained what that might be.  heapify(); } /**  * Creates a <em><a href="Spliterator.html#binding">late-binding</a></em>  * and <em>fail-fast</em> {@link Spliterator} over the elements in this  * queue.  *  * <p>The {@code Spliterator} reports {@link Spliterator#SIZED},  * {@link Spliterator#SUBSIZED}, and {@link Spliterator#NONNULL}.  * Overriding implementations should document the reporting of additional  * characteristic values.  *  * @return a {@code Spliterator} over the elements in this queue  * @since 1.8  */ public final Spliterator<E> spliterator() {  return new PriorityMeetingQueueSpliterator<E>(this, 0, -1, 0); } static final class PriorityMeetingQueueSpliterator<E> implements Spliterator<E> {  /*   * This is very similar to ArrayList Spliterator, except for   * extra null checks.   */  private final PriorityMeetingQueue<E> pq;  private int index;   // current index, modified on advance/split  private int fence;   // -1 until first use  private int expectedModCount; // initialized when fence set  /** Creates new spliterator covering the given range */  PriorityMeetingQueueSpliterator(PriorityMeetingQueue<E> pq, int origin, int fence,          int expectedModCount) {   this.pq = pq;   this.index = origin;   this.fence = fence;   this.expectedModCount = expectedModCount;  }  private int getFence() { // initialize fence to size on first use   int hi;   if ((hi = fence) < 0) {    expectedModCount = pq.modCount;    hi = fence = pq.size;   }   return hi;  }  public PriorityMeetingQueueSpliterator<E> trySplit() {   int hi = getFence(), lo = index, mid = (lo + hi) >>> 1;   return (lo >= mid) ? null :     new PriorityMeetingQueueSpliterator<E>(pq, lo, index = mid,       expectedModCount);  }  @SuppressWarnings("unchecked")  public void forEachRemaining(Consumer<? super E> action) {   int i, hi, mc; // hoist accesses and checks from loop   PriorityMeetingQueue<E> q; Object[] a;   if (action == null)    throw new NullPointerException();   if ((q = pq) != null && (a = q.queue) != null) {    if ((hi = fence) < 0) {     mc = q.modCount;     hi = q.size;    }    else     mc = expectedModCount;    if ((i = index) >= 0 && (index = hi) <= a.length) {     for (E e;; ++i) {      if (i < hi) {       if ((e = (E) a[i]) == null) // must be CME        break;       action.accept(e);      }      else if (q.modCount != mc)       break;      else       return;     }    }   }   throw new ConcurrentModificationException();  }  public boolean tryAdvance(Consumer<? super E> action) {   if (action == null)    throw new NullPointerException();   int hi = getFence(), lo = index;   if (lo >= 0 && lo < hi) {    index = lo + 1;    @SuppressWarnings("unchecked") E e = (E)pq.queue[lo];    if (e == null)     throw new ConcurrentModificationException();    action.accept(e);    if (pq.modCount != expectedModCount)     throw new ConcurrentModificationException();    return true;   }   return false;  }  public long estimateSize() {   return (long) (getFence() - index);  }  public int characteristics() {   return Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.NONNULL;  } }}

  

DelayedElement  这个就相当于是实体类了 大家可以在里面去自定义字段,以供 后续进行使用
package com.isoftstone.bussiness.expertService.task;import lombok.Data;import java.util.concurrent.Delayed;import java.util.concurrent.TimeUnit;/** * @Author: Fan ZhiWei * @Description: * @Date: Create in 15:54 2021/4/23 */@Datapublic class DelayedElement implements Delayed { private String id; //id private long delay; //延迟时间 private long expire; //到期时间 private String code; // 会议号 private String userId; private long now; //创建时间 public DelayedElement(){ } /**  *  * @param expire 开始时间  * @param id  id  * @param userId 用户id  * @param code  会议号  */ public DelayedElement(long expire, String id,String userId,String code) {  this.expire = expire;  this.userId=userId;  this.id = id;  this.code=code;  delay =expire-System.currentTimeMillis(); //延迟时间 = 到期时间-延迟时间  now = System.currentTimeMillis(); } /**  * 需要实现的接口,获得延迟时间 用过期时间-当前时间  * @param unit  * @return  */ @Override public long getDelay(TimeUnit unit) {  return unit.convert(this.expire - System.currentTimeMillis() , TimeUnit.MILLISECONDS); } /**  * 用于延迟队列内部比较排序 当前时间的延迟时间 - 比较对象的延迟时间  * @param o  * @return  */ @Override public int compareTo(Delayed o) {  return (int) (this.getDelay(TimeUnit.MILLISECONDS) -o.getDelay(TimeUnit.MILLISECONDS)); } @Override public String toString() {  final StringBuilder sb = new StringBuilder("DelayedElement{");  sb.append("delay=").append(delay);  sb.append(", expire=").append(expire);  sb.append(", userId='").append(userId).append('\'');  sb.append(", now=").append(now);  sb.append('}');  return sb.toString(); }}

  

SmsExpertTaskQueue 当中使用一个多线程去消费延时队列中的数据,然后定时任务,定时去取数据库中的数据,当队列中的数据被消费后,我这边是发送短信,发送短信后,然后将数据库中的数据标识为已发送,下次查询就不会查询到已发送的数据了
package com.isoftstone.bussiness.expertService.task;import com.github.pagehelper.util.StringUtil;import com.isoftstone.bussiness.expertService.model.Record;import com.isoftstone.bussiness.expertService.service.RecordService;import com.isoftstone.bussiness.system.model.User;import com.isoftstone.bussiness.system.services.UserService;import com.isoftstone.platform.utils.Constant;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.boot.ApplicationArguments;import org.springframework.boot.ApplicationRunner;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.stereotype.Component;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.*;import java.util.stream.Collectors;/** * @Author: Fan ZhiWei * @Description: * @Date: Create in 15:52 2021/4/23 */@Componentpublic class SmsExpertTaskQueue implements ApplicationRunner { final static MeetingDelayQueue<DelayedElement> delayQueue = new MeetingDelayQueue<DelayedElement>(); private static final Logger logger = LoggerFactory.getLogger(SmsExpertTaskQueue.class); private SimpleDateFormat formatYMD=new SimpleDateFormat("yyyy-MM-dd"); private SimpleDateFormat formatHM=new SimpleDateFormat("HH:mm"); private SimpleDateFormat formatYMDHM=new SimpleDateFormat("yyyy-MM-dd HH:mm"); //测试数据使用 /* private Boolean flag=true;*/ private void producer(){ } //这个其实就是一个前缀,如何你好你的话费余额不足 @Value("${ide.sms.text.expert}") private String expert; //这个值就是true 和false了 @Value("${scheduled.enable}") private String scheduledEnable; @Autowired private UserService userService; @Autowired private RecordService recordService; //这个也是一个定时器 每十五分钟执行一次 cron: 0 0/15 * * * ? @Scheduled(cron = "${scheduled.expertTime}") public void task(){  if (!Boolean.parseBoolean(scheduledEnable)) {   return;  }  addDelayQueueDate(); } public Date dateAdd(Date date1,Date date2){  if(null==date1||null==date2){   return null;  }  Date date=null;  try {   date = formatYMDHM.parse((formatYMD.format(date1) +" "+ formatHM.format(date2)));  } catch (ParseException e) {   e.printStackTrace();  }  return date; } private List<Record> getRecordList(){  List<Record> recordList = new ArrayList<>();  Date date=new Date();  Map<String,String> map=new HashMap<>();  map.put("date",formatYMD.format(date));  map.put("startDate",formatHM.format(date));  date.setTime(date.getTime()+(1000*60*60));  map.put("endDate",formatHM.format(date));  recordList=recordService.getRecordList(map);  /*if(flag) {   try {    Record record1 = new Record();    record1.setId("123456789");    record1.setMeetingCode("123456");    record1.setSelectedExpertId("2e9b121414444086a1ae794807468d14");    String date1 = "2021-04-25";    record1.setMeetingTime(formatYMD.parse(date1));    String date2 = "15:44";    record1.setMeetingStartTime(formatHM.parse(date2));    Record record2 = new Record();    record2.setId("12345678");    record2.setMeetingCode("123456");    String date3 = "2021-04-25";    record2.setMeetingTime(formatYMD.parse(date3));    String date4 = "15:44";    record2.setSelectedExpertId("2e9b121414444086a1ae794807468d14");    record2.setMeetingStartTime(formatHM.parse(date4));    recordList.add(record1);    recordList.add(record2);   } catch (Exception e) {    e.printStackTrace();   }   flag=false;  }*/  return recordList; } private void addDelayQueueDate(){  logger.info("添加需要发送短信的数据!");  //拿到模拟数据 getRecordList 当中有模拟数据,不过目前是取数据库,需要使用的时候可以解开注释并注释掉查数据库的操作  //Record 这个类就不贴出来了,其实就用到了那几个字段 大家自己去建个实体类也是一样的  /* 其实就用到了这几个字段,有需要自己建把 @ApiModelProperty("会议号")  private String meetingCode;  @ApiModelProperty("会议时间")  @JsonFormat(pattern="yyyy-MM-dd", timezone = "GMT+8")  @DateTimeFormat(pattern="yyyy-MM-dd")  private Date meetingTime;  @ApiModelProperty("会议结束时间")  @JsonFormat(pattern="HH:mm", timezone = "GMT+8")  @DateTimeFormat(pattern="HH:mm")  private Date meetingEndTime;  @ApiModelProperty("会议开始时间")  @JsonFormat(pattern="HH:mm", timezone = "GMT+8")  @DateTimeFormat(pattern="HH:mm")  private Date meetingStartTime;  @ApiModelProperty("会议是否已通知 0未通知1已通知")  private int meetingNotice;*/  List<Record> recordList = getRecordList();  recordList.forEach(o->{   Date date = dateAdd(o.getMeetingTime(), o.getMeetingStartTime());   //是否有相同的数据   int id = delayQueue.indexOfKeyByValue("id", o.getId());   if(-1!=id){    //有id相同的数据    if(-1==delayQueue.indexOfKeyByValue("id",o.getId(),"expire",String.valueOf(date.getTime()))) {     //id相同,到期时间不同,就先移除当前id的任务,然后在添加     delayQueue.removeByKeyValue("id", o.getId());     delayQueue.add(new DelayedElement(date.getTime(),o.getId(),o.getSelectedExpertId(),o.getMeetingCode()));    }   }else{    //无相同的数据直接添加 15071135982 默认自己的手机号,测试后通过专家id获取    delayQueue.add(new DelayedElement(date.getTime(),o.getId(),o.getSelectedExpertId(),o.getMeetingCode()));   }  });  logger.info("目前还有多少个任务待执行"+delayQueue.size()); } /**  * 消费者,从延迟队列中获得数据,进行处理  * @param delayQueue  */ private void consumer(final MeetingDelayQueue<DelayedElement> delayQueue){  new Thread(() -> {   while (true){    DelayedElement element = null;    try {     element = delayQueue.take();    } catch (InterruptedException e) {     e.printStackTrace();    }    logger.info("=======================================");    logger.info("consumer"+System.currentTimeMillis()+"---"+element);    /*这块是发送短信的 这个方法就没必要看了,看了你们也用不上,自己写方法去把*/    SMSSend(element.getUserId(),element.getExpire(),element.getCode(),element.getId());    logger.info("=======================================");   }  }).start(); } private void SMSSend(String userIdStr,long time,String code,String id){  try {   if(StringUtil.isNotEmpty(userIdStr)){    List<String> userList = Arrays.stream(userIdStr.split(",")).filter(o -> StringUtil.isNotEmpty(o)).collect(Collectors.toList());    userList.forEach(o->{     User user = userService.selectByKey(o);     Boolean falg=true;     if(null==user){      falg=false;      logger.info("查无此人"+user);     }     String[] roleCode = userService.selectRoleCodeByUserId(user.getUserId());     //专家     if(!Arrays.asList(roleCode).contains(Constant.SYS_ROLE_6)){      falg=false;      logger.info("角色为专家才会短信提醒");     }     if(StringUtil.isEmpty(user.getPhone())){      falg=false;      logger.info("手机号不能为空");     }     if(falg){      logger.info("发送成功 手机号为"+user.getPhone()+"发送人="+user.getRealName());      /*ResultModel sendSms= SmsUtils.sendSms(user.getPhone(),expert+code+",会议开始时间"+formatYMDHM.format(new Date(time)));      if("SUCCESS".equals(sendSms.getCode())) {       logger.info("发送成功 手机号为"+user.getPhone()+"发送人="+user.getRealName());      }else {       logger.info("发送失败");      }*/     }    });    //全部发完短信后修改    recordService.updateMeetingNotice(id);   }  } catch (Exception e) {   logger.error("发送失败报错", e);  } } @Override public void run(ApplicationArguments args){
     /*这块就是添加初始的数据了,15分钟执行一次添加任务的操作主要担心这一个小时内临时的就变更会议时间了*/ addDelayQueueDate(); logger.info("启动consumer消费者进行读取延时队列中的数据"); consumer(delayQueue); }}

  









原文转载:http://www.shaoqun.com/a/708742.html

跨境电商:https://www.ikjzd.com/

壹米滴答:https://www.ikjzd.com/w/2314

易趣:https://www.ikjzd.com/w/210


1.需求,通过数据库时间字段指定日期提前1小时给用户发送一条短信2.解决方案,1,jdk延时队列DelayQueue      JDK自带的,随时可用,不需要引入外部组件啥的,使用起来比较方便,缺点,如果项目突然挂了,也队列中的数据也会消失。2,通过MQ的延时队列         优点,性能比较好,但是代价比较大,需要单独的部署MQ组件3,直接通过定时器每一分钟读取数据(如Quartz)  比较耗
淘粉吧官网:https://www.ikjzd.com/w/1725.html
ask me:https://www.ikjzd.com/w/2459
usps:https://www.ikjzd.com/w/513
如何通过Facebook自动找到亚马逊买家,并进行持续营销?:https://www.ikjzd.com/home/23147
重磅!亚马逊宣布中国卖家可以进驻沙特站点了!进驻攻略(2):https://www.ikjzd.com/home/134968
eBay中的Selling Manager 和 Selling Manager Pro傻傻分不清?:https://www.ikjzd.com/home/96142

No comments:

Post a Comment