<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
工作中常常會遇到這樣的場景,如訂單到期未支付取消,到期自動續費等,我們發現延遲佇列非常適合在這樣的場景中使用。常見的延遲佇列的優秀實現有rabbitMQ的死信佇列,RocketMQ的延遲佇列等,但是了有時候專案沒有特別的大,沒有引入類似的訊息中介軟體,但是了又遇到了特別適合使用延遲佇列的場景,我們一般會利用已有的redis實現一個簡陋的延遲佇列。常見的實現方式有監聽過期key,使用zset利用分值進行一個匹配,但是了這些實現或多或少有些問題,不夠優雅。監聽過期key是一種危險行為,一是如果過redis中key數量較大監聽過期key可能導致服務負載異常,二是redis中key過期後key是惰性刪除的,因此監聽機制需要主動觸發。利用zset分值實現呢,需要自己開發程式碼處理定時輪訓以及key刪除的邏輯,具有一定的工作量和複雜度。哪有沒有一種優雅的redis延遲佇列的實現呢?
Redisson是Redis伺服器上的分散式可伸縮Java資料結構----駐記憶體資料網格(In-Memory Data Grid,IMDG)。底層使用netty框架,並提供了與java物件相對應的分散式物件、分散式集合、分散式鎖和同步器、分散式服務等一系列的Redisson的分散式物件。為我們提供了許多開箱即用的功能。今天介紹Redisson實現的優雅的延遲佇列。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.12.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.homeey</groupId> <artifactId>redis-delay-queue</artifactId> <version>0.0.1-SNAPSHOT</version> <name>redis-delay-queue</name> <description>redis-delay-queue</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/org.redisson/redisson --> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.19.3</version> </dependency> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-data-23</artifactId> <version>3.19.3</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
springboot整合redisson有三種方式
詳細的整合檢視 springboot整合redisson設定
spring: redis: database: 0 host: localhost port: 6379 timeout: 10000 lettuce: pool: max-active: 8 max-wait: -1 min-idle: 0 max-idle: 8
package com.homeey.redisdelayqueue.delay; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.redisson.api.RBlockingQueue; import org.redisson.api.RDelayedQueue; import org.redisson.api.RedissonClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** * 明天的你會因今天到的努力而幸運 * * @author jt4mrg@qq.com * 23:11 2023-02-19 2023 **/ @Slf4j @Component @RequiredArgsConstructor public class RedissonDelayQueue { private final RDelayedQueue<String> delayedQueue; private final RBlockingQueue<String> blockingQueue; @PostConstruct public void init() { ExecutorService executorService = Executors.newFixedThreadPool(1); executorService.submit(() -> { while (true) { try { String task = blockingQueue.take(); log.info("rev delay task:{}", task); } catch (Exception e) { log.error("occur error", e); } } }); } public void offerTask(String task, long seconds) { log.info("add delay task:{},delay time:{}s", task, seconds); delayedQueue.offer(task, seconds, TimeUnit.SECONDS); } @Configuration static class RedissonDelayQueueConfigure { @Bean public RBlockingQueue<String> blockingQueue(RedissonClient redissonClient) { return redissonClient.getBlockingQueue("TOKEN-RENEWAL"); } @Bean public RDelayedQueue<String> delayedQueue(RBlockingQueue<String> blockingQueue, RedissonClient redissonClient) { return redissonClient.getDelayedQueue(blockingQueue); } } }
從RedissonDelayedQueue
實現中我們看到有四個角色
RedissonDelayedQueue
延遲佇列建立時,指定了佇列轉移服務,以及實現延遲佇列的四個重要校色的key。核心程式碼是指定佇列轉移任務
QueueTransferTask task = new QueueTransferTask(commandExecutor.getConnectionManager()) { @Override protected RFuture<Long> pushTaskAsync() { return commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_LONG, "local expiredValues = redis.call('zrangebyscore', KEYS[2], 0, ARGV[1], 'limit', 0, ARGV[2]); "//拿到zset中過期的值列表 + "if #expiredValues > 0 then " //如果有 + "for i, v in ipairs(expiredValues) do " + "local randomId, value = struct.unpack('dLc0', v);"//解構訊息,在提交任務時打包的訊息 + "redis.call('rpush', KEYS[1], value);" //放入無字首的list 隊頭 + "redis.call('lrem', KEYS[3], 1, v);"//移除帶字首list 隊尾元素 + "end; " + "redis.call('zrem', KEYS[2], unpack(expiredValues));" //移除zset中本次讀取的過期元素 + "end; " // get startTime from scheduler queue head task + "local v = redis.call('zrange', KEYS[2], 0, 0, 'WITHSCORES'); "//取zset最小分值的元素 + "if v[1] ~= nil then " + "return v[2]; " //返回分值,即過期時間 + "end " + "return nil;", Arrays.asList(getRawName(), timeoutSetName, queueName), System.currentTimeMillis(), 100); } @Override protected RTopic getTopic() { return RedissonTopic.createRaw(LongCodec.INSTANCE, commandExecutor, channelName); } };
核心程式碼RedissonDelayedQueue#offerAsync
return commandExecutor.evalWriteNoRetryAsync(getRawName(), codec, RedisCommands.EVAL_VOID, "local value = struct.pack('dLc0', tonumber(ARGV[2]), string.len(ARGV[3]), ARGV[3]);" //打包訊息體:訊息id,訊息長度,訊息值 + "redis.call('zadd', KEYS[2], ARGV[1], value);"//zset中加入訊息及其超時分值 + "redis.call('rpush', KEYS[3], value);" //向帶字首的list中新增訊息 // if new object added to queue head when publish its startTime // to all scheduler workers + "local v = redis.call('zrange', KEYS[2], 0, 0); "//取出zset中第一個元素 + "if v[1] == value then " //如果最快過期的元素就是這次傳送的訊息 + "redis.call('publish', KEYS[4], ARGV[1]); " //channel中釋出一下超時時間 + "end;", Arrays.asList(getRawName(), timeoutSetName, queueName, channelName), timeout, randomId, encode(e));
消費者最簡單,直接從不帶字首的list中BLPOP讀取就可以
Lua是redis的好朋友,我們可以看到Redisson實現延遲佇列時,大量使用到lua指令碼,因Redis會將整個指令碼作為一個整體執行,中間不會被其他請求插入。因此在指令碼執行過程中無需擔心會出現競態條件,無需使用事務。我們在平時開發時有多個redis命令操作的有簡單的業務邏輯,不妨嘗試一下lua指令碼的方式,可以避免使用分散式鎖來保障一致性。
Redisson的原始碼值得一讀,有很多新東西值得學習,如果其用到的netty基於時間輪演演算法的定時任務排程,可以讓我們基於此實現自己的任務排程框架,也讓我有了去探究這種實現方式和基於ScheduledThreadPoolExecutor的定時排程的差異及各自優劣的慾望。
以上就是Redis優雅地實現延遲佇列的方法分享的詳細內容,更多關於Redis延遲佇列的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45