基于Redis延迟队列的实现代码


Posted in Redis onMay 13, 2021

使用场景

工作中大家往往会遇到类似的场景:

1.对于红包场景,账户 A 对账户 B 发出红包通常在 1 天后会自动归还到原账户。

2.对于实时支付场景,如果账户 A 对商户 S 付款 100 元,5秒后没有收到支付方回调将自动取消订单。

解决方案分析

方案一:

采用通过定时任务采用数据库/非关系型数据库轮询方案。

优点:

1. 实现简单,对于项目前期这样是最容易的解决方案。

缺点:

1. DB 有效使用率低,需要将一部分的数据库的QPS分配给 JOB 的无效轮询。

2. 服务资源浪费,因为轮询需要对所有的数据做一次 SCAN 扫描 JOB 服务的资源开销很大。

方案二:

采用延迟队列:

优点:

1. 服务的资源使用率较高,能够精确的实现超时任务的执行。

2. 减少 DB 的查询次数,能够降低数据库的压力

缺点:

1. 对于延迟队列来说本身设计比较复杂,目前没有通用的比较好过的方案。

基于 Redis 的延迟队列实现

基于以上的分析,我决定通过 Redis 来实现分布式队列。

设计思路:

基于Redis延迟队列的实现代码

1. 第一步将需要发放的消息发送到延迟队列中。

2. 延迟队列将数据存入 Redis 的 ZSet 有序集合中score 为当前时间戳,member 存入需要发送的数据。

3. 添加一个 schedule 来进行对 Redis 有序队列的轮询。

4. 如果到达达到消息的执行时间,那么就进行业务的执行。

5. 如果没有达到消息的执行是将,那么消息等待下轮执行。

实现步骤:

由于本处篇幅有限,所以只列举部分代码,完整的代码可以在本文最后访问 GitHub 获取。由于本人阅历/水平有限,如有建议/或更正欢迎留言或提问。先在此谢谢大家驻足阅读 ? ? ?。

需要注意的问题:

单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。

事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。

我们可以通过 Redis 的 eval 命令来执行 lua 脚本来保证原子性实现Redis的事务。

实现步骤如下:

1. 延迟队列接口

/**
 * 延迟队列
 *
 * @author zhengsh
 * @date 2020-03-27
 */
public interface RedisDelayQueue<E extends DelayMessage> {

    String META_TOPIC_WAIT = "delay:meta:topic:wait";
    String META_TOPIC_ACTIVE = "delay:meta:topic:active";
    String TOPIC_ACTIVE = "delay:active:9999";
    /**
     * 拉取消息
     */
    void poll();

    /**
     * 推送延迟消息
     *
     * @param e
     */
    void push(E e);
}

2. 延迟队列消息

/**
 * 消息体
 *
 * @author zhengsh
 * @date 2020-03-27
 */
@Setter
@Getter
public class DelayMessage {
    /**
     * 消息唯一标识
     */
    private String id;
    /**
     * 消息主题
     */
    private String topic = "default";
    /**
     * 具体消息 json
     */
    private String body;
    /**
     * 延时时间, 格式为时间戳: 当前时间戳 + 实际延迟毫秒数
     */
    private Long delayTime = System.currentTimeMillis() + 30000L;
    /**
     * 消息发送时间
     */
    private LocalDateTime createTime;
}

3. 延迟队列实现

/**
 * 延迟队列实现
 *
 * @author zhengsh
 * @date 2020-03-27
 */
@Component
public class RedisDelayQueueImpl<E extends DelayMessage> implements RedisDelayQueue<E> {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public void poll() {
        // todo
    }

