DelayQueue延迟队列和Redis缓存实现订单自动取消功能

DelayQueue延迟队列和Redis缓存实现订单自动取消功能

  • 前言
  • 一、加入redis依赖
  • 二、开始撸代码
    • 1.订单队列对象主要记录订单id和订单失效时间
    • 2.编写队列业务层
    • 3.创建线程池,用于订单创建的时候将订单id加入到队列中
    • 4.编写Redis业务层,主要用来将订单存入缓存和便利缓存对象到队列中
    • 5.编写redis业务层实现类
    • 6. 考虑到系统宕机后会将队列中的数据删除掉,服务器重启后数据消失的情况,下面加入监听器,在服务启动时去redis中查找未支付的订单并重新加入到队列中,前提是redis的数据没有被删除
    • 7. 订单创建成功之后将订单id和超时时间放入队列和redis中
    • 8. 订单支付成功之后从队列和redis中移除订单信息
  • 总结


前言

最近项目中需要使用到订单倒计时取消功能,刚开始是想到使用一个定时器去跑,每隔多少时间去数据查,然后在根据订单创建时间计算出是否要过期在一一改变订单状态,而这种方式虽然说能可以解决这个问题,但是在资源上就会比较损耗,我接下来的是使用jdk自带的延时队列DelayQueue和redis来实现,redis相信大家都比较熟悉了,DelayQueue大家可以自行百度。


提示:以下是本篇文章正文内容,下面案例可供参考

一、加入redis依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

二、开始撸代码

1.订单队列对象主要记录订单id和订单失效时间

代码如下:

package com.ctb.obd.order.time;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

@Data
@ApiModel(description = "订单队列对象")
public class DshOrder implements Delayed {
    @ApiModelProperty(value = "订单id")
    private String orderId;
    @ApiModelProperty(value = "超时时间")
    private long startTime;
    /**
     * orderId:订单id
     * timeout:自动取消订单的超时时间,秒
     * */
    public DshOrder(String orderId, int timeout){
        this.orderId = orderId;
        this.startTime = System.currentTimeMillis() + timeout*1000L;
    }
    @Override
    public int compareTo(Delayed other) {
        if (other == this){
            return 0;
        }
        if(other instanceof DshOrder){
            DshOrder otherRequest = (DshOrder)other;
            long otherStartTime = otherRequest.getStartTime();
            return (int)(this.startTime - otherStartTime);
        }
        return 0;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(startTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }

}

2.编写队列业务层

代码如下:

package com.ctb.obd.order.time;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.concurrent.DelayQueue;

@Slf4j
@Service
@Data
public class DelayService {
    private boolean start ;
    private OnDelayedListener listener;
    private DelayQueue<DshOrder> delayQueue = new DelayQueue<DshOrder>();

    public static interface OnDelayedListener{
        public void onDelayedArrived(DshOrder order);
    }

