关于任务补偿的那些儿事

845 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情

前言

涉及到生成订单的系统,相信大家都遇到过订单生成失败或者异常的情况,由于业务逻辑服的复杂性,会使我们的订单生成失败的原因很多,当然我们不能坐以待毙,一般的情况下我们就需要让这些失败的订单使它生成成功,给公司增加点收入..在失败订单数据量很少的情况下我们很好处理,后台开发人员,手动修数据就好了, 但是在实际公司订单生成量比较大,或者说数据库失败的订单比较多的情况下,我们应该如何去重试补偿这些数据。

定时任务补偿重试

由于目前公司,一天的订单量不是很大,订单组的同事都很忙,没有时间配合我们开发, 所以我们直接在项目中连接订单系统的DB,只读操作和更新我们加的补偿、重试字段,不去影响订单表的其他字段,采用的解决方式是通过xxl-job启动两个定时任务,一个重试、一个补偿的定时任务去对失败的订单进行重试补偿处理,但是我觉得这是一个优先级比较低的一个选择,频繁的去扫表,会造成网络IO和磁盘IO的消耗,所以这种做法真的很low。

65B96B8B-2C99-4AB9-B05D-D1A1D7BB29EB.png

rabbitmq延迟队列

Rabbitmq本身没有延迟队列,是基于死信交换机和消息的存活时间TTL,来实现的。

关于死信交换机,定义一个交换机可以对应多个死信队列。在消息被consumer拒收,或者设置消息的TTL时间消息过期了,就会被放到死信队列。 这里消息和队列都可以设置TTL,最终取最小的的那个TTL时间。

TTL过期时间一种是通过x-message-tt给当前的队列所有的消息设置过期时间。

 //创建direct交换机
    @Bean
    public DirectExchange ttlDirectExchange(){
        return new DirectExchange(“ttl-direct-exchange”,true,false);
    }
    /**
     * 创建队列
     * durable:是否持久化
     * exclusive:默认false,只能在当前创建连接时使用,连接关闭后队列自动删除,该优先级高于durable
     * autoDelete:是否自动删除,当没有生产者或消费者使用该交换机时,会自动删除
     */
    //创建ttl队列
    @Bean
    public Queue ttlQueue(){
        Map<String, Object> map = new HashMap<>();
        //设置过期时间为10s
        map.put(“x-message-ttl”,10000);
        return new Queue(“ttlQueue”,true,false,false,map);
    }

另一种是通过Expiration参数给单个消息设置过期时间‘

    @Test
    void testDirect() {
        MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                //在此进行TTL设置           message.getMessageProperties().setExpiration(“10000”);          message.getMessageProperties().setContentEncoding(“UTF-8”);
                return message;
            }
        };

以上是对死信队列和ttl的设置,接下来说死信如何处理订单系统失败的订单。

关于任务补偿的那些儿事 #后端技术学习

前言

涉及到生成订单的系统,相信大家都遇到过订单生成失败或者异常的情况,由于业务逻辑服的复杂性,会使我们的订单生成失败的原因很多,当然我们不能坐以待毙,一般的情况下我们就需要让这些失败的订单使它生成成功,给公司增加点收入..在失败订单数据量很少的情况下我们很好处理,后台开发人员,手动修数据就好了, 但是在实际公司订单生成量比较大,或者说数据库失败的订单比较多的情况下,我们应该如何去重试补偿这些数据。

定时任务补偿重试

由于目前公司,一天的订单量不是很大,订单组的同事都很忙,没有时间配合我们开发, 所以我们直接在项目中连接订单系统的DB,只读操作和更新我们加的补偿、重试字段,不去影响订单表的其他字段,采用的解决方式是通过xxl-job启动两个定时任务,一个重试、一个补偿的定时任务去对失败的订单进行重试补偿处理,但是我觉得这是一个优先级比较低的一个选择,频繁的去扫表,会造成网络IO和磁盘IO的消耗,所以这种做法真的很low。

5CCC55B6-80AD-4F38-9703-8942DB2FBA4B.png

rabbitmq延迟队列