    /**
     * 发送消息
     *
     * @param e
     */
    @SneakyThrows
    @Override
    public void push(E e) {
        try {
            String jsonStr = JSON.toJSONString(e);
            String topic = e.getTopic();
            String zkey = String.format("delay:wait:%s", topic);
            String u =
                    "redis.call('sadd', KEYS[1], ARGV[1])\n" +
                            "redis.call('zadd', KEYS[2], ARGV[2], ARGV[3])\n" +
                            "return 1";

            Object[] keys = new Object[]{serialize(META_TOPIC_WAIT), serialize(zkey)};
            Object[] values = new Object[]{ serialize(zkey), serialize(String.valueOf(e.getDelayTime())),serialize(jsonStr)};

            Long result = redisTemplate.execute((RedisCallback<Long>) connection -> {
                Object nativeConnection = connection.getNativeConnection();

                if (nativeConnection instanceof RedisAsyncCommands) {
                    RedisAsyncCommands commands = (RedisAsyncCommands) nativeConnection;
                    return (Long) commands.getStatefulConnection().sync().eval(u, ScriptOutputType.INTEGER, keys, values);
                } else if (nativeConnection instanceof RedisAdvancedClusterAsyncCommands) {
                    RedisAdvancedClusterAsyncCommands commands = (RedisAdvancedClusterAsyncCommands) nativeConnection;
                    return (Long) commands.getStatefulConnection().sync().eval(u, ScriptOutputType.INTEGER, keys, values);
                }
                return 0L;
            });
            logger.info("延迟队列[1],消息推送成功进入等待队列({}), topic: {}", result != null && result > 0, e.getTopic());
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }

    private byte[] serialize(String key) {
        RedisSerializer<String> stringRedisSerializer =
                (RedisSerializer<String>) redisTemplate.getKeySerializer();
        //lettuce连接包下序列化键值,否则无法用默认的ByteArrayCodec解析
        return stringRedisSerializer.serialize(key);
    }
}

4. 定时任务

/**
 * 分发任务
 */
@Component
public class DistributeTask {

    private static final String LUA_SCRIPT;
    private Logger logger = LoggerFactory.getLogger(getClass());
    @Autowired
    private StringRedisTemplate redisTemplate;

    static {
        StringBuilder sb = new StringBuilder(128);
        sb.append("local val = redis.call('zrangebyscore', KEYS[1], '-inf', ARGV[1], 'limit', 0, 1)\n");
        sb.append("if(next(val) ~= nil) then\n");
        sb.append("    redis.call('sadd', KEYS[2], ARGV[2])\n");
        sb.append("    redis.call('zremrangebyrank', KEYS[1], 0, #val - 1)\n");
        sb.append("    for i = 1, #val, 100 do\n");
        sb.append("        redis.call('rpush', KEYS[3], unpack(val, i, math.min(i+99, #val)))\n");
        sb.append("    end\n");
        sb.append("    return 1\n");
        sb.append("end\n");
        sb.append("return 0");
        LUA_SCRIPT = sb.toString();
    }

