基于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主从复制以及主从复制原理
May 29 Redis
Redis可视化客户端小结
Jun 10 Redis
Redis入门教程详解
Aug 30 Redis
使用redis生成唯一编号及原理示例详解
Sep 15 Redis
Redis三种集群模式详解
Oct 05 Redis
Redis中有序集合的内部实现方式的详细介绍
Mar 16 Redis
在Centos 8.0中安装Redis服务器的教程详解
Mar 21 Redis
使用Redis做预定库存缓存功能
Apr 02 Redis
Grafana可视化监控系统结合SpringBoot使用
Apr 19 Redis
浅谈Redis的事件驱动模型
May 30 Redis
浅谈Redis缓冲区机制
Jun 05 Redis
浅谈Redis变慢的原因及排查方法
Jun 21 Redis
基于Redis实现分布式锁的方法(lua脚本版)
redis三种高可用方式部署的实现
May 11 #Redis
Redis数据结构之链表与字典的使用
基于Redis位图实现用户签到功能
May 08 #Redis
基于Redis过期事件实现订单超时取消
May 08 #Redis
Redis实现订单自动过期功能的示例代码
May 08 #Redis
redis 限制内存使用大小的实现
You might like
收听困难?教您超简便短波广播抗干扰方法!
2021/03/01 无线电
简单的PHP缓存设计实现代码
2011/09/30 PHP
ASP.NET jQuery 实例2 (表单中使用回车在TextBox之间向下移动)
2012/01/13 Javascript
JS命名空间的另一种实现
2013/08/09 Javascript
常见表单重复提交问题整理及解决方法
2013/11/13 Javascript
解决jquery插件冲突的问题
2014/01/23 Javascript
js使用Array.prototype.sort()对数组对象排序的方法
2015/01/28 Javascript
JQUERY简单按钮轮换选中效果实现方法
2015/05/07 Javascript
微信企业号开发之微信考勤百度地图定位
2015/09/11 Javascript
JavaScript的History API使搜索引擎抓取AJAX内容
2015/12/07 Javascript
jQuery实现获取table表格第一列值的方法
2016/03/01 Javascript
JS 事件绑定、事件监听、事件委托详细介绍
2016/09/28 Javascript
js仿小米官网图片轮播特效
2016/09/29 Javascript
scroll事件实现监控滚动条并分页显示(zepto.js)
2016/12/18 Javascript
AngularJS实现页面定时刷新
2017/03/14 Javascript
基于JS对象创建常用方式及原理分析
2017/06/28 Javascript
十大 Node.js 的 Web 框架(快速提升工作效率)
2017/06/30 Javascript
微信小程序使用npm支持踩坑
2018/11/07 Javascript
了解JavaScript中let语句
2019/05/30 Javascript
python中__call__方法示例分析
2014/10/11 Python
Python中属性和描述符的正确使用
2016/08/23 Python
Python调用C# Com dll组件实战教程
2017/10/12 Python
python使用PyCharm进行远程开发和调试
2017/11/02 Python
python计算两个数的百分比方法
2018/06/29 Python
python检测文件夹变化,并拷贝有更新的文件到对应目录的方法
2018/10/17 Python
Python venv虚拟环境配置过程解析
2020/07/08 Python
Sixt美国租车:高端豪华车型自驾体验
2017/09/02 全球购物
沙特阿拉伯电子产品和家用电器购物网站:Black Box
2019/07/24 全球购物
意大利一家专营包包和配饰的网上商店:Borse Last Minute
2019/08/26 全球购物
安全责任书范本
2014/04/15 职场文书
行政管理专业求职信
2014/07/06 职场文书
创业计划书之家政服务
2019/09/18 职场文书
Python合并多张图片成PDF
2021/06/09 Python
Python中的协程(Coroutine)操作模块(greenlet、gevent)
2022/05/30 Python
Redis过期数据是否会被立马删除
2022/07/23 Redis
CSS使用Flex和Grid布局实现3D骰子
2022/08/05 HTML / CSS