Rabbitmq本身没有延迟队列,是基于死信交换机和消息的存活时间TTL,来实现的。

关于死信交换机,定义一个交换机可以对应多个死信队列。在消息被consumer拒收,或者设置消息的TTL时间消息过期了,就会被放到死信队列。 这里消息和队列都可以设置TTL,最终取最小的的那个TTL时间。

TTL过期时间一种是通过x-message-tt给当前的队列所有的消息设置过期时间。

 //创建direct交换机
    @Bean
    public DirectExchange ttlDirectExchange(){
        return new DirectExchange(“ttl-direct-exchange”,true,false);
    }
    /**
     * 创建队列
     * durable:是否持久化
     * exclusive:默认false,只能在当前创建连接时使用,连接关闭后队列自动删除,该优先级高于durable
     * autoDelete:是否自动删除,当没有生产者或消费者使用该交换机时,会自动删除
     */
    //创建ttl队列
    @Bean
    public Queue ttlQueue(){
        Map<String, Object> map = new HashMap<>();
        //设置过期时间为10s
        map.put(“x-message-ttl”,10000);
        return new Queue(“ttlQueue”,true,false,false,map);
    }

另一种是通过Expiration参数给单个消息设置过期时间‘

    @Test
    void testDirect() {
        MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                //在此进行TTL设置           message.getMessageProperties().setExpiration(“10000”);          message.getMessageProperties().setContentEncoding(“UTF-8”);
                return message;
            }
        };

以上是对死信队列和ttl的设置,接下来说死信如何处理订单系统失败的订单。

5CCC55B6-80AD-4F38-9703-8942DB2FBA4B.png

订单系统生成失败的订单,订单系统订单状态处于生成中,在生成中失败的,或者已经创建订单长时间不处理的都会进入死信队列,建立补偿消费者队列监听死信队列去重新生成订单,订单生成成功后给结算系统发送消息,通知结算系统。

Redis延时队列实现

Redis延迟队列优势

1.zset数据结构支持高性能的score排序 2.内存操作,速度非常快 3.redis有哨兵和cluster集群模式,当消息多的时候可以使用集群模式处理 4.redis的持久化机制,保证数据的可持久性

Redis延迟队列优势

1.mq具有生产者的confirm机制和消费者的ack确认机制 2.没有重试机制,需要自己实现和重试次数

延迟队列设计思路

1.将redis做消息池 KV结构:K=prefix+projectName field = topic+jobId V=CONENT;V由客户端传入的数据,消费的时候回传 2.zset做延迟优先队列,score做优先级 3.list结构,做消息队列先进先出的方式进行消费 4.zset和list存储消息地址 5.点对点消息从zset路由到list队列 6.定时器维护路由 7.ttl规则实现消息的延迟

具体实现

1.新增加JOB,ZING:DELAY_QUEUE:JOB_POOL插入一条数据记录业务消费方;ZING:DELAY_QUEUE:BUCKET### 也会插入一条记录,记录执行的时间戳 2.开启定时任务定时去扫 ZING:DELAY_QUEUE:BUCKET,相当于搬运线程查找执行时间戳比当前时间小就全部删除,同时解析出每个任务的topic,将这些任务push到list结构ZING:DELAY_QUEUE:QUEUE 3.创建监听线程去批量获取list待消费的消息,获取到就丢给这个路由的消费线程 4.消费现在在去JOB池查找数据结构,返回回调结构,执行回调方法。

出现的问题解决

保证消息执行的时效性,设置定时器每隔1秒去请求redis获取是否有待消费的job,如果一直没有任务就失去了扫描的意义,List任务队列有BLPOP阻塞,如果list中没有数据就会一直阻塞,可以设置阻塞时间。

Redis分布式锁保证消费的重复消费问题 分布式锁保证定时器的执行频率

代码实现

核心Job实体类


@Data
public class Job implements Serializable {
 
 private static final long serialVersionUID = 1L;
 
 /**
 * Job的唯一标识。用来检索和删除指定的Job信息
 */
 @NotBlank
 private String jobId;
 
 
 /**
 * Job类型。可以理解成具体的业务名称
 */
 @NotBlank
 private String topic;
 
