基于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 相关文章推荐
Redis5之后版本的高可用集群搭建的实现
Apr 27 Redis
基于Redis过期事件实现订单超时取消
May 08 Redis
基于Redis位图实现用户签到功能
May 08 Redis
Redis数据结构之链表与字典的使用
May 11 Redis
分布式锁为什么要选择Zookeeper而不是Redis?看完这篇你就明白了
May 21 Redis
解决linux下redis数据库overcommit_memory问题
Feb 24 Redis
Redis如何使用乐观锁(CAS)保证数据一致性
Mar 25 Redis
 Redis 串行生成顺序编码的方法实现
Apr 03 Redis
详解Redis的三种常用的缓存读写策略步骤
May 06 Redis
Redis 异步机制
May 15 Redis
一文教你快速生成MySQL数据库关系图
Jun 28 Redis
Redis实战之Lettuce的使用技巧详解
Dec 24 Redis
基于Redis实现分布式锁的方法(lua脚本版)
redis三种高可用方式部署的实现
May 11 #Redis
Redis数据结构之链表与字典的使用
基于Redis位图实现用户签到功能
May 08 #Redis
基于Redis过期事件实现订单超时取消
May 08 #Redis
Redis实现订单自动过期功能的示例代码
May 08 #Redis
redis 限制内存使用大小的实现
You might like
PHP多进程编程实例
2014/10/15 PHP
php画图实例
2014/11/05 PHP
php+mysql大量用户登录解决方案分析
2014/12/29 PHP
Yii隐藏URL中index.php的方法
2016/07/12 PHP
PHP实现上一篇下一篇的方法实例总结
2016/09/22 PHP
PHP memcache在微信公众平台的应用方法示例
2017/09/13 PHP
js类 from qq
2006/11/13 Javascript
理解Javascript_09_Function与Object
2010/10/16 Javascript
加速IE的Javascript document输出的方法
2010/12/02 Javascript
jQuery 1.5.1 发布,全面支持IE9 修复大量bug
2011/02/26 Javascript
可在线编辑网页文字效果代码(单击)
2013/03/02 Javascript
JavaScript自定义数组排序方法
2015/02/12 Javascript
webpack+vue.js实现组件化详解
2016/10/12 Javascript
vue 怎么创建组件及组件使用方法
2017/07/27 Javascript
浅析为什么a=&quot;abc&quot; 不等于 a=new String(&quot;abc&quot;)
2017/10/25 Javascript
Vue如何实现响应式系统
2018/07/11 Javascript
JS实现页面数据懒加载
2020/02/13 Javascript
Vue组件简易模拟实现购物车
2020/12/21 Vue.js
python批量导出导入MySQL用户的方法
2013/11/15 Python
在Python中用has_key()方法查找键是否存在的教程
2015/05/21 Python
python实现linux下使用xcopy的方法
2015/06/28 Python
Python抓取淘宝下拉框关键词的方法
2015/07/08 Python
django中账号密码验证登陆功能的实现方法
2019/07/15 Python
python标准库sys和OS的函数使用方法与实例详解
2020/02/12 Python
OpenCV图片漫画效果的实现示例
2020/08/18 Python
深入分析python 排序
2020/08/24 Python
Python爬虫教程之利用正则表达式匹配网页内容
2020/12/08 Python
Unineed中文官网:高端护肤美妆与时尚配饰,英国直邮
2020/07/23 全球购物
医学院校毕业生自荐信范文
2014/01/01 职场文书
消防器材管理制度
2014/01/28 职场文书
最新奶茶店创业计划书范文
2014/02/08 职场文书
幼儿教师研修感言
2014/02/12 职场文书
中班教师个人总结
2015/02/05 职场文书
努力工作保证书
2015/02/28 职场文书
心灵点滴观后感
2015/06/02 职场文书
Java中Dijkstra(迪杰斯特拉)算法
2022/05/20 Java/Android