持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情
前言
涉及到生成订单的系统,相信大家都遇到过订单生成失败或者异常的情况,由于业务逻辑服的复杂性,会使我们的订单生成失败的原因很多,当然我们不能坐以待毙,一般的情况下我们就需要让这些失败的订单使它生成成功,给公司增加点收入..在失败订单数据量很少的情况下我们很好处理,后台开发人员,手动修数据就好了, 但是在实际公司订单生成量比较大,或者说数据库失败的订单比较多的情况下,我们应该如何去重试补偿这些数据。
定时任务补偿重试
由于目前公司,一天的订单量不是很大,订单组的同事都很忙,没有时间配合我们开发, 所以我们直接在项目中连接订单系统的DB,只读操作和更新我们加的补偿、重试字段,不去影响订单表的其他字段,采用的解决方式是通过xxl-job启动两个定时任务,一个重试、一个补偿的定时任务去对失败的订单进行重试补偿处理,但是我觉得这是一个优先级比较低的一个选择,频繁的去扫表,会造成网络IO和磁盘IO的消耗,所以这种做法真的很low。
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。
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的设置,接下来说死信如何处理订单系统失败的订单。
订单系统生成失败的订单,订单系统订单状态处于生成中,在生成中失败的,或者已经创建订单长时间不处理的都会进入死信队列,建立补偿消费者队列监听死信队列去重新生成订单,订单生成成功后给结算系统发送消息,通知结算系统。
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的延迟队列要很多,总而言之在业务的开发中,适合自身业务的才是最好的。