 /**
 * Job需要延迟的时间。单位:秒。(服务端会将其转换为绝对时间)
 */
 private Long delay;
 
 /**
 * Job的内容,供消费者做具体的业务处理,以json格式存储
 */
 @NotBlank
 private String body;
 
 /**
 * 失败重试次数
 */
 private int retry = 0;
 
 /**
 * 通知URL
 */
 @NotBlank
 private String url;
}

删除Job实体

@Data
public class JobDie implements Serializable {
 
 private static final long serialVersionUID = 1L;
 
 /**
 * Job的唯一标识。用来检索和删除指定的Job信息
 */
 @NotBlank
 private String jobId;
 
 
 /**
 * Job类型。可以理解成具体的业务名称
 */
 @NotBlank
 pri

搬运线程

@Slf4j
@Component
public class CarryJobScheduled {
 
 @Autowired
 private RedissonClient redissonClient;
 
 /**
 * 启动定时开启搬运JOB信息
 */
 @Scheduled(cron = "*/1 * * * * *")
 public void carryJobToQueue() {
 System.out.println("carryJobToQueue --->");

 //分布式锁控制定时任务执行频率
 RLock lock = redissonClient.getLock(RedisQueueKey.CARRY_THREAD_LOCK);

 try {
 boolean lockFlag = lock.tryLock(LOCK_WAIT_TIME, LOCK_RELEASE_TIME, TimeUnit.SECONDS);
 if (!lockFlag) {
 throw new BusinessException(ErrorMessageEnum.ACQUIRE_LOCK_FAIL);
 }
 RScoredSortedSet<Object> bucketSet = redissonClient.getScoredSortedSet(RD_ZSET_BUCKET_PRE);
 long now = System.currentTimeMillis();
 //获取到目前为止的需要执行的job消息
 Collection<Object> jobCollection = bucketSet.valueRange(0, false, now, true);
 //找到对应list里面的待消费的消息
 List<String> jobList = jobCollection.stream().map(String::valueOf).collect(Collectors.toList());
 //将带消费的消息加入list队列
 RList<String> readyQueue = redissonClient.getList(RD_LIST_TOPIC_PRE);
 readyQueue.addAll(jobList);
 bucketSet.removeAllAsync(jobList);
 } catch (InterruptedException e) {
 log.error("carryJobToQueue error", e);
 } finally {
 if (lock != null) {
 lock.unlock();
 }
 }
 }
}

消费线程

@Slf4j
@Component
public class ReadyQueueContext {
 
 @Autowired
 private RedissonClient redissonClient;
 
 @Autowired
 private ConsumerService consumerService;
 
 /**
 * TOPIC消费线程
 */
 @PostConstruct
 public void startTopicConsumer() {
 TaskManager.doTask(this::runTopicThreads, “开启TOPIC消费线程”);
 }
 
