基于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 相关文章推荐
Redis6.0搭建集群Redis-cluster的方法
May 08 Redis
使用Redis实现秒杀功能的简单方法
May 08 Redis
浅谈redis缓存在项目中的使用
May 20 Redis
详解Redis瘦身指南
May 26 Redis
redis哨兵常用命令和监控示例详解
May 27 Redis
Windows下redis下载、redis安装及使用教程
Jun 02 Redis
基于Redis的List实现特价商品列表功能
Aug 30 Redis
关于SpringBoot 使用 Redis 分布式锁解决并发问题
Nov 17 Redis
解决linux下redis数据库overcommit_memory问题
Feb 24 Redis
Redis中有序集合的内部实现方式的详细介绍
Mar 16 Redis
使用Redis做预定库存缓存功能
Apr 02 Redis
Redis批量生成数据的实现
Jun 05 Redis
基于Redis实现分布式锁的方法(lua脚本版)
redis三种高可用方式部署的实现
May 11 #Redis
Redis数据结构之链表与字典的使用
基于Redis位图实现用户签到功能
May 08 #Redis
基于Redis过期事件实现订单超时取消
May 08 #Redis
Redis实现订单自动过期功能的示例代码
May 08 #Redis
redis 限制内存使用大小的实现
You might like
Windows下IIS6/Apache2.2.4+MySQL5.2+PHP5.2.1安装配置方法
2007/05/03 PHP
php 文件缓存函数
2011/10/08 PHP
WordPress中用于创建以及获取侧边栏的PHP函数讲解
2015/12/29 PHP
laravel项目利用twemproxy部署redis集群的完整步骤
2018/05/11 PHP
在页面上点击任一链接时触发一个事件的代码
2007/04/07 Javascript
JSON 客户端和服务器端的格式转换
2009/08/27 Javascript
asp.net+js 实现无刷新上传解析csv文件的代码
2010/05/17 Javascript
有关js的变量作用域和this指针的讨论
2010/12/16 Javascript
JQuery 返回布尔值Is()条件判断方法代码
2012/05/14 Javascript
jQuery隔行变色与普通JS写法的对比
2013/04/21 Javascript
判断一个对象是否为jquery对象的方法
2014/03/12 Javascript
node.js适合游戏后台开发吗?
2014/09/03 Javascript
jQuery实现按键盘方向键翻页特效
2015/03/18 Javascript
JavaScript中的fontsize()方法使用详解
2015/06/08 Javascript
JavaScript遍历求解数独问题的主要思路小结
2016/06/12 Javascript
angularJS+requireJS实现controller及directive的按需加载示例
2017/02/20 Javascript
微信小程序之GET请求的实例详解
2017/09/29 Javascript
微信小程序实现天气预报功能
2018/07/18 Javascript
webpack 3.X学习之多页面打包的方法
2018/09/04 Javascript
vue自定义switch开关组件,实现样式可自行更改
2019/11/01 Javascript
vue跳转同一个组件,参数不同,页面接收值只接收一次的解决方法
2019/11/05 Javascript
[01:43]3.19DOTA2发布会 三代刀塔人第三代
2014/03/25 DOTA
[06:25]第二届DOTA2亚洲邀请赛主赛事第二天比赛集锦.mp4
2017/04/03 DOTA
Python多线程threading和multiprocessing模块实例解析
2018/01/29 Python
使用python对excle和json互相转换的示例
2018/10/23 Python
Python+selenium点击网页上指定坐标的实例
2019/07/05 Python
如何基于python生成list的所有的子集
2019/11/11 Python
使用python切片实现二维数组复制示例
2019/11/26 Python
Python 生成VOC格式的标签实例
2020/03/10 Python
销售经理工作职责
2014/02/03 职场文书
开门红主持词
2014/04/02 职场文书
小班幼儿评语大全
2014/04/30 职场文书
大专生自我鉴定怎么写
2014/09/16 职场文书
2016母亲节感恩话语
2015/12/09 职场文书
php+laravel 扫码二维码签到功能
2021/05/15 PHP
关于nginx 实现jira反向代理的问题
2021/09/25 Servers