    /**
     * 2秒钟扫描一次执行队列
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void scheduledTaskByCorn() {
        try {
            Set<String> members = redisTemplate.opsForSet().members(META_TOPIC_WAIT);
            assert members != null;
            for (String k : members) {
                if (!redisTemplate.hasKey(k)) {
                    // 如果 KEY 不存在元数据中删除
                    redisTemplate.opsForSet().remove(META_TOPIC_WAIT, k);
                    continue;
                }

                String lk = k.replace("delay:wait", "delay:active");
                Object[] keys = new Object[]{serialize(k), serialize(META_TOPIC_ACTIVE), serialize(lk)};
                Object[] values = new Object[]{serialize(String.valueOf(System.currentTimeMillis())), serialize(lk)};
                Long result = redisTemplate.execute((RedisCallback<Long>) connection -> {
                    Object nativeConnection = connection.getNativeConnection();

                    if (nativeConnection instanceof RedisAsyncCommands) {
                        RedisAsyncCommands commands = (RedisAsyncCommands) nativeConnection;
                        return (Long) commands.getStatefulConnection().sync().eval(LUA_SCRIPT, ScriptOutputType.INTEGER, keys, values);
                    } else if (nativeConnection instanceof RedisAdvancedClusterAsyncCommands) {
                        RedisAdvancedClusterAsyncCommands commands = (RedisAdvancedClusterAsyncCommands) nativeConnection;
                        return (Long) commands.getStatefulConnection().sync().eval(LUA_SCRIPT, ScriptOutputType.INTEGER, keys, values);
                    }
                    return 0L;
                });
                logger.info("延迟队列[2],消息到期进入执行队列({}): {}", result != null && result > 0, TOPIC_ACTIVE);
            }
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }

    private byte[] serialize(String key) {
        RedisSerializer<String> stringRedisSerializer =
                (RedisSerializer<String>) redisTemplate.getKeySerializer();
        //lettuce连接包下序列化键值,否则无法用默认的ByteArrayCodec解析
        return stringRedisSerializer.serialize(key);
    }
}

GitHub 地址

https://github.com/zhengsh/redis-delay-queue

参考地址

1.https://www.runoob.com/redis/redis-transactions.html

到此这篇关于基于Redis延迟队列的实现代码的文章就介绍到这了,更多相关Redis 延迟队列内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Redis 相关文章推荐
详解Redis实现限流的三种方式
Apr 27 Redis
Redis主从配置和底层实现原理解析(实战记录)
Jun 30 Redis
redis客户端实现高可用读写分离的方式详解
Jul 04 Redis
redis不能访问本机真实ip地址的解决方案
Jul 07 Redis
Redisson实现Redis分布式锁的几种方式
Aug 07 Redis
基于Redis zSet实现滑动窗口对短信进行防刷限流的问题
Feb 12 Redis
Redis分布式锁的7种实现
Apr 01 Redis
sentinel支持的redis高可用集群配置详解
Apr 01 Redis
使用Redis做预定库存缓存功能
Apr 02 Redis
Redis超详细讲解高可用主从复制基础与哨兵模式方案
Apr 07 Redis
Redis实现主从复制方式(Master&Slave)
Jun 21 Redis
Redis Lua脚本实现ip限流示例
Jul 15 Redis
基于Redis实现分布式锁的方法(lua脚本版)
redis三种高可用方式部署的实现
May 11 #Redis
Redis数据结构之链表与字典的使用
基于Redis位图实现用户签到功能
May 08 #Redis
基于Redis过期事件实现订单超时取消
May 08 #Redis
Redis实现订单自动过期功能的示例代码
May 08 #Redis
redis 限制内存使用大小的实现
You might like
PHP5新特性: 更加面向对象化的PHP
2006/11/18 PHP
模拟flock实现文件锁定
2007/02/14 PHP
解决Codeigniter不能上传rar和zip压缩包问题
2014/03/07 PHP
PHP读取文件内容后清空文件示例代码
2014/03/18 PHP
PHP实现采集抓取淘宝网单个商品信息
2015/01/08 PHP
Laravel配置全局公共函数的方法步骤
2019/05/09 PHP
探讨在JQuery和Js中,如何让ajax执行完后再继续往下执行
2013/07/09 Javascript
使用jquery获取网页中图片高度的两种方法
2013/09/26 Javascript
javascript禁止访客复制网页内容的实现代码
2015/08/05 Javascript
基于PHP和Mysql相结合使用jqGrid读取数据并显示
2015/12/02 Javascript
js实现的星星评分功能函数
2015/12/09 Javascript
javascript简单比较日期大小的方法
2016/01/05 Javascript
基于jQuery实现页面搜索功能
2020/03/26 Javascript
AngularJS实现动态添加Option的方法
2017/05/17 Javascript
详解vue-meta如何让你更优雅的管理头部标签
2018/01/18 Javascript
ES6 Promise对象的含义和基本用法分析
2019/06/14 Javascript
layui实现数据表格table分页功能(ajax异步)
2019/07/27 Javascript
vue 实现Web端的定位功能 获取经纬度
2019/08/08 Javascript
ES6中的类(Class)示例详解
2020/12/09 Javascript
python将html转成PDF的实现代码(包含中文)
2013/03/04 Python
python教程之用py2exe将PY文件转成EXE文件
2014/06/12 Python
python实现比较两段文本不同之处的方法
2015/05/30 Python
Python中pygal绘制雷达图代码分享
2017/12/07 Python
Python3利用Dlib19.7实现摄像头人脸识别的方法
2018/05/11 Python
解决pycharm 工具栏Tool中找不到Run manager.py Task的问题
2019/07/01 Python
python的re模块使用方法详解
2019/07/26 Python
python用opencv完成图像分割并进行目标物的提取
2020/05/25 Python
使用css3制作动感导航条示例
2014/01/26 HTML / CSS
师范生自荐信
2013/10/27 职场文书
党员培训思想汇报
2014/01/07 职场文书
军神教学反思
2014/02/04 职场文书
高中同学会活动方案
2014/08/14 职场文书
学校政风行风自查自纠报告
2014/10/21 职场文书
政风行风整改方案
2014/10/25 职场文书
优秀新员工事迹材料
2019/05/13 职场文书
年终工作总结范文
2019/06/20 职场文书