 /**
 * 开启TOPIC消费线程
 * 将所有可能出现的异常全部catch住,确保While(true)能够不中断
 */
 @SuppressWarnings(“InfiniteLoopStatement”)
 private void runTopicThreads() {
 while (true) {
 RLock lock = null;
 try {
 lock = redissonClient.getLock(CONSUMER_TOPIC_LOCK);
 } catch (Exception e) {
 log.error(“runTopicThreads getLock error”, e);
 }
 try {
 if (lock == null) {
 continue;
 }
 // 分布式锁时间比Blpop阻塞时间多1S,避免出现释放锁的时候,锁已经超时释放,unlock报错
 boolean lockFlag = lock.tryLock(LOCK_WAIT_TIME, LOCK_RELEASE_TIME, TimeUnit.SECONDS);
 if (!lockFlag) {
 continue;
 }
 
 // 1. 获取ReadyQueue中待消费的数据
 RBlockingQueue<String> queue = redissonClient.getBlockingQueue(RD_LIST_TOPIC_PRE);
 String topicId = queue.poll(60, TimeUnit.SECONDS);
 if (StringUtils.isEmpty(topicId)) {
 continue;
 }
 
 // 2. 获取job元信息内容
 RMap<String, Job> jobPoolMap = redissonClient.getMap(JOB_POOL_KEY);
 Job job = jobPoolMap.get(topicId);
 
 // 3. 消费
 FutureTask<Boolean> taskResult = TaskManager.doFutureTask(() -> consumerService.consumerMessage(job.getUrl(), job.getBody()), job.getTopic() + “—>消费JobId—>” + job.getJobId());
 if (taskResult.get()) {
 // 3.1 消费成功,删除JobPool和DelayBucket的job信息
 jobPoolMap.remove(topicId);
 } else {
 int retrySum = job.getRetry() + 1;
 // 3.2 消费失败,则根据策略重新加入Bucket
 
 // 如果重试次数大于5,则将jobPool中的数据删除,持久化到DB
 if (retrySum > RetryStrategyEnum.RETRY_FIVE.getRetry()) {
 jobPoolMap.remove(topicId);
 continue;
 }
 job.setRetry(retrySum);
 long nextTime = job.getDelay() + RetryStrategyEnum.getDelayTime(job.getRetry()) * 1000;
 log.info(“next retryTime is [{}]”, DateUtil.long2Str(nextTime));
 RScoredSortedSet<Object> delayBucket = redissonClient.getScoredSortedSet(RedisQueueKey.RD_ZSET_BUCKET_PRE);
 delayBucket.add(nextTime, topicId);
 // 3.3 更新元信息失败次数
 jobPoolMap.put(topicId, job);
 }
 } catch (Exception e) {
 log.error(“runTopicThreads error”, e);
 } finally {
 if (lock != null) {
 try {
 lock.unlock();
 } catch (Exception e) {
 log.error(“runTopicThreads unlock error”, e);
 }
 }
 }
 }
 }
}

往Job消息池里面添加job

 /**
 * 添加job元信息
 *
 * @param job 元信息
 */
 @Override
 public void addJob(Job job) {
 
 RLock lock = redissonClient.getLock(ADD_JOB_LOCK + job.getJobId());
 try {
 boolean lockFlag = lock.tryLock(LOCK_WAIT_TIME, LOCK_RELEASE_TIME, TimeUnit.SECONDS);
 if (!lockFlag) {
 throw new BusinessException(ErrorMessageEnum.ACQUIRE_LOCK_FAIL);
 }
 String topicId = RedisQueueKey.getTopicId(job.getTopic(), job.getJobId());
 
 // 1. 将job添加到 JobPool中
 RMap<String, Job> jobPool = redissonClient.getMap(RedisQueueKey.JOB_POOL_KEY);
 if (jobPool.get(topicId) != null) {
 throw new BusinessException(ErrorMessageEnum.JOB_ALREADY_EXIST);
 }
 
 jobPool.put(topicId, job);
 
 // 2. 将job添加到 DelayBucket中
 RScoredSortedSet<Object> delayBucket = redissonClient.getScoredSortedSet(RedisQueueKey.RD_ZSET_BUCKET_PRE);
 delayBucket.add(job.getDelay(), topicId);
 } catch (InterruptedException e) {
 log.error("addJob error", e);
 } finally {
 if (lock != null) {
 lock.unlock();
 }
 }
 }

Job删除:搬运线程查找执行时间戳比当前时间小就全部删除

/**
 * 删除job信息
 *
 * @param job 元信息
 */
 @Override
 public void deleteJob(JobDie jobDie) {
 
 RLock lock = redissonClient.getLock(DELETE_JOB_LOCK + jobDie.getJobId());
 try {
 boolean lockFlag = lock.tryLock(LOCK_WAIT_TIME, LOCK_RELEASE_TIME, TimeUnit.SECONDS);
 if (!lockFlag) {
 throw new BusinessException(ErrorMessageEnum.ACQUIRE_LOCK_FAIL);
 }
 String topicId = RedisQueueKey.getTopicId(jobDie.getTopic(), jobDie.getJobId());
 
 RMap<String, Job> jobPool = redissonClient.getMap(RedisQueueKey.JOB_POOL_KEY);
 jobPool.remove(topicId);
 
//delayBucket
 RScoredSortedSet<Object> delayBucket = redissonClient.getScoredSortedSet(RedisQueueKey.RD_ZSET_BUCKET_PRE);
 delayBucket.remove(topicId);
 } catch (InterruptedException e) {
 log.error(“addJob error”, e);
 } finally {
 if (lock != null) {
 lock.unlock();
 }
 }
 }
}

