前言
最近发现公司内部很多同事利用redis搭配定时器做了一些类似消息中间件的功能。一面生产一面消费。不过每个人都有各自的做法,于是对针对每种做法做了一些总结。
提示:以下是本篇文章正文内容,下面案例可供参考
一、利用Hash数据结构,搭配xxljob方式?直接上代码
生产者
业务代码省略
stringRedisTemplate.opsForHash().put(com.kyexpress.vms.adapter.provider.constant.Constants.VIDEO_CUT_OUT_RECORD_REDIS_KEY, videoCutOutBO.getThirdNo(),
JsonUtils.deserializer(videoCutOutBO));
消费者 部分代码
@Scheduled(cron = "0/30 * * * * ?")
public void execute() {
if (RedisTemplateLockUtil.tryGetDistributedLock(stringRedisTemplate, VIDEO_CUT_OUT_STATUS_LOCK_KEY, VIDEO_CUT_OUT_STATUS_LOCK_VALUE, 60 * 1)) {
logger.info("queryVideoCutOutStatus#execute获取锁成功");
try {
long beginTime = System.currentTimeMillis();
executeQueryVideoCutOutStatus();
logger.info("queryVideoCutOutStatus execute total time:{}", System.currentTimeMillis() - beginTime);
} catch (Exception e) {
logger.error("queryVideoCutOutStatus#execute出错:", e);
} finally {
RedisTemplateLockUtil.releaseDistributedLock(stringRedisTemplate, VIDEO_CUT_OUT_STATUS_LOCK_KEY, VIDEO_CUT_OUT_STATUS_LOCK_VALUE);
}
}
}
public void executeQueryVideoCutOutStatus() {
List<Object> objectList = stringRedisTemplate.opsForHash().values(Constants.VIDEO_CUT_OUT_RECORD_REDIS_KEY);
省略
stringRedisTemplate.opsForHash().delete(Constants.VIDEO_CUT_OUT_RECORD_REDIS_KEY,
videoCutOutBO.getThirdNo());
从代码可以看出,这里就是通过Hash结构作为存储,搭配定时任务做消费。定时器的业务时间是30
秒,但是有眼尖的同学应该可以看到定时器的入口加了一把分布式锁。这是为什么呢?
思来想去,那可能就是怕30秒执行不完业务代码,带来重复消费的问题把,因为key是在最后被删除掉的。
二、利用List结构,搭配while死循环方式
生产者
stringRedisTemplate.opsForList().leftPush(Constants.VIDEO_NAV_UPLOAD_REDIS_KEY, JSON.toJSONString(videoNavBO));
消费者1
@Override
public void run(String... args) throws Exception {
String redisLstKey = com.kyexpress.vms.adapter.provider.constant.Constants.VIDEO_WAY_BILL_UPLOAD;
while(true){
try {
//视频导航上传到腾讯云
String videoNavRedisListKey = com.kyexpress.vms.adapter.provider.constant.Constants.VIDEO_NAV_UPLOAD_REDIS_KEY;
Long videoNavRedisListSize = stringRedisTemplate.opsForList().size(videoNavRedisListKey);
if(videoNavRedisListSize>0){
String videoNavString = stringRedisTemplate.opsForList().rightPop(videoNavRedisListKey);
logger.info("视频导航上传videoNavString:{}",videoNavString);
VideoNavBO videoNavBO = JSONObject.parseObject(videoNavString, VideoNavBO.class);
videoNavExecutor.execute(new Runnable() {
@Override
public void run() {
try {
videoNavUploadService.uploadVideo(videoNavBO);
} catch (Exception e) {
logger.error("COSClient#uploadVideo#exception",e);
}
}
});
}
// logger.info("CosService#commandLine#run#redisLstSize="+redisSize);
if (stringRedisTemplate.opsForList().size(redisLstKey) < 10) {
sleep(3000);
} else {
sleep(1000);
}
} catch (Exception e) {
logger.error("CosService#commandLine#runException="+e.getMessage());
}
}
这里通过while死循环的方式,去消费List中的数据,但是下面为什么加了一个睡眠呢 sleep(3000);
而且最长还睡了三秒,后来分析这是怕对cpu的资源造成消耗。值得注意的是,这里用到消费的命令是,stringRedisTemplate.opsForList().rightPop(videoNavRedisListKey)。
针对以上的写法,我稍微做了些修改,虽然不是很完美,但是可能对cpu的消耗减少很多:
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
//视频导航上传到腾讯云
String videoNavRedisListKey = com.kyexpress.vms.adapter.provider.constant.Constants.VIDEO_NAV_UPLOAD_REDIS_KEY;
scheduler.scheduleWithFixedDelay(() -> {
Long videoNavSize = stringRedisTemplate.opsForList().size(videoNavRedisListKey);
if(videoNavSize > 0) {
String videoNavString = stringRedisTemplate.opsForList().rightPop(videoNavRedisListKey, 3, TimeUnit.SECONDS);
logger.info("视频导航上传videoNavString:{}", videoNavString);
VideoNavBO videoNavBO = JSONObject.parseObject(videoNavString, VideoNavBO.class);
try {
videoNavUploadService.uploadVideo(videoNavBO);
} catch (Exception e) {
logger.error("COSClient#uploadVideo#exception", e);
}
}
},1,1,TimeUnit.SECONDS);
这里就是利用了延时任务,以及阻塞消费的方式,无数据,挂起3秒钟。
总结
针对以上两种方式我稍微做了下总结,这两种方式,可能是我们优先想到的方式,但是仔细思考,都存在着一定的缺陷。
1.针对第一种方式,利用hash作为存储,搭配xxljob,并且融入了分布式锁。使用上依赖了很多的技术,两个字概括麻烦,本身一个很简单的功能,却做的这么麻烦,如果生产者一旦速度很快,30秒消费一次,是否会使我们的Hash造成大key,都存在一定的可能。
2.针对第二种方式,虽然去除掉了很多技术上的依赖,但是一旦消费失败,数据可能存在丢失的问题。虽然针对List结构给了我们一个消费并存储的到另一个数据结构的命令,但是实现太复杂。
如果利用上面两种方式去实现,反而不如直接利用rabbitmq。哈哈哈
接下来继续尝试redis是否有更好,更简单的命令呢
三、利用redis发布订阅模式
生产者:
redisTemplate.convertAndSend(channel, message);
消费者
public class RedisMessageListener implements MessageListener {
@Autowired
private RedisTemplate redisTemplate;
@Override
public void onMessage(Message message, byte[] pattern) {
redisTemplate.getValueSerializer().deserialize(message.getBody());
log.info(messageDto.getData()+","+messageDto.getContent());
}
}
以上消费简单,快速,并且不需要介入定时器。
内部实现原理,其实就是存储了一个 以channel为key,以订阅者为链表的数据结构,虽然也会出现数据丢失,但是如果扩展起来方便。
四、利用redis stream
生产者
String result = redisUtil.addMap(redisKey, map);
消费者
@Slf4j
@Component
public class ConsumeListener1 implements StreamListener<String, MapRecord<String, String, String>> {
@Autowired
private RedisUtil redisUtil;
private static ConsumeListener1 consumeListener1;
@PostConstruct
public void init(){
consumeListener1 = this;
consumeListener1.redisUtil=this.redisUtil;
}
@Override
public void onMessage(MapRecord<String, String, String> message) {
String stream = message.getStream();
RecordId id = message.getId();
Map<String, String> map = message.getValue();
log.info("[不自动ack] group:[group-a] consumerName:[{}] 接收到一个消息 stream:[{}],id:[{}],value:[{}]", stream, id, map);
consumeListener1.redisUtil.ack(stream, "group-a", id.getValue());
consumeListener1.redisUtil.del(stream, id.getValue());
}
————————————————
redis stream 是redis5.0之后出来的,增加了消费者组,ack,数据持久化等概念,简直跟kafka太像了。这里就不一一介绍了
针对以上的各个方式,你觉得哪个更合理呢?