由于MYSQL操作较慢,在大型的秒杀系统中,一般是把订单临时存放到Reids中,然后做一个定时任务,逐步的把Redis中的订单数据写入MYSQL,写入成功后删除Redis中的缓存数据,这里采用Redis的stream队列实现该逻辑,供大家参考。
@Component
@EnableScheduling
@Slf4j
public class RedisCacheOrder2MySqlTask {
@Value("${redis_cache_order_2_mysql.retry}")
private int retry;
@Resource
private StringRedisTemplate redisTemplate;
@Resource
private BaseHttpClient baseHttpClient;
@Resource(name="redisCacheOrder2MySqlThreadPool")
private ThreadPoolTaskExecutor threadPool;
/**
* 每秒钟读取一次Redis缓存单队列,Scheduled第二次调度会等到第一次调度执行完毕后的下一个调度时间点才会执行
* 这里采用线程池来加快任务的处理,在线程池未满时一直添加任务,一旦满则等待出现空闲时再添加任
* 重点!重点!重点理解!
* 虽然Scheduled第二次调度会等到第一次调度执行完毕后的下一个调度时间点才会执行,
* 但这里引入了线程池,只要任务加入线程池,主线程就会执行下一步,会有一定概率出现上次调度时池中的任务未全部处理完毕,又新开启了新的一轮调度。
* 由于采用了redis stream队列,一旦读取,则队列内部维护的游标会变化,所以不会出现同一条数据被多次读取的情况。
*/
//@Scheduled(cron = "0/1 * * * * ?")
public void doTask() {
log.info("Redis缓存单异步落库监控任务 is running");
Consumer consumer = Consumer.from(Constants.REDIS_CACHE_ORDERS_QUEUE_CONSUMER_GROUP, Constants.REDIS_CACHE_ORDERS_QUEUE_CONSUMER_NAME);
List<MapRecord<String, Object, Object>> list = redisTemplate.opsForStream().read(consumer, StreamReadOptions.empty().count(10),StreamOffset.create(Constants.REDIS_CACHE_ORDERS_QUEUE, ReadOffset.lastConsumed()));
log.info("读取10条Redis缓存单队列中的消息:{}", list);
if(list == null || list.isEmpty()){
return;
}
list.forEach(record->{
threadPool.execute(()->{
String orderNo="";
try {
orderNo=(String) record.getValue().get("orderNo");
log.info("第一次异步落库,内部线程={},正在处理orderNo={}",Thread.currentThread().getName(),orderNo);
//调用接口处理异步落库逻辑
MultiValueMap<String, String> params=new LinkedMultiValueMap<>();
params.add("order_no",orderNo);
Result<Object> res = baseHttpClient.doPost("/order/entryOrder", params);
if(res.getCode()==0){
//接口返回成功,则删除队列中的缓存单数据
redisTemplate.opsForStream().acknowledge(Constants.REDIS_CACHE_ORDERS_QUEUE_CONSUMER_GROUP,record);
redisTemplate.opsForStream().delete(record);
}else{
//php接口返回失败时,把消息转给同组的第二个消费者,对异常数据进行重试
redisTemplate.opsForStream().claim(record.getRequiredStream(),Constants.REDIS_CACHE_ORDERS_QUEUE_CONSUMER_GROUP,
Constants.REDIS_CACHE_ORDERS_QUEUE_PENDING_CONSUMER_NAME,Duration.ofMillis(0),record.getId());
}
} catch (Exception e) {
//如果执行过程中发生异常,把消息转给同组的第二个消费者,对异常数据进行重试
redisTemplate.opsForStream().claim(record.getRequiredStream(),Constants.REDIS_CACHE_ORDERS_QUEUE_CONSUMER_GROUP,
Constants.REDIS_CACHE_ORDERS_QUEUE_PENDING_CONSUMER_NAME,Duration.ofMillis(0),record.getId());
log.error("第一次异步落库,出现异常orderNo="+orderNo,e);
}
});
while(threadPool.getActiveCount()>=threadPool.getCorePoolSize()){
//如果正在执行的线程数>=核心执行线程数,则等待一会儿
try {
log.info("第一次异步落库线程池满,等待,核心线程池大小{},正在执行的线程数约为{}",threadPool.getCorePoolSize(),threadPool.getActiveCount());
Thread.sleep(5*1000);
} catch (InterruptedException e) {
log.error("线程休眠被打断",e);
}
}
});
}
/**
* 单独开启一个定时任务处理因各种情况没有成功入库的数据
* 这种情况发生的机会少,暂不考虑使用线程池,串行处理
* 对于redis stream队列我们通过把pending消息转给同组的第二个消费者机制,对异常数据进行重试
*/
//@Scheduled(cron = "0/1 * * * * ?")
public void reTry() {
log.info("扫描是否有未成功入库的缓存单数据,进行重试");
//注意,这里的consumer和第一次处理时的consumer属于同一个消费者组,但是不是一个消费者
Consumer consumer = Consumer.from(Constants.REDIS_CACHE_ORDERS_QUEUE_CONSUMER_GROUP, Constants.REDIS_CACHE_ORDERS_QUEUE_PENDING_CONSUMER_NAME);
StreamOperations<String, Object, Object> streamOperations = redisTemplate.opsForStream();
PendingMessages pendingMessageList=streamOperations.pending(Constants.REDIS_CACHE_ORDERS_QUEUE,consumer, Range.unbounded(),1);
if(pendingMessageList==null || pendingMessageList.isEmpty()){
return;
}
String orderNo="";
PendingMessage pendingMessage=null;
MapRecord<String, Object, Object> record=null;
try {
pendingMessage = pendingMessageList.get(0);
RecordId id = pendingMessage.getId();
List<MapRecord<String, Object, Object>> range = streamOperations.range(Constants.REDIS_CACHE_ORDERS_QUEUE, Range.just(id.getValue()));
if(range==null || range.isEmpty()){
return;
}
record=range.get(0);
orderNo= (String) record.getValue().get("orderNo");
log.info("发现需要进行二次异步落库的数据,第{}次尝试,orderNo={}",pendingMessage.getTotalDeliveryCount(),orderNo);
//调用接口处理异步落库逻辑
MultiValueMap<String, String> params=new LinkedMultiValueMap<>();
params.add("order_no",orderNo);
Result<Object> res = baseHttpClient.doPost("/order/entryOrder", params);
if(res.getCode()==0){
//接口返回成功,则删除队列中的缓存单数据
streamOperations.acknowledge(Constants.REDIS_CACHE_ORDERS_QUEUE_CONSUMER_GROUP,record);
streamOperations.delete(record);
}else{
if(pendingMessage.getTotalDeliveryCount()<retry){
//判断是否满足了指定的重试次数,不满足继续claim,会继续在pending状态下重试
streamOperations.claim(record.getRequiredStream(),Constants.REDIS_CACHE_ORDERS_QUEUE_CONSUMER_GROUP,
Constants.REDIS_CACHE_ORDERS_QUEUE_PENDING_CONSUMER_NAME,Duration.ofMillis(0),record.getId());
}else{
//如果满足了重试次数还没有处理OK,则不再处理,把数据从本消费者组移除,但数据依然保留在队列中。
streamOperations.acknowledge(Constants.REDIS_CACHE_ORDERS_QUEUE_CONSUMER_GROUP,record);
}
}
} catch (Exception e) {
if(pendingMessage!=null && record!=null){
if(pendingMessage.getTotalDeliveryCount()<retry){
//判断是否满足了指定的重试次数,不满足继续claim,会继续在pending状态下重试
streamOperations.claim(record.getRequiredStream(),Constants.REDIS_CACHE_ORDERS_QUEUE_CONSUMER_GROUP,
Constants.REDIS_CACHE_ORDERS_QUEUE_PENDING_CONSUMER_NAME,Duration.ofMillis(0),record.getId());
}else{
//如果满足了重试次数还没有处理OK,则不再处理,把数据从本消费者组移除,但数据依然保留在队列中。
streamOperations.acknowledge(Constants.REDIS_CACHE_ORDERS_QUEUE_CONSUMER_GROUP,record);
}
}
log.error("二次异步落库异常,orderNo={}",orderNo,e);
}
}
}