    public void start(OnDelayedListener listener){
        if(start){
            return;
        }
        log.error("DelayService 启动");
        start = true;
        this.listener = listener;
        new Thread(new Runnable(){
            @Override
            public void run(){
                try{
                    while(true){
                        DshOrder order = delayQueue.take();
                        if(DelayService.this.listener != null){
                            DelayService.this.listener.onDelayedArrived(order);
                        }
                    }
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
        }).start();
    }

    public void add(DshOrder order){
        delayQueue.put(order);
    }

    public void remove(String orderId){
        DshOrder[] array = delayQueue.toArray(new DshOrder[]{});
        if(array == null || array.length <= 0){
            return;
        }
        DshOrder target = null;
        for(DshOrder order : array){
            if(order.getOrderId() == orderId){
                target = order;
                break;
            }
        }
        if(target != null){
            delayQueue.remove(target);
        }
    }

}

3.创建线程池,用于订单创建的时候将订单id加入到队列中

代码如下:

package com.ctb.obd.order.time;

import com.google.common.util.concurrent.ThreadFactoryBuilder;

import java.util.concurrent.*;

public class ThreadPoolUtils {

    private final ExecutorService executor;

    private static ThreadPoolUtils instance = new ThreadPoolUtils();

    private ThreadPoolUtils() {
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("order-runner-%d").build();
        int size = Runtime.getRuntime().availableProcessors() * 2;
        this.executor = new ThreadPoolExecutor(size,size,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(),namedThreadFactory);
    }

    public static ThreadPoolUtils getInstance() {
        return instance;
    }

    public static <T> Future<T> execute(final Callable<T> runnable) {
        return getInstance().executor.submit(runnable);
    }

    public static Future<?> execute(final Runnable runnable) {
        return getInstance().executor.submit(runnable);
    }


}

4.编写Redis业务层,主要用来将订单存入缓存和便利缓存对象到队列中

代码如下:

package com.ctb.obd.order.service;
import com.ctb.obd.order.model.Order;

import java.util.Set;

public interface OrderRedisService {
    /**
     * 订单对象加入缓存
     *
     * @param orderId     订单id
     * @param orderObject 订单对象
     */
    void saveOrder(String orderId, Order orderObject);

    /**
     * 获得缓存订单对象
     *
     * @param orderId 订单id
     * @return
     */
    String getOrder(String orderId);

    /**
     * 删除缓存订单对象
     *
     * @param orderId 订单id
     */
    void deleteOrder(String orderId);

    /**
     * 查询所有需要缓存的订单对象
     *
     * @return
     */
    Set<String> sacn();

    /**
     * 获得redis键的剩余时间
     *
     * @param key redis键
     * @return 剩余时间
     */
    Long getSurplusTime(String key);

}

5.编写redis业务层实现类

代码如下:

package com.ctb.obd.order.service.impl;

import com.alibaba.fastjson.JSON;
import com.ctb.obd.order.model.Order;
import com.ctb.obd.order.service.OrderRedisService;
import com.ctb.obd.order.time.DelayService;
import com.ctb.obd.order.time.DshOrder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class OrderRedisServiceImpl implements OrderRedisService {
    @Autowired
    private StringRedisTemplate redisTemplate;

    @Resource
    private DelayService delayService;

    /**
     * 保存订单并设置过期时间
     * @param outTradeId
     * @param redisDo
     */
    @Override
    public void saveOrder(String outTradeId, Order redisDo) {
        String key = outTradeId;
        //key过期时间为120分钟
        redisTemplate.opsForValue().set(key, JSON.toJSONString(redisDo), 120, TimeUnit.MINUTES);
        //加入队列
        delayService.add(new DshOrder(redisDo.getId().toString(), 7200));
    }

    /**
     * 获取订单
     * @param outTradeNo
     * @return
     */
    @Override
    public String getOrder(String outTradeNo) {
        String key = outTradeNo;
        String message = redisTemplate.opsForValue().get(key);
        if(message != null){
            return key;
        }
        return "";
    }

    /**
     * 删除订单
     * @param outTradeNo
     */
    @Override
    public void deleteOrder(String outTradeNo) {
        String key = outTradeNo;
        redisTemplate.delete(key);
    }

    /**
     * 获取订单中所有的key
     * @return
     */
    @Override
    public Set<String> sacn(){
        Set<String> execute = redisTemplate.execute(new RedisCallback<Set<String>>() {
            @Override
            public Set<String> doInRedis(RedisConnection connection) throws DataAccessException {
                Set<String> binaryKeys = new HashSet<>();
                Cursor<byte[]> cursor = connection.scan( new ScanOptions.ScanOptionsBuilder().match("order*").count(100).build());
                while (cursor.hasNext()) {
                    binaryKeys.add(new String(cursor.next()));
                }
                return binaryKeys;
            }
        });
        return execute;
    }

    @Override
    public Long getSurplusTime(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }
}

6. 考虑到系统宕机后会将队列中的数据删除掉,服务器重启后数据消失的情况,下面加入监听器,在服务启动时去redis中查找未支付的订单并重新加入到队列中,前提是redis的数据没有被删除

package com.ctb.obd.order.time;

import com.ctb.obd.order.service.OrderRedisService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Service;

import java.util.Set;

@Slf4j
@Service
public class StartupListener implements ApplicationListener<ContextRefreshedEvent> {

    @Autowired
    DelayService delayService;
    @Autowired
    OrderRedisService redisService;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent evt) {
        log.info(">>>>>>>>>>>>系统启动完成,onApplicationEvent()");

        //自动取消订单
        delayService.start(new DelayService.OnDelayedListener(){
            @Override
            public void onDelayedArrived(final DshOrder order) {
                //异步来做
                ThreadPoolUtils.execute(new Runnable(){
                    @Override
                    public void run(){
                        String orderId = order.getOrderId();
                        //查库判断是否需要自动取消订单
                        int surpsTime = redisService.getSurplusTime(orderId).intValue();
                        log.info("redis键:" + orderId + ";剩余过期时间:"+surpsTime);
                        if(surpsTime > 0){
                            log.info("没有需要取消的订单!");
                        }else{
                            log.info("自动取消订单,删除队列:"+orderId);
                            //从队列中删除
                            delayService.remove(orderId);
                            //从redis删除
                            redisService.deleteOrder("order"+orderId);
                            log.info("自动取消订单,删除redis:"+orderId);
                            //todo 对订单进行取消订单操作 修改订单状态

                        }
                    }
                });
            }
        });
        //查找需要入队的订单
        ThreadPoolUtils.execute(new Runnable(){
            @Override
            public void run() {
                log.info("查找需要入队的订单");
                Set<String> keys = redisService.sacn();
                if(keys == null || keys.size() <= 0){
                    return;
                }
                log.info("需要入队的订单keys:"+keys);
                log.info("写到DelayQueue");
                for(String key : keys){
                    String orderKey = redisService.getOrder(key);
                    int surpsTime = redisService.getSurplusTime(key).intValue();
                    log.info("读redis,key:"+key);
                    log.info("redis键:" + key + ";剩余过期时间:"+surpsTime);
                    if(orderKey != null){
                        DshOrder dshOrder = new DshOrder(orderKey,surpsTime);
                        delayService.add(dshOrder);
                        log.info("订单自动入队:"+dshOrder);
                    }
                }
            }
        });
    }
}

7. 订单创建成功之后将订单id和超时时间放入队列和redis中

//订单生成之后把订单插入到待取消的队列和redis
        ThreadPoolUtils.execute(new Runnable() {
            @Override
            public void run() {
                String itrOrderId = "order" + orderId;
                //1 插入到待收货队列
                DshOrder dshOrder = new DshOrder(itrOrderId, 600);
                delayService.add(dshOrder);
                log.error("订单order" + orderId + "入队列");
                //2插入到redis
                orderRedisService.saveOrder(itrOrderId, orderObject);
                log.error("订单order" + orderId + "入redis缓存");
            }
        });

8. 订单支付成功之后从队列和redis中移除订单信息

String delOrderId = "order" + orderId;
        int surpsTime = orderRedisService.getSurplusTime(delOrderId).intValue();
        log.error("redis键:" + delOrderId + ";剩余过期时间:"+surpsTime);
        if (surpsTime <= 0) {
            delayService.remove(delOrderId);
            log.error("订单手动出队:" + delOrderId);
            orderRedisService.deleteOrder(delOrderId);
            log.error("订单手动出redis:" + delOrderId);
        }

总结

以上就是今天讲的内容,也可以使用单独使用DelayQueue延迟队列完成,只不过加了redis之后防止服务器宕机数据丢失的情况,下期我们使用RabbitMQ 延时队列和死信队列完成订单倒计时取消的功能

你可能感兴趣的:(springboot,java,redis)