总结

在对于定时补偿的业务场景,mq和redis的延迟队列各有优势,在使用方便的场景下我还是比较建议使用mq,首先是开箱即用,可以兼容很多种语言,有图形化的控制界面,可以快速看到队列中的消息情况,消息的持久化和队列的扩展性都比redis的延迟队列要很多,总而言之在业务的开发中,适合业务的才是最好的。

订单系统生成失败的订单,订单系统订单状态处于生成中,在生成中失败的,或者已经创建订单长时间不处理的都会进入死信队列,建立补偿消费者队列监听死信队列去重新生成订单,订单生成成功后给结算系统发送消息,通知结算系统。

Redis延时队列实现

Redis延迟队列优势

1.zset数据结构支持高性能的score排序 2.内存操作,速度非常快 3.redis有哨兵和cluster集群模式,当消息多的时候可以使用集群模式处理 4.redis的持久化机制,保证数据的可持久性

Redis延迟队列优势

1.mq具有生产者的confirm机制和消费者的ack确认机制 2.没有重试机制,需要自己实现和重试次数

延迟队列设计思路

1.将redis做消息池 KV结构:K=prefix+projectName field = topic+jobId V=CONENT;V由客户端传入的数据,消费的时候回传 2.zset做延迟优先队列,score做优先级 3.list结构,做消息队列先进先出的方式进行消费 4.zset和list存储消息地址 5.点对点消息从zset路由到list队列 6.定时器维护路由 7.ttl规则实现消息的延迟

具体实现

1.新增加JOB,ZING:DELAY_QUEUE:JOB_POOL插入一条数据记录业务消费方;ZING:DELAY_QUEUE:BUCKET### 也会插入一条记录,记录执行的时间戳 2.开启定时任务定时去扫 ZING:DELAY_QUEUE:BUCKET,相当于搬运线程查找执行时间戳比当前时间小就全部删除,同时解析出每个任务的topic,将这些任务push到list结构ZING:DELAY_QUEUE:QUEUE 3.创建监听线程去批量获取list待消费的消息,获取到就丢给这个路由的消费线程 4.消费现在在去JOB池查找数据结构,返回回调结构,执行回调方法。

出现的问题解决

保证消息执行的时效性,设置定时器每隔1秒去请求redis获取是否有待消费的job,如果一直没有任务就失去了扫描的意义,List任务队列有BLPOP阻塞,如果list中没有数据就会一直阻塞,可以设置阻塞时间。

Redis分布式锁保证消费的重复消费问题 分布式锁保证定时器的执行频率

代码实现

核心Job实体类


@Data
public class Job implements Serializable {
 
 private static final long serialVersionUID = 1L;
 
 /**
 * Job的唯一标识。用来检索和删除指定的Job信息
 */
 @NotBlank
 private String jobId;
 
 
 /**
 * Job类型。可以理解成具体的业务名称
 */
 @NotBlank
 private String topic;
 
 /**
 * Job需要延迟的时间。单位:秒。(服务端会将其转换为绝对时间)
 */
 private Long delay;
 
 /**
 * Job的内容,供消费者做具体的业务处理,以json格式存储
 */
 @NotBlank
 private String body;
 
 /**
 * 失败重试次数
 */
 private int retry = 0;
 
 /**
 * 通知URL
 */
 @NotBlank
 private String url;
}

删除Job实体

@Data
public class JobDie implements Serializable {
 
 private static final long serialVersionUID = 1L;
 
 /**
 * Job的唯一标识。用来检索和删除指定的Job信息
 */
 @NotBlank
 private String jobId;
 
 
 /**
 * Job类型。可以理解成具体的业务名称
 */
 @NotBlank
 pri

搬运线程

@Slf4j
@Component
public class CarryJobScheduled {
 
