基于HashedWheelTimer的一个定时器实现

之前有几个需要用到定时器超时的场景,比如线程池大小有限,如何让task不死等,但又不至于一旦队列满就直接reject或者让提交线程来做,但后来还是用让提交线程做事的方式来做,也就是并行暂时退化成了串行。

定时器有几个关键部分,1.定时扫描机制和设定,2.超时处理callback,3.超时前成功返回cancel.定时器的实现有很多种。下面的代码主要是团队使用的改造过的HashedWheelTimer,内部有很多细节,但不妨碍理解机制。

业务使用代码:
Java代码   收藏代码
  1. Timeout timeout = this.timer.newTimeout(new TimerTask() {  
  2.   
  3.           public void run(Timeout timeout) throws Exception {  
  4.               // 如果连接没有推送元,则断开连接  
  5.               if (!timeout.isCancelled() && conn.isConnected()) {  
  6.                   if (conn.getAttribute(CONN_META_DATA_ATTR) == null) {  
  7.                       log.error(LOGPREFX + "连接" + conn.getRemoteSocketAddress() + "没有推送元信息,主动关闭连接");  
  8.                       conn.close(false);  
  9.                   }  
  10.               }  
  11.   
  12.           }  
  13.       }, this.delay, TimeUnit.SECONDS);  
  14.       // 已经添加了,取消最新的  
  15.       if (conn.setAttributeIfAbsent(CONN_METADATA_TIMEOUT_ATTR, timeout) != null) {  
  16.           timeout.cancel();  
  17.       }  