 @Autowired
 private RedissonClient redissonClient;
 
 /**
 * 启动定时开启搬运JOB信息
 */
 @Scheduled(cron = "*/1 * * * * *")
 public void carryJobToQueue() {
 System.out.println("carryJobToQueue --->");

 //分布式锁控制定时任务执行频率
 RLock lock = redissonClient.getLock(RedisQueueKey.CARRY_THREAD_LOCK);

 try {
 boolean lockFlag = lock.tryLock(LOCK_WAIT_TIME, LOCK_RELEASE_TIME, TimeUnit.SECONDS);
 if (!lockFlag) {
 throw new BusinessException(ErrorMessageEnum.ACQUIRE_LOCK_FAIL);
 }
 RScoredSortedSet<Object> bucketSet = redissonClient.getScoredSortedSet(RD_ZSET_BUCKET_PRE);
 long now = System.currentTimeMillis();
 //获取到目前为止的需要执行的job消息
 Collection<Object> jobCollection = bucketSet.valueRange(0, false, now, true);
 //找到对应list里面的待消费的消息
 List<String> jobList = jobCollection.stream().map(String::valueOf).collect(Collectors.toList());
 //将带消费的消息加入list队列
 RList<String> readyQueue = redissonClient.getList(RD_LIST_TOPIC_PRE);
 readyQueue.addAll(jobList);
 bucketSet.removeAllAsync(jobList);
 } catch (InterruptedException e) {
 log.error("carryJobToQueue error", e);
 } finally {
 if (lock != null) {
 lock.unlock();
 }
 }
 }
}

消费线程

@Slf4j
@Component
public class ReadyQueueContext {
 
 @Autowired
 private RedissonClient redissonClient;
 
 @Autowired
 private ConsumerService consumerService;
 
 /**
 * TOPIC消费线程
 */
 @PostConstruct
 public void startTopicConsumer() {
 TaskManager.doTask(this::runTopicThreads, “开启TOPIC消费线程”);
 }
 
 /**
 * 开启TOPIC消费线程
 * 将所有可能出现的异常全部catch住,确保While(true)能够不中断
 */
 @SuppressWarnings(“InfiniteLoopStatement”)
 private void runTopicThreads() {
 while (true) {
 RLock lock = null;
 try {
 lock = redissonClient.getLock(CONSUMER_TOPIC_LOCK);
 } catch (Exception e) {
 log.error(“runTopicThreads getLock error”, e);
 }
 try {
 if (lock == null) {
 continue;
 }
 // 分布式锁时间比Blpop阻塞时间多1S,避免出现释放锁的时候,锁已经超时释放,unlock报错
 boolean lockFlag = lock.tryLock(LOCK_WAIT_TIME, LOCK_RELEASE_TIME, TimeUnit.SECONDS);
 if (!lockFlag) {
 continue;
 }
 
 // 1. 获取ReadyQueue中待消费的数据
 RBlockingQueue<String> queue = redissonClient.getBlockingQueue(RD_LIST_TOPIC_PRE);
 String topicId = queue.poll(60, TimeUnit.SECONDS);
 if (StringUtils.isEmpty(topicId)) {
 continue;
 }
 
 // 2. 获取job元信息内容
 RMap<String, Job> jobPoolMap = redissonClient.getMap(JOB_POOL_KEY);
 Job job = jobPoolMap.get(topicId);
 
 // 3. 消费
 FutureTask<Boolean> taskResult = TaskManager.doFutureTask(() -> consumerService.consumerMessage(job.getUrl(), job.getBody()), job.getTopic() + “—>消费JobId—>” + job.getJobId());
 if (taskResult.get()) {
 // 3.1 消费成功,删除JobPool和DelayBucket的job信息
 jobPoolMap.remove(topicId);
 } else {
 int retrySum = job.getRetry() + 1;
 // 3.2 消费失败,则根据策略重新加入Bucket
 
 // 如果重试次数大于5,则将jobPool中的数据删除,持久化到DB
 if (retrySum > RetryStrategyEnum.RETRY_FIVE.getRetry()) {
 jobPoolMap.remove(topicId);
 continue;
 }
 job.setRetry(retrySum);
 long nextTime = job.getDelay() + RetryStrategyEnum.getDelayTime(job.getRetry()) * 1000;
 log.info(“next retryTime is [{}]”, DateUtil.long2Str(nextTime));
 RScoredSortedSet<Object> delayBucket = redissonClient.getScoredSortedSet(RedisQueueKey.RD_ZSET_BUCKET_PRE);
 delayBucket.add(nextTime, topicId);
 // 3.3 更新元信息失败次数
 jobPoolMap.put(topicId, job);
 }
 } catch (Exception e) {
 log.error(“runTopicThreads error”, e);
 } finally {
 if (lock != null) {
 try {
 lock.unlock();
 } catch (Exception e) {
 log.error(“runTopicThreads unlock error”, e);
 }
 }
 }
 }
 }
}

往Job消息池里面添加job