定时器实现代码
Java代码   收藏代码
  1. /** 
  2.  * A {@link Timer} optimized for approximated I/O timeout scheduling. 
  3.  *  
  4.  * <h3>Tick Duration</h3> 
  5.  *  
  6.  * As described with 'approximated', this timer does not execute the scheduled 
  7.  * {@link TimerTask} on time. {@link HashedWheelTimer}, on every tick, will 
  8.  * check if there are any {@link TimerTask}s behind the schedule and execute 
  9.  * them. 
  10.  * <p> 
  11.  * You can increase or decrease the accuracy of the execution timing by 
  12.  * specifying smaller or larger tick duration in the constructor. In most 
  13.  * network applications, I/O timeout does not need to be accurate. Therefore, 
  14.  * the default tick duration is 100 milliseconds and you will not need to try 
  15.  * different configurations in most cases. 
  16.  *  
  17.  * <h3>Ticks per Wheel (Wheel Size)</h3> 
  18.  *  
  19.  * {@link HashedWheelTimer} maintains a data structure called 'wheel'. To put 
  20.  * simply, a wheel is a hash table of {@link TimerTask}s whose hash function is 
  21.  * 'dead line of the task'. The default number of ticks per wheel (i.e. the size 
  22.  * of the wheel) is 512. You could specify a larger value if you are going to 
  23.  * schedule a lot of timeouts. 
  24.  *  
  25.  * <h3>Implementation Details</h3> 
  26.  *  
  27.  * {@link HashedWheelTimer} is based on <a 
  28.  * href="http://cseweb.ucsd.edu/users/varghese/">George Varghese</a> and Tony 
  29.  * Lauck's paper, <a 
  30.  * href="http://cseweb.ucsd.edu/users/varghese/PAPERS/twheel.ps.Z">'Hashed and 
  31.  * Hierarchical Timing Wheels: data structures to efficiently implement a timer 
  32.  * facility'</a>. More comprehensive slides are located <a 
  33.  * href="http://www.cse.wustl.edu/~cdgill/courses/cs6874/TimingWheels.ppt" 
  34.  * >here</a>. 
  35.  *  
  36.  * @author <a href="http://www.jboss.org/netty/">The Netty Project</a> 
  37.  * @author <a href="http://gleamynode.net/">Trustin Lee</a> 
  38.  */  
  39. public class HashedWheelTimer implements Timer {  
  40.   
  41.     static final Log logger = LogFactory.getLog(HashedWheelTimer.class);  
  42.     private static final AtomicInteger id = new AtomicInteger();  
  43.   
  44.     // I'd say 64 active timer threads are obvious misuse.  
  45.     private static final int MISUSE_WARNING_THRESHOLD = 64;  
  46.     private static final AtomicInteger activeInstances = new AtomicInteger();  
  47.     private static final AtomicBoolean loggedMisuseWarning = new AtomicBoolean();  
  48.   
  49.     private final Worker worker = new Worker();  
  50.     final Thread workerThread;  
  51.     final AtomicBoolean shutdown = new AtomicBoolean();  
  52.   
  53.     private final long roundDuration;  
  54.     final long tickDuration;  
  55.     final Set<HashedWheelTimeout>[] wheel;  
  56.     final ReusableIterator<HashedWheelTimeout>[] iterators;  
  57.     final int mask;  
  58.     final ReadWriteLock lock = new ReentrantReadWriteLock();  
  59.     private volatile int wheelCursor;  
  60.   
  61.     private final AtomicInteger size = new AtomicInteger(0);  
  62.   
  63.     final int maxTimerCapacity;  
  64.   
  65.   
  66.     /** 
  67.      * Creates a new timer with the default thread factory ( 
  68.      * {@link Executors#defaultThreadFactory()}), default tick duration, and 
  69.      * default number of ticks per wheel. 
  70.      */  
  71.     public HashedWheelTimer() {  
  72.         this(Executors.defaultThreadFactory());  
  73.     }  
  74.   
  75.   
  76.     /** 
  77.      * Creates a new timer with the default thread factory ( 
  78.      * {@link Executors#defaultThreadFactory()}) and default number of ticks per 
  79.      * wheel. 
  80.      *  
  81.      * @param tickDuration 
  82.      *            the duration between tick 
  83.      * @param unit 
  84.      *            the time unit of the {@code tickDuration} 
  85.      */  
  86.     public HashedWheelTimer(long tickDuration, TimeUnit unit) {  
  87.         this(Executors.defaultThreadFactory(), tickDuration, unit);  
  88.     }  
  89.   
  90.   
  91.     /** 
  92.      * Creates a new timer with the default thread factory ( 
  93.      * {@link Executors#defaultThreadFactory()}). 
  94.      *  
  95.      * @param tickDuration 
  96.      *            the duration between tick 
  97.      * @param unit 
  98.      *            the time unit of the {@code tickDuration} 
  99.      * @param ticksPerWheel 
  100.      *            the size of the wheel 
  101.      */  
  102.     public HashedWheelTimer(long tickDuration, TimeUnit unit, int ticksPerWheel, int maxTimerCapacity) {  
  103.         this(Executors.defaultThreadFactory(), tickDuration, unit, ticksPerWheel, maxTimerCapacity);  
  104.     }  
  105.   
  106.   
  107.     /** 
  108.      * Creates a new timer with the default tick duration and default number of 
  109.      * ticks per wheel. 
  110.      *  
  111.      * @param threadFactory 
  112.      *            a {@link ThreadFactory} that creates a background 
  113.      *            {@link Thread} which is dedicated to {@link TimerTask} 
  114.      *            execution. 
  115.      */  
  116.     public HashedWheelTimer(ThreadFactory threadFactory) {  
  117.         this(threadFactory, 100, TimeUnit.MILLISECONDS);  
  118.     }  
  119.   
  120.   
  121.     /** 
  122.      * 返回当前timer个数 
  123.      *  
  124.      * @return 
  125.      */  
  126.     public int size() {  
  127.         return this.size.get();  
  128.     }  
  129.   
  130.   
  131.     /** 
  132.      * Creates a new timer with the default number of ticks per wheel. 
  133.      *  
  134.      * @param threadFactory 
  135.      *            a {@link ThreadFactory} that creates a background 
  136.      *            {@link Thread} which is dedicated to {@link TimerTask} 
  137.      *            execution. 
  138.      * @param tickDuration 
  139.      *            the duration between tick 
  140.      * @param unit 
  141.      *            the time unit of the {@code tickDuration} 
  142.      */  
  143.     public HashedWheelTimer(ThreadFactory threadFactory, long tickDuration, TimeUnit unit) {  
  144.         this(threadFactory, tickDuration, unit, 51250000);  
  145.     }  
  146.   
  147.   
  148.     /** 
  149.      * Creates a new timer. 
  150.      *  
  151.      * @param threadFactory 
  152.      *            a {@link ThreadFactory} that creates a background 
  153.      *            {@link Thread} which is dedicated to {@link TimerTask} 
  154.      *            execution. 
  155.      * @param tickDuration 
  156.      *            the duration between tick 
  157.      * @param unit 
  158.      *            the time unit of the {@code tickDuration} 
  159.      * @param ticksPerWheel 
  160.      * @param maxTimerCapacity 
  161.      *            the size of the wheel 
  162.      */  
  163.     public HashedWheelTimer(ThreadFactory threadFactory, long tickDuration, TimeUnit unit, int ticksPerWheel,  
  164.             int maxTimerCapacity) {  
  165.   
  166.         if (threadFactory == null) {  
  167.             throw new NullPointerException("threadFactory");  
  168.         }  
  169.         if (unit == null) {  
  170.             throw new NullPointerException("unit");  
  171.         }  
  172.         if (tickDuration <= 0) {  
  173.             throw new IllegalArgumentException("tickDuration must be greater than 0: " + tickDuration);  
  174.         }  
  175.         if (ticksPerWheel <= 0) {  
  176.             throw new IllegalArgumentException("ticksPerWheel must be greater than 0: " + ticksPerWheel);  
  177.         }  
  178.         if (maxTimerCapacity <= 0) {  
  179.             throw new IllegalArgumentException("maxTimerCapacity must be greater than 0: " + maxTimerCapacity);  
  180.         }  
  181.   
  182.         // Normalize ticksPerWheel to power of two and initialize the wheel.  
  183.         this.wheel = createWheel(ticksPerWheel);  
  184.         this.iterators = createIterators(this.wheel);  
  185.         this.maxTimerCapacity = maxTimerCapacity;  
  186.         this.mask = this.wheel.length - 1;  
  187.   
  188.         // Convert tickDuration to milliseconds.  
  189.         this.tickDuration = tickDuration = unit.toMillis(tickDuration);  
  190.   
  191.         // Prevent overflow.  
  192.         if (tickDuration == Long.MAX_VALUE || tickDuration >= Long.MAX_VALUE / this.wheel.length) {  
  193.             throw new IllegalArgumentException("tickDuration is too long: " + tickDuration + ' ' + unit);  
  194.         }  
  195.   
  196.         this.roundDuration = tickDuration * this.wheel.length;  
  197.   
  198.         this.workerThread =  
  199.                 threadFactory.newThread(new ThreadRenamingRunnable(this.worker, "Hashed wheel timer #"  
  200.                         + id.incrementAndGet()));  
  201.   
  202.         // Misuse check  
  203.         int activeInstances = HashedWheelTimer.activeInstances.incrementAndGet();  
  204.         if (activeInstances >= MISUSE_WARNING_THRESHOLD && loggedMisuseWarning.compareAndSet(falsetrue)) {  
  205.             logger.debug("There are too many active " + HashedWheelTimer.class.getSimpleName() + " instances ("  
  206.                     + activeInstances + ") - you should share the small number "  
  207.                     + "of instances to avoid excessive resource consumption.");  
  208.         }  
  209.     }  
  210.   
  211.   
  212.     @SuppressWarnings("unchecked")  
  213.     private static Set<HashedWheelTimeout>[] createWheel(int ticksPerWheel) {  
  214.         if (ticksPerWheel <= 0) {  
  215.             throw new IllegalArgumentException("ticksPerWheel must be greater than 0: " + ticksPerWheel);  
  216.         }  
  217.         if (ticksPerWheel > 1073741824) {  
  218.             throw new IllegalArgumentException("ticksPerWheel may not be greater than 2^30: " + ticksPerWheel);  
  219.         }  
  220.   
  221.         ticksPerWheel = normalizeTicksPerWheel(ticksPerWheel);  
  222.         Set<HashedWheelTimeout>[] wheel = new Set[ticksPerWheel];  
  223.         for (int i = 0; i < wheel.length; i++) {  
  224.             wheel[i] =  
  225.                     new MapBackedSet<HashedWheelTimeout>(new ConcurrentIdentityHashMap<HashedWheelTimeout, Boolean>(16,  
  226.                         0.95f, 4));  
  227.         }  
  228.         return wheel;  
  229.     }  
  230.   
  231.   
  232.     @SuppressWarnings("unchecked")  
  233.     private static ReusableIterator<HashedWheelTimeout>[] createIterators(Set<HashedWheelTimeout>[] wheel) {  
  234.         ReusableIterator<HashedWheelTimeout>[] iterators = new ReusableIterator[wheel.length];  
  235.         for (int i = 0; i < wheel.length; i++) {  
  236.             iterators[i] = (ReusableIterator<HashedWheelTimeout>) wheel[i].iterator();  
  237.         }  
  238.         return iterators;  
  239.     }  
  240.   
  241.   
  242.     private static int normalizeTicksPerWheel(int ticksPerWheel) {  
  243.         int normalizedTicksPerWheel = 1;  
  244.         while (normalizedTicksPerWheel < ticksPerWheel) {  
  245.             normalizedTicksPerWheel <<= 1;  
  246.         }  
  247.         return normalizedTicksPerWheel;  
  248.     }  
  249.   
  250.   
  251.     /** 
  252.      * Starts the background thread explicitly. The background thread will start 
  253.      * automatically on demand even if you did not call this method. 
  254.      *  
  255.      * @throws IllegalStateException 
  256.      *             if this timer has been {@linkplain #stop() stopped} already 
  257.      */  
  258.     public synchronized void start() {  
  259.         if (this.shutdown.get()) {  
  260.             throw new IllegalStateException("cannot be started once stopped");  
  261.         }  
  262.   
  263.         if (!this.workerThread.isAlive()) {  
  264.             this.workerThread.start();  
  265.         }  
  266.     }  
  267.   
  268.   
  269.     public synchronized Set<Timeout> stop() {  
  270.         if (!this.shutdown.compareAndSet(falsetrue)) {  
  271.             return Collections.emptySet();  
  272.         }  
  273.   
  274.         boolean interrupted = false;  
  275.         while (this.workerThread.isAlive()) {  
  276.             this.workerThread.interrupt();  
  277.             try {  
  278.                 this.workerThread.join(100);  
  279.             }  
  280.             catch (InterruptedException e) {  
  281.                 interrupted = true;  
  282.             }  
  283.         }  
  284.   
  285.         if (interrupted) {  
  286.             Thread.currentThread().interrupt();  
  287.         }  
  288.   
  289.         activeInstances.decrementAndGet();  
  290.   
  291.         Set<Timeout> unprocessedTimeouts = new HashSet<Timeout>();  
  292.         for (Set<HashedWheelTimeout> bucket : this.wheel) {  
  293.             unprocessedTimeouts.addAll(bucket);  
  294.             bucket.clear();  
  295.         }  
  296.   
  297.         return Collections.unmodifiableSet(unprocessedTimeouts);  
  298.     }  
  299.   
  300.   
  301.     public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {  
  302.         final long currentTime = System.currentTimeMillis();  
  303.   
  304.         if (task == null) {  
  305.             throw new NullPointerException("task");  
  306.         }  
  307.         if (unit == null) {  
  308.             throw new NullPointerException("unit");  
  309.         }  
  310.   
  311.         delay = unit.toMillis(delay);  
  312.         if (delay < this.tickDuration) {  
  313.             delay = this.tickDuration;  
  314.         }  
  315.   
  316.         if (!this.workerThread.isAlive()) {  
  317.             this.start();  
  318.         }  
  319.   
  320.         if (this.size.get() >= this.maxTimerCapacity) {  
  321.             throw new RejectedExecutionException("Timer size " + this.size + " is great than maxTimerCapacity "  
  322.                     + this.maxTimerCapacity);  
  323.         }  
  324.   
  325.         // Prepare the required parameters to create the timeout object.  
  326.         HashedWheelTimeout timeout;  
  327.         final long lastRoundDelay = delay % this.roundDuration;  
  328.         final long lastTickDelay = delay % this.tickDuration;  
  329.   
  330.         final long relativeIndex = lastRoundDelay / this.tickDuration + (lastTickDelay != 0 ? 1 : 0);  
  331.   
  332.         final long deadline = currentTime + delay;  
  333.   
  334.         final long remainingRounds = delay / this.roundDuration - (delay % this.roundDuration == 0 ? 1 : 0);  
  335.   
  336.         // Add the timeout to the wheel.  
  337.         this.lock.readLock().lock();  
  338.         try {  
  339.             timeout =  
  340.                     new HashedWheelTimeout(task, deadline, (int) (this.wheelCursor + relativeIndex & this.mask),  
  341.                         remainingRounds);  
  342.   
  343.             this.wheel[timeout.stopIndex].add(timeout);  
  344.         }  
  345.         finally {  
  346.             this.lock.readLock().unlock();  
  347.         }  
  348.         this.size.incrementAndGet();  
  349.   
  350.         return timeout;  
  351.     }  
  352.   
  353.     private final class Worker implements Runnable {  
  354.   
  355.         private long startTime;  
  356.         private long tick;  
  357.   
  358.   
  359.         Worker() {  
  360.             super();  
  361.         }  
  362.   
  363.   
  364.         public void run() {  
  365.             List<HashedWheelTimeout> expiredTimeouts = new ArrayList<HashedWheelTimeout>();  
  366.   
  367.             this.startTime = System.currentTimeMillis();  
  368.             this.tick = 1;  
  369.   
  370.             while (!HashedWheelTimer.this.shutdown.get()) {  
  371.                 this.waitForNextTick();  
  372.                 this.fetchExpiredTimeouts(expiredTimeouts);  
  373.                 this.notifyExpiredTimeouts(expiredTimeouts);  
  374.             }  
  375.         }  
  376.   
  377.   
  378.         private void fetchExpiredTimeouts(List<HashedWheelTimeout> expiredTimeouts) {  
  379.   
  380.             // Find the expired timeouts and decrease the round counter  
  381.             // if necessary. Note that we don't send the notification  
  382.             // immediately to make sure the listeners are called without  
  383.             // an exclusive lock.  
  384.             HashedWheelTimer.this.lock.writeLock().lock();  
  385.             try {  
  386.                 int oldBucketHead = HashedWheelTimer.this.wheelCursor;  
  387.   
  388.                 int newBucketHead = oldBucketHead + 1 & HashedWheelTimer.this.mask;  
  389.                 HashedWheelTimer.this.wheelCursor = newBucketHead;  
  390.   
  391.                 ReusableIterator<HashedWheelTimeout> i = HashedWheelTimer.this.iterators[oldBucketHead];  
  392.                 this.fetchExpiredTimeouts(expiredTimeouts, i);  
  393.             }  
  394.             finally {  
  395.                 HashedWheelTimer.this.lock.writeLock().unlock();  
  396.             }  
  397.         }  
  398.   
  399.   
  400.         private void fetchExpiredTimeouts(List<HashedWheelTimeout> expiredTimeouts,  
  401.                 ReusableIterator<HashedWheelTimeout> i) {  
  402.   
  403.             long currentDeadline = System.currentTimeMillis() + HashedWheelTimer.this.tickDuration;  
  404.             i.rewind();  
  405.             while (i.hasNext()) {  
  406.                 HashedWheelTimeout timeout = i.next();  
  407.                 if (timeout.remainingRounds <= 0) {  
  408.                     if (timeout.deadline < currentDeadline) {  
  409.                         i.remove();  
  410.                         expiredTimeouts.add(timeout);  
  411.                     }  
  412.                     else {  
  413.                         // A rare case where a timeout is put for the next  
  414.                         // round: just wait for the next round.  
  415.                     }  
  416.                 }  
  417.                 else {  
  418.                     timeout.remainingRounds--;  
  419.                 }  
  420.             }  
  421.         }  
  422.   
  423.   
  424.         private void notifyExpiredTimeouts(List<HashedWheelTimeout> expiredTimeouts) {  
  425.             // Notify the expired timeouts.  
  426.             for (int i = expiredTimeouts.size() - 1; i >= 0; i--) {  
  427.                 expiredTimeouts.get(i).expire();  
  428.                 HashedWheelTimer.this.size.decrementAndGet();  
  429.             }  
  430.   
  431.             // Clean up the temporary list.  
  432.             expiredTimeouts.clear();  
  433.   
  434.         }  
  435.   
  436.   
  437.         private void waitForNextTick() {  
  438.             for (;;) {  
  439.                 final long currentTime = System.currentTimeMillis();  
  440.                 final long sleepTime = HashedWheelTimer.this.tickDuration * this.tick - (currentTime - this.startTime);  
  441.   
  442.                 if (sleepTime <= 0) {  
  443.                     break;  
  444.                 }  
  445.   
  446.                 try {  
  447.                     Thread.sleep(sleepTime);  
  448.                 }  
  449.                 catch (InterruptedException e) {  
  450.                     if (HashedWheelTimer.this.shutdown.get()) {  
  451.                         return;  
  452.                     }  
  453.                 }  
  454.             }  
  455.   
  456.             // Reset the tick if overflow is expected.  
  457.             if (HashedWheelTimer.this.tickDuration * this.tick > Long.MAX_VALUE - HashedWheelTimer.this.tickDuration) {  
  458.                 this.startTime = System.currentTimeMillis();  
  459.                 this.tick = 1;  
  460.             }  
  461.             else {  
  462.                 // Increase the tick if overflow is not likely to happen.  
  463.                 this.tick++;  
  464.             }  
  465.         }  
  466.     }  
  467.   
  468.     private final class HashedWheelTimeout implements Timeout {  
  469.   
  470.         private final TimerTask task;  
  471.         final int stopIndex;  
  472.         final long deadline;  
  473.         volatile long remainingRounds;  
  474.         private volatile boolean cancelled;  
  475.   
  476.   
  477.         HashedWheelTimeout(TimerTask task, long deadline, int stopIndex, long remainingRounds) {  
  478.             this.task = task;  
  479.             this.deadline = deadline;  
  480.             this.stopIndex = stopIndex;  
  481.             this.remainingRounds = remainingRounds;  
  482.         }  
  483.   
  484.   
  485.         public Timer getTimer() {  
  486.             return HashedWheelTimer.this;  
  487.         }  
  488.   
  489.   
  490.         public TimerTask getTask() {  
  491.             return this.task;  
  492.         }  
  493.   
  494.   
  495.         public void cancel() {  
  496.             if (this.isExpired()) {  
  497.                 return;  
  498.             }  
  499.   
  500.             this.cancelled = true;  
  501.             // Might be called more than once, but doesn't matter.  
  502.             if (HashedWheelTimer.this.wheel[this.stopIndex].remove(this)) {  
  503.                 HashedWheelTimer.this.size.decrementAndGet();  
  504.             }  
  505.         }  
  506.   
  507.   
  508.         public boolean isCancelled() {  
  509.             return this.cancelled;  
  510.         }  
  511.   
  512.   
  513.         public boolean isExpired() {  
  514.             return this.cancelled || System.currentTimeMillis() > this.deadline;  
  515.         }  
  516.   
  517.   
  518.         public void expire() {  
  519.             if (this.cancelled) {  
  520.                 return;  
  521.             }  
  522.   
  523.             try {  
  524.                 this.task.run(this);  
  525.             }  
  526.             catch (Throwable t) {  
  527.                 logger.warn("An exception was thrown by " + TimerTask.class.getSimpleName() + ".", t);  
  528.             }  
  529.         }  
  530.   
  531.   
  532.         @Override  
  533.         public String toString() {  
  534.             long currentTime = System.currentTimeMillis();  
  535.             long remaining = this.deadline - currentTime;  
  536.   
  537.             StringBuilder buf = new StringBuilder(192);  
  538.             buf.append(this.getClass().getSimpleName());  
  539.             buf.append('(');  
  540.   
  541.             buf.append("deadline: ");  
  542.             if (remaining > 0) {  
  543.                 buf.append(remaining);  
  544.                 buf.append(" ms later, ");  
  545.             }  
  546.             else if (remaining < 0) {  
  547.                 buf.append(-remaining);  
  548.                 buf.append(" ms ago, ");  
  549.             }  
  550.             else {  
  551.                 buf.append("now, ");  
  552.             }  
  553.   
  554.             if (this.isCancelled()) {  
  555.                 buf.append(", cancelled");  
  556.             }  
  557.   
  558.             return buf.append(')').toString();  
  559.         }  
  560.     }  

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值