 /**
 * 添加job元信息
 *
 * @param job 元信息
 */
 @Override
 public void addJob(Job job) {
 
 RLock lock = redissonClient.getLock(ADD_JOB_LOCK + job.getJobId());
 try {
 boolean lockFlag = lock.tryLock(LOCK_WAIT_TIME, LOCK_RELEASE_TIME, TimeUnit.SECONDS);
 if (!lockFlag) {
 throw new BusinessException(ErrorMessageEnum.ACQUIRE_LOCK_FAIL);
 }
 String topicId = RedisQueueKey.getTopicId(job.getTopic(), job.getJobId());
 
 // 1. 将job添加到 JobPool中
 RMap<String, Job> jobPool = redissonClient.getMap(RedisQueueKey.JOB_POOL_KEY);
 if (jobPool.get(topicId) != null) {
 throw new BusinessException(ErrorMessageEnum.JOB_ALREADY_EXIST);
 }
 
 jobPool.put(topicId, job);
 
 // 2. 将job添加到 DelayBucket中
 RScoredSortedSet<Object> delayBucket = redissonClient.getScoredSortedSet(RedisQueueKey.RD_ZSET_BUCKET_PRE);
 delayBucket.add(job.getDelay(), topicId);
 } catch (InterruptedException e) {
 log.error("addJob error", e);
 } finally {
 if (lock != null) {
 lock.unlock();
 }
 }
 }

Job删除:搬运线程查找执行时间戳比当前时间小就全部删除

/**
 * 删除job信息
 *
 * @param job 元信息
 */
 @Override
 public void deleteJob(JobDie jobDie) {
 
 RLock lock = redissonClient.getLock(DELETE_JOB_LOCK + jobDie.getJobId());
 try {
 boolean lockFlag = lock.tryLock(LOCK_WAIT_TIME, LOCK_RELEASE_TIME, TimeUnit.SECONDS);
 if (!lockFlag) {
 throw new BusinessException(ErrorMessageEnum.ACQUIRE_LOCK_FAIL);
 }
 String topicId = RedisQueueKey.getTopicId(jobDie.getTopic(), jobDie.getJobId());
 
 RMap<String, Job> jobPool = redissonClient.getMap(RedisQueueKey.JOB_POOL_KEY);
 jobPool.remove(topicId);
 
//delayBucket
 RScoredSortedSet<Object> delayBucket = redissonClient.getScoredSortedSet(RedisQueueKey.RD_ZSET_BUCKET_PRE);
 delayBucket.remove(topicId);
 } catch (InterruptedException e) {
 log.error(“addJob error”, e);
 } finally {
 if (lock != null) {
 lock.unlock();
 }
 }
 }
}

总结

在对于定时补偿的业务场景,mq和redis的延迟队列各有优势,在使用方便的场景下我还是比较建议使用mq,首先是开箱即用,可以兼容很多种语言,有图形化的控制界面,可以快速看到队列中的消息情况,消息的持久化和队列的扩展性都比redis的延迟队列要很多,总而言之在业务的开发中,适合自身业务的才是最好的。