├── README.assets ├── image-20221019164836734.png ├── image-20221019165019235.png ├── image-20221019165059870.png ├── image-20221019165158272.png ├── image-20221019165255782.png ├── image-20221019165355531.png └── image-20221019185748811.png ├── lisa-delay-queue-producer-spring-boot-autoconfigure ├── src │ └── main │ │ ├── resources │ │ ├── script │ │ │ └── lua │ │ │ │ ├── test_time_compare.lua │ │ │ │ ├── test_hget.lua │ │ │ │ ├── test.lua │ │ │ │ ├── test_list_result.lua │ │ │ │ ├── test_zrangebyscore_2.lua │ │ │ │ ├── test_xrange.lua │ │ │ │ ├── test_xpending.lua │ │ │ │ ├── test_zrangebyscore_1.lua │ │ │ │ ├── test_zrangebyscore_3.lua │ │ │ │ └── push_msg.lua │ │ └── META-INF │ │ │ └── spring.factories │ │ └── java │ │ └── org │ │ └── lisa │ │ └── delayqueue │ │ └── producer │ │ ├── service │ │ ├── Producer.java │ │ └── impl │ │ │ └── ProducerImpl.java │ │ ├── util │ │ └── PublishMessageUtil.java │ │ └── config │ │ └── DelayQueueProducerAutoConfiguration.java └── pom.xml ├── lisa-delay-queue-manager-spring-boot-autoconfigure ├── src │ └── main │ │ ├── resources │ │ ├── META-INF │ │ │ └── spring.factories │ │ └── script │ │ │ └── lua │ │ │ ├── move_msg_to_ready_queue.lua │ │ │ └── pending_msg_to_retry_queue.lua │ │ └── java │ │ └── org │ │ └── lisa │ │ └── delayqueue │ │ └── manager │ │ ├── config │ │ ├── ProcessPendingMessageConfig.java │ │ └── DelayQueueManagerAutoConfiguration.java │ │ └── scheduled │ │ ├── CleanStreamJob.java │ │ ├── ProcessPendingMessageJob.java │ │ └── MoveMessageToReadyQueueJob.java └── pom.xml ├── lisa-delay-queue-consumer-spring-boot-autoconfigure ├── src │ └── main │ │ ├── resources │ │ ├── META-INF │ │ │ └── spring.factories │ │ └── script │ │ │ └── lua │ │ │ └── ack_message.lua │ │ └── java │ │ └── org │ │ └── lisa │ │ └── delayqueue │ │ └── consumer │ │ ├── listener │ │ ├── MessageEvent.java │ │ └── DefaultStreamListener.java │ │ └── config │ │ ├── DelayQueueConsumerServerConfig.java │ │ └── DelayQueueConsumerAutoConfiguration.java └── pom.xml ├── lisa-delay-queue-manager-demo ├── src │ ├── main │ │ ├── java │ │ │ └── org │ │ │ │ └── lisa │ │ │ │ └── delayqueue │ │ │ │ └── manager │ │ │ │ └── ManagerApplication.java │ │ └── resources │ │ │ └── application.yml │ └── test │ │ └── java │ │ └── org │ │ └── lisa │ │ └── delayqueue │ │ └── manager │ │ └── SimpleTest.java └── pom.xml ├── lisa-delay-queue-consumer-demo ├── src │ ├── main │ │ ├── java │ │ │ └── org │ │ │ │ └── lisa │ │ │ │ └── delayqueue │ │ │ │ ├── consumer │ │ │ │ ├── ConsumerApplication.java │ │ │ │ └── service │ │ │ │ │ └── DemoService.java │ │ │ │ └── producer │ │ │ │ └── dto │ │ │ │ ├── User.java │ │ │ │ └── OrderInfo.java │ │ └── resources │ │ │ └── application.yml │ └── test │ │ └── java │ │ └── org │ │ └── lisa │ │ └── delayqueue │ │ └── comsumer │ │ └── SimpleTest.java └── pom.xml ├── .gitignore ├── lisa-delay-queue-producer-demo ├── src │ ├── main │ │ ├── java │ │ │ └── org │ │ │ │ └── lisa │ │ │ │ └── delayqueue │ │ │ │ └── producer │ │ │ │ ├── dto │ │ │ │ ├── User.java │ │ │ │ └── OrderInfo.java │ │ │ │ ├── ProducerApplication.java │ │ │ │ ├── controller │ │ │ │ └── RedisStreamController.java │ │ │ │ └── service │ │ │ │ └── DemoService.java │ │ └── resources │ │ │ └── application.yml │ └── test │ │ └── java │ │ └── org │ │ └── lisa │ │ └── delayqueue │ │ └── producer │ │ └── SimpleProducerTest.java └── pom.xml ├── lisa-delay-queue-producer-and-manager-demo ├── src │ └── main │ │ ├── java │ │ └── org │ │ │ └── lisa │ │ │ └── delayqueue │ │ │ └── producer_and_manager │ │ │ ├── dto │ │ │ ├── User.java │ │ │ └── OrderInfo.java │ │ │ ├── ProducerAndManagerApplication.java │ │ │ ├── controller │ │ │ └── RedisStreamController.java │ │ │ └── service │ │ │ └── DemoService.java │ │ └── resources │ │ └── application.yml └── pom.xml ├── lisa-delay-queue-base ├── src │ └── main │ │ └── java │ │ └── org │ │ └── lisa │ │ └── delayqueue │ │ └── base │ │ ├── entity │ │ └── Message.java │ │ ├── config │ │ └── DelayQueueConfigProperties.java │ │ ├── constant │ │ └── Constant.java │ │ └── util │ │ ├── SpringContextUtil.java │ │ └── RedisScriptUtils.java └── pom.xml ├── lisa-delay-queue-manager-spring-boot-starter └── pom.xml ├── lisa-delay-queue-consumer-spring-boot-starter └── pom.xml ├── lisa-delay-queue-producer-spring-boot-starter └── pom.xml ├── pom.xml ├── README.md └── LICENSE /README.assets/image-20221019164836734.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solid210/lisa-delay-queue/HEAD/README.assets/image-20221019164836734.png -------------------------------------------------------------------------------- /README.assets/image-20221019165019235.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solid210/lisa-delay-queue/HEAD/README.assets/image-20221019165019235.png -------------------------------------------------------------------------------- /README.assets/image-20221019165059870.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solid210/lisa-delay-queue/HEAD/README.assets/image-20221019165059870.png -------------------------------------------------------------------------------- /README.assets/image-20221019165158272.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solid210/lisa-delay-queue/HEAD/README.assets/image-20221019165158272.png -------------------------------------------------------------------------------- /README.assets/image-20221019165255782.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solid210/lisa-delay-queue/HEAD/README.assets/image-20221019165255782.png -------------------------------------------------------------------------------- /README.assets/image-20221019165355531.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solid210/lisa-delay-queue/HEAD/README.assets/image-20221019165355531.png -------------------------------------------------------------------------------- /README.assets/image-20221019185748811.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solid210/lisa-delay-queue/HEAD/README.assets/image-20221019185748811.png -------------------------------------------------------------------------------- /lisa-delay-queue-producer-spring-boot-autoconfigure/src/main/resources/script/lua/test_time_compare.lua: -------------------------------------------------------------------------------- 1 | local key = KEYS[1] 2 | local expectAt = ARGV[1] 3 | local now = ARGV[2] 4 | 5 | return now >= expectAt -------------------------------------------------------------------------------- /lisa-delay-queue-manager-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.lisa.delayqueue.manager.config.DelayQueueManagerAutoConfiguration -------------------------------------------------------------------------------- /lisa-delay-queue-consumer-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.lisa.delayqueue.consumer.config.DelayQueueConsumerAutoConfiguration -------------------------------------------------------------------------------- /lisa-delay-queue-producer-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.lisa.delayqueue.producer.config.DelayQueueProducerAutoConfiguration -------------------------------------------------------------------------------- /lisa-delay-queue-producer-spring-boot-autoconfigure/src/main/resources/script/lua/test_hget.lua: -------------------------------------------------------------------------------- 1 | local key = KEYS[1] 2 | local field = ARGV[1] 3 | 4 | local retryCount = redis.call('hget', key, field) 5 | if not(retryCount) then 6 | return 0 7 | end 8 | return retryCount -------------------------------------------------------------------------------- /lisa-delay-queue-producer-spring-boot-autoconfigure/src/main/resources/script/lua/test.lua: -------------------------------------------------------------------------------- 1 | local value = ARGV[1] 2 | local key = KEYS[1] 3 | 4 | redis.call('SET', key, value) 5 | 6 | local result = redis.call('GET', key) 7 | local msg = {} 8 | msg[1] = "hello" 9 | msg[2] = "world" 10 | 11 | return json.encode(msg) -------------------------------------------------------------------------------- /lisa-delay-queue-producer-spring-boot-autoconfigure/src/main/resources/script/lua/test_list_result.lua: -------------------------------------------------------------------------------- 1 | local function test(val) 2 | local ret1 = {1, 2} 3 | local ret2 = "hello" 4 | local ret3 = val 5 | local ret = {} 6 | ret[1] = ret1 7 | ret[2] = ret2 8 | ret[3] = ret3 9 | return ret 10 | end 11 | return test(KEYS[1]) -------------------------------------------------------------------------------- /lisa-delay-queue-producer-spring-boot-autoconfigure/src/main/resources/script/lua/test_zrangebyscore_2.lua: -------------------------------------------------------------------------------- 1 | local key_zset = KEYS[1] 2 | local key_stream = KEYS[2] 3 | local score = ARGV[1] 4 | local count = ARGV[2] 5 | 6 | local msgIList = redis.call('zrangebyscore', key_zset, 0, score, 'withscores', 'limit', 0, count) 7 | 8 | -- 将取出的msgId存入stream中 9 | redis.call('xadd', key_stream, msgIList) -------------------------------------------------------------------------------- /lisa-delay-queue-producer-spring-boot-autoconfigure/src/main/resources/script/lua/test_xrange.lua: -------------------------------------------------------------------------------- 1 | local streamKey = KEYS[1] 2 | local recordId = ARGV[1] 3 | 4 | local data = redis.call('xrange', streamKey, recordId, recordId) 5 | local msg = data[1][2][2] 6 | local position = string.find(msg, '|') 7 | local msgId = string.sub(msg, 1, position - 1) 8 | local score = string.sub(msg, position + 1) 9 | return score -------------------------------------------------------------------------------- /lisa-delay-queue-producer-spring-boot-autoconfigure/src/main/resources/script/lua/test_xpending.lua: -------------------------------------------------------------------------------- 1 | local key = KEYS[1] 2 | local group = ARGV[1] 3 | local range_start = ARGV[2] 4 | local range_end = ARGV[3] 5 | local count = ARGV[4] 6 | 7 | local results = redis.call('XPENDING', key, group, range_start, range_end, count) 8 | --local results = redis.call('XPENDING', key, group) 9 | --local results = redis.call('XPENDING', key, group, "-", "+", count) 10 | return results -------------------------------------------------------------------------------- /lisa-delay-queue-manager-demo/src/main/java/org/lisa/delayqueue/manager/ManagerApplication.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.manager; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * @author solid 8 | */ 9 | @SpringBootApplication 10 | public class ManagerApplication { 11 | 12 | public static void main(String[] args) { 13 | SpringApplication.run(ManagerApplication.class, args); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lisa-delay-queue-producer-spring-boot-autoconfigure/src/main/resources/script/lua/test_zrangebyscore_1.lua: -------------------------------------------------------------------------------- 1 | local key_zset = KEYS[1] 2 | local score = ARGV[1] 3 | local count = ARGV[2] 4 | 5 | local msgIdAndScoreList = redis.call('zrangebyscore', key_zset, 0, score, 'withscores', 'limit', 0, count) 6 | local data = {} 7 | for i = 1, #msgIdAndScoreList, 2 do 8 | local msg = {} 9 | msg[1] = msgIdAndScoreList[i] 10 | msg[2] = msgIdAndScoreList[i + 1] 11 | table.insert(data, msg) 12 | end 13 | return data -------------------------------------------------------------------------------- /lisa-delay-queue-consumer-demo/src/main/java/org/lisa/delayqueue/consumer/ConsumerApplication.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.consumer; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * @author solid 8 | */ 9 | @SpringBootApplication 10 | public class ConsumerApplication { 11 | 12 | public static void main(String[] args) { 13 | SpringApplication.run(ConsumerApplication.class, args); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lisa-delay-queue-consumer-demo/src/main/java/org/lisa/delayqueue/producer/dto/User.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.producer.dto; 2 | 3 | import lombok.Data; 4 | import lombok.ToString; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * @description: 10 | * @author: wuxu 11 | * @createDate: 2022/9/26 12 | */ 13 | @Data 14 | @ToString 15 | public class User implements Serializable { 16 | 17 | private static final long serialVersionUID = -5085323625744225501L; 18 | 19 | private Long id; 20 | 21 | private String name; 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /lisa-delay-queue-consumer-spring-boot-autoconfigure/src/main/resources/script/lua/ack_message.lua: -------------------------------------------------------------------------------- 1 | --确认消费message,即ack 2 | local streamKey = KEYS[1] 3 | local retryCountKey = KEYS[2] 4 | local garbageKey = KEYS[3] 5 | local group = ARGV[1] 6 | local msgId = ARGV[2] 7 | local msgBodyKey = ARGV[3] 8 | local recordId = ARGV[4] 9 | 10 | -- 向stream发出ack命令 11 | redis.call('xack', streamKey, group, recordId) 12 | 13 | -- 同时删除msgId对应的msgValue,释放内存 14 | local count = redis.call('del', msgBodyKey) 15 | redis.call('hdel', retryCountKey, msgId) 16 | redis.call('srem', garbageKey, msgId) 17 | 18 | return count -------------------------------------------------------------------------------- /lisa-delay-queue-producer-spring-boot-autoconfigure/src/main/resources/script/lua/test_zrangebyscore_3.lua: -------------------------------------------------------------------------------- 1 | local key_zset = KEYS[1] 2 | local score = ARGV[1] 3 | local count = ARGV[2] 4 | 5 | local msgIdAndScoreList = redis.call('zrangebyscore', key_zset, 0, score, 'withscores', 'limit', 0, count) 6 | local data = {} 7 | for i = 1, #msgIdAndScoreList, 2 do 8 | local msg = {} 9 | msg[1] = msgIdAndScoreList[i] 10 | msg[2] = msgIdAndScoreList[i + 1] 11 | table.insert(data, msg) 12 | -- 从zset中删除这些msgId 13 | redis.call('zrem', key_zset, tostring(msg[1])) 14 | end 15 | return data -------------------------------------------------------------------------------- /lisa-delay-queue-producer-demo/src/main/java/org/lisa/delayqueue/producer/dto/User.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.producer.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.ToString; 7 | 8 | import java.io.Serializable; 9 | 10 | /** 11 | * @description: 12 | * @author: wuxu 13 | * @createDate: 2022/9/26 14 | */ 15 | @Data 16 | @ToString 17 | public class User implements Serializable { 18 | 19 | private static final long serialVersionUID = -5085323625744225501L; 20 | 21 | private Long id; 22 | 23 | private String name; 24 | } 25 | -------------------------------------------------------------------------------- /lisa-delay-queue-producer-demo/src/main/java/org/lisa/delayqueue/producer/dto/OrderInfo.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.producer.dto; 2 | 3 | import lombok.Data; 4 | import lombok.ToString; 5 | 6 | import java.io.Serializable; 7 | import java.time.LocalDateTime; 8 | /** 9 | * @description: 10 | * @author: wuxu 11 | * @createDate: 2022/9/26 12 | */ 13 | @Data 14 | @ToString 15 | public class OrderInfo implements Serializable { 16 | 17 | private static final long serialVersionUID = -3246066729154836541L; 18 | 19 | private Long orderNo; 20 | 21 | private Long userId; 22 | 23 | private LocalDateTime createTime; 24 | } 25 | -------------------------------------------------------------------------------- /lisa-delay-queue-producer-demo/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8091 3 | servlet: 4 | context-path: / 5 | 6 | spring: 7 | redis: 8 | database: 0 9 | host: 192.168.95.220 10 | port: 6379 11 | password: 12 | timeout: 0 13 | lettuce: 14 | pool: 15 | max-active: 8 16 | max-wait: -1 17 | max-idle: 8 18 | min-idle: 0 19 | 20 | lisa-delay-queue: 21 | enabled: true 22 | groups: 23 | - topic: mystream 24 | group: group-1 25 | # - topic: mystream2 26 | # group: group-2 27 | # consumer1: consumer-2 28 | # consumers: [ consumer-2 ] -------------------------------------------------------------------------------- /lisa-delay-queue-consumer-demo/src/main/java/org/lisa/delayqueue/producer/dto/OrderInfo.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.producer.dto; 2 | 3 | import lombok.Data; 4 | import lombok.ToString; 5 | 6 | import java.io.Serializable; 7 | import java.time.LocalDateTime; 8 | 9 | /** 10 | * @description: 11 | * @author: wuxu 12 | * @createDate: 2022/9/26 13 | */ 14 | @Data 15 | @ToString 16 | public class OrderInfo implements Serializable { 17 | 18 | private static final long serialVersionUID = -3246066729154836541L; 19 | 20 | private Long orderNo; 21 | 22 | private Long userId; 23 | 24 | private LocalDateTime createTime; 25 | } 26 | -------------------------------------------------------------------------------- /lisa-delay-queue-producer-and-manager-demo/src/main/java/org/lisa/delayqueue/producer_and_manager/dto/User.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.producer_and_manager.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.ToString; 7 | 8 | import java.io.Serializable; 9 | 10 | /** 11 | * @description: 12 | * @author: wuxu 13 | * @createDate: 2022/9/26 14 | */ 15 | @Data 16 | @ToString 17 | public class User implements Serializable { 18 | 19 | private static final long serialVersionUID = -5085323625744225501L; 20 | 21 | private Long id; 22 | 23 | private String name; 24 | } 25 | -------------------------------------------------------------------------------- /lisa-delay-queue-producer-spring-boot-autoconfigure/src/main/resources/script/lua/push_msg.lua: -------------------------------------------------------------------------------- 1 | -- 将消息的msgId加入zset中,同时存储msgId与msgValue映射数据 2 | 3 | local waitingQueueKey = KEYS[1] 4 | local readyQueueKey = KEYS[2] 5 | local msgId = ARGV[1] 6 | local score = ARGV[2] 7 | local msgBodyKey = ARGV[3] 8 | local msgBodyValue = ARGV[4] 9 | local now = ARGV[5] 10 | 11 | redis.call('set', msgBodyKey, msgBodyValue) 12 | if now >= score then 13 | -- 当前时间大于期望执行时间,直接将消息加入stream中(立即执行) 14 | local msg = msgId .. '|' .. score 15 | redis.call('xadd', readyQueueKey, '*', 'msg', msg) 16 | else 17 | redis.call('zadd', waitingQueueKey, score, msgId) 18 | end 19 | return true -------------------------------------------------------------------------------- /lisa-delay-queue-producer-and-manager-demo/src/main/java/org/lisa/delayqueue/producer_and_manager/dto/OrderInfo.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.producer_and_manager.dto; 2 | 3 | import lombok.Data; 4 | import lombok.ToString; 5 | 6 | import java.io.Serializable; 7 | import java.time.LocalDateTime; 8 | /** 9 | * @description: 10 | * @author: wuxu 11 | * @createDate: 2022/9/26 12 | */ 13 | @Data 14 | @ToString 15 | public class OrderInfo implements Serializable { 16 | 17 | private static final long serialVersionUID = -3246066729154836541L; 18 | 19 | private Long orderNo; 20 | 21 | private Long userId; 22 | 23 | private LocalDateTime createTime; 24 | } 25 | -------------------------------------------------------------------------------- /lisa-delay-queue-producer-demo/src/main/java/org/lisa/delayqueue/producer/ProducerApplication.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.producer; 2 | 3 | 4 | import org.lisa.delayqueue.base.util.SpringContextUtil; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.context.annotation.Import; 8 | 9 | /** 10 | * @author solid 11 | */ 12 | @SpringBootApplication 13 | @Import(SpringContextUtil.class) 14 | public class ProducerApplication { 15 | 16 | public static void main(String[] args) { 17 | SpringApplication.run(ProducerApplication.class, args); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /lisa-delay-queue-consumer-demo/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8092 3 | servlet: 4 | context-path: / 5 | 6 | spring: 7 | redis: 8 | database: 0 9 | host: 192.168.95.220 10 | port: 6379 11 | password: 12 | timeout: 0 13 | lettuce: 14 | pool: 15 | max-active: 8 16 | max-wait: -1 17 | max-idle: 8 18 | min-idle: 0 19 | 20 | lisa-delay-queue: 21 | enabled: true 22 | consumer-server: 23 | pollTimeoutMillis: 5000 24 | pollBatchSize: 10 25 | groups: 26 | - topic: mystream 27 | group: group-1 28 | consumer: consumer-1 29 | # - topic: mystream2 30 | # group: group-2 31 | # consumer: consumer-2 32 | -------------------------------------------------------------------------------- /lisa-delay-queue-consumer-spring-boot-autoconfigure/src/main/java/org/lisa/delayqueue/consumer/listener/MessageEvent.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.consumer.listener; 2 | 3 | import lombok.Getter; 4 | import lombok.ToString; 5 | import org.springframework.context.ApplicationEvent; 6 | 7 | /** 8 | * @description: 9 | * @author: wuxu 10 | * @createDate: 2022/9/25 11 | */ 12 | @ToString(callSuper = true) 13 | @Getter 14 | public class MessageEvent extends ApplicationEvent { 15 | 16 | private static final long serialVersionUID = -5546589030626479941L; 17 | 18 | private final String topic; 19 | 20 | public MessageEvent(String topic, Object source) { 21 | super(source); 22 | this.topic = topic; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lisa-delay-queue-producer-and-manager-demo/src/main/java/org/lisa/delayqueue/producer_and_manager/ProducerAndManagerApplication.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.producer_and_manager; 2 | 3 | 4 | import org.lisa.delayqueue.base.util.SpringContextUtil; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.context.annotation.Import; 8 | 9 | /** 10 | * @author solid 11 | */ 12 | @SpringBootApplication 13 | @Import(SpringContextUtil.class) 14 | public class ProducerAndManagerApplication { 15 | 16 | public static void main(String[] args) { 17 | SpringApplication.run(ProducerAndManagerApplication.class, args); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /lisa-delay-queue-producer-demo/src/main/java/org/lisa/delayqueue/producer/controller/RedisStreamController.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.producer.controller; 2 | 3 | 4 | import org.lisa.delayqueue.producer.service.DemoService; 5 | import io.lettuce.core.dynamic.annotation.Param; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | import javax.annotation.Resource; 10 | 11 | /** 12 | * @author solid 13 | */ 14 | @RestController 15 | public class RedisStreamController { 16 | 17 | @Resource 18 | private DemoService demoService; 19 | 20 | @GetMapping("produceMsg") 21 | public void produceMsg(@Param("msg") String msg) { 22 | demoService.test(msg); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lisa-delay-queue-base/src/main/java/org/lisa/delayqueue/base/entity/Message.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.base.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.ToString; 7 | 8 | import java.io.Serializable; 9 | 10 | /** 11 | * @description: 12 | * @author: wuxu 13 | * @createDate: 2022/9/26 14 | */ 15 | @Data 16 | @ToString 17 | @NoArgsConstructor 18 | @AllArgsConstructor 19 | public class Message implements Serializable { 20 | 21 | private static final long serialVersionUID = 5384673765770002045L; 22 | 23 | private T body; 24 | 25 | private Class clazz; 26 | 27 | public Message(T body){ 28 | this.body = body; 29 | this.clazz = body.getClass(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lisa-delay-queue-producer-and-manager-demo/src/main/java/org/lisa/delayqueue/producer_and_manager/controller/RedisStreamController.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.producer_and_manager.controller; 2 | 3 | 4 | import io.lettuce.core.dynamic.annotation.Param; 5 | import org.lisa.delayqueue.producer_and_manager.service.DemoService; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | import javax.annotation.Resource; 10 | 11 | /** 12 | * @author solid 13 | */ 14 | @RestController 15 | public class RedisStreamController { 16 | 17 | @Resource 18 | private DemoService demoService; 19 | 20 | @GetMapping("produceMsg") 21 | public void produceMsg(@Param("msg") String msg) { 22 | demoService.test(msg); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lisa-delay-queue-consumer-spring-boot-autoconfigure/src/main/java/org/lisa/delayqueue/consumer/config/DelayQueueConsumerServerConfig.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.consumer.config; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | import static org.lisa.delayqueue.base.constant.Constant.LISA_DELAY_QUEUE_CONSUMER_SERVER_CONFIG_PREFIX; 7 | 8 | /** 9 | * @description: 10 | * @author: wuxu 11 | * @createDate: 2022/10/17 12 | */ 13 | @Data 14 | @ConfigurationProperties(prefix = LISA_DELAY_QUEUE_CONSUMER_SERVER_CONFIG_PREFIX) 15 | public class DelayQueueConsumerServerConfig { 16 | 17 | /** 18 | * 消息拉取超时时间 19 | */ 20 | private int pollTimeoutMillis = 5000; 21 | 22 | /** 23 | * 批量抓取消息数量 24 | */ 25 | private int pollBatchSize = 10; 26 | } 27 | -------------------------------------------------------------------------------- /lisa-delay-queue-manager-spring-boot-autoconfigure/src/main/resources/script/lua/move_msg_to_ready_queue.lua: -------------------------------------------------------------------------------- 1 | -- 将到期需要执行的msgId从zset移动到stream中,有两个地方需要用到 2 | -- 1. move message from waiting queue to ready queue 3 | -- 2. move message from retry queue to ready queue 4 | local keyZset = KEYS[1] 5 | local readyQueueKey = KEYS[2] 6 | local startScore = ARGV[1] 7 | local endScore = ARGV[2] 8 | local count = ARGV[3] 9 | 10 | -- 从zset中取出count条到期的msgId 11 | local msgIdAndScoreList = redis.call('zrangebyscore', keyZset, startScore, endScore, 'withscores', 'limit', 0, count) 12 | local data = {} 13 | for i = 1, #msgIdAndScoreList, 2 do 14 | local msgId = msgIdAndScoreList[i] 15 | local msg = msgIdAndScoreList[i] .. '|' .. msgIdAndScoreList[i + 1] 16 | table.insert(data, msg) 17 | -- 将取出的msgId存入stream中 18 | redis.call('xadd', readyQueueKey, '*', 'msg', msg) 19 | -- 从zset中删除这些msgId 20 | redis.call('zrem', keyZset, tostring(msgId)) 21 | end 22 | return data -------------------------------------------------------------------------------- /lisa-delay-queue-manager-demo/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8090 3 | servlet: 4 | context-path: / 5 | 6 | spring: 7 | redis: 8 | database: 0 9 | host: 192.168.95.220 10 | port: 6379 11 | password: 12 | timeout: 0 13 | lettuce: 14 | pool: 15 | max-active: 8 16 | max-wait: -1 17 | max-idle: 8 18 | min-idle: 0 19 | 20 | lisa-delay-queue: 21 | manager-server: 22 | crontab-move-to-ready-queue: '0/1 * * * * ?' 23 | crontab-clean-stream: '0 */1 * * * ?' 24 | crontab-process-pending-message: '0/5 * * * * ?' 25 | range-start: '-' 26 | range-end: '+' 27 | count: 20 28 | timeout: 10000 29 | delay-time: 20000 30 | max-retry-count: 10 31 | enabled: true 32 | groups: 33 | - topic: mystream 34 | group: group-1 35 | consumer: consumer-1 36 | max-length: 10000 37 | # - topic: mystream2 38 | # group: group-2 39 | # consumer: consumer-2 40 | -------------------------------------------------------------------------------- /lisa-delay-queue-manager-spring-boot-starter/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | 8 | lisa-delay-queue 9 | org.lisa.stream 10 | 1.0.0-SNAPSHOT 11 | 12 | 13 | lisa-delay-queue-manager-spring-boot-starter 14 | 1.0.0 15 | 16 | lisa-delay-queue-manager-spring-boot-starter 17 | 18 | 19 | 20 | org.lisa.stream 21 | lisa-delay-queue-manager-spring-boot-autoconfigure 22 | 1.0.0-SNAPSHOT 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /lisa-delay-queue-consumer-spring-boot-starter/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | 8 | lisa-delay-queue 9 | org.lisa.stream 10 | 1.0.0-SNAPSHOT 11 | 12 | 13 | lisa-delay-queue-consumer-spring-boot-starter 14 | 1.0.0 15 | 16 | lisa-delay-queue-consumer-spring-boot-starter 17 | 18 | 19 | 20 | org.lisa.stream 21 | lisa-delay-queue-consumer-spring-boot-autoconfigure 22 | 1.0.0-SNAPSHOT 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /lisa-delay-queue-producer-spring-boot-starter/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | 8 | lisa-delay-queue 9 | org.lisa.stream 10 | 1.0.0-SNAPSHOT 11 | 12 | 13 | lisa-delay-queue-producer-spring-boot-starter 14 | 1.0.0 15 | 16 | lisa-delay-queue-producer-spring-boot-starter 17 | 18 | 19 | 20 | org.lisa.stream 21 | lisa-delay-queue-producer-spring-boot-autoconfigure 22 | 1.0.0-SNAPSHOT 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /lisa-delay-queue-producer-and-manager-demo/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8091 3 | servlet: 4 | context-path: / 5 | 6 | spring: 7 | redis: 8 | database: 0 9 | host: 192.168.95.220 10 | port: 6379 11 | password: 12 | timeout: 0 13 | lettuce: 14 | pool: 15 | max-active: 8 16 | max-wait: -1 17 | max-idle: 8 18 | min-idle: 0 19 | 20 | lisa-delay-queue: 21 | manager-server: 22 | crontab-move-to-ready-queue: '0/1 * * * * ?' 23 | crontab-clean-stream: '0 */1 * * * ?' 24 | crontab-process-pending-message: '0/5 * * * * ?' 25 | range-start: '-' 26 | range-end: '+' 27 | count: 20 28 | timeout: 10000 29 | delay-time: 20000 30 | max-retry-count: 10 31 | enabled: true 32 | groups: 33 | - topic: mystream 34 | group: group-1 35 | consumer: consumer-1 36 | max-length: 10000 37 | moving-size: 20 38 | # - topic: mystream2 39 | # group: group-2 40 | # consumer1: consumer-2 41 | # consumers: [ consumer-2 ] -------------------------------------------------------------------------------- /lisa-delay-queue-manager-spring-boot-autoconfigure/src/main/java/org/lisa/delayqueue/manager/config/ProcessPendingMessageConfig.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.manager.config; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | import static org.lisa.delayqueue.base.constant.Constant.LISA_DELAY_QUEUE_MANAGER_SERVER_CONFIG_PREFIX; 7 | 8 | /** 9 | * @description: 10 | * @author: wuxu 11 | * @createDate: 2022/10/17 12 | */ 13 | @Data 14 | @ConfigurationProperties(prefix = LISA_DELAY_QUEUE_MANAGER_SERVER_CONFIG_PREFIX) 15 | public class ProcessPendingMessageConfig { 16 | 17 | /** 18 | * 命令举例::xpending stream:ready_queue:mystream group-1 - + 20 19 | */ 20 | private String rangeStart = "-"; 21 | 22 | private String rangeEnd = "+"; 23 | 24 | private int count = 20; 25 | 26 | /** 27 | * pending多久后算超时,开始进行超时处理 28 | */ 29 | private int timeout = 10000; 30 | 31 | /** 32 | * 推迟多久后运行 33 | */ 34 | private int delayTime = 20000; 35 | 36 | /** 37 | * 最大重试次数 38 | */ 39 | private int maxRetryCount = 10; 40 | } 41 | -------------------------------------------------------------------------------- /lisa-delay-queue-base/src/main/java/org/lisa/delayqueue/base/config/DelayQueueConfigProperties.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.base.config; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * @author solid 10 | */ 11 | @Data 12 | @ConfigurationProperties(prefix = DelayQueueConfigProperties.LISA_DELAY_QUEUE_CONFIG_PREFIX) 13 | public class DelayQueueConfigProperties { 14 | 15 | public static final String LISA_DELAY_QUEUE_CONFIG_PREFIX = "lisa-delay-queue"; 16 | public static final String PROPERTY_NAME_GROUPS = "groups"; 17 | 18 | private List groups; 19 | 20 | @Data 21 | public static class DelayQueueConfig { 22 | /** 23 | * 延迟队列的topic,必填 24 | */ 25 | private String topic; 26 | 27 | /** 28 | * 延迟队列分组,必填 29 | */ 30 | private String group; 31 | 32 | /** 33 | * 消费者名称,作为consumer使用时必填 34 | */ 35 | private String consumer; 36 | 37 | /** 38 | * stream队列长度(默认1000),如果队列长度超过该值,则会进行修剪 39 | */ 40 | private int maxLength = 1000; 41 | 42 | /** 43 | * 单次移动的消息数量 44 | */ 45 | private int movingSize = 100; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /lisa-delay-queue-producer-spring-boot-autoconfigure/src/main/java/org/lisa/delayqueue/producer/service/Producer.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.producer.service; 2 | 3 | 4 | import org.lisa.delayqueue.base.entity.Message; 5 | 6 | import java.time.LocalDateTime; 7 | import java.time.ZoneOffset; 8 | import java.util.Map; 9 | import java.util.concurrent.ConcurrentHashMap; 10 | 11 | /** 12 | * 发送消息 13 | * 14 | * @author solid 15 | */ 16 | public interface Producer { 17 | 18 | static Map STREAM_MAP = new ConcurrentHashMap<>(); 19 | 20 | /** 21 | * 发送消息(立即发送,基本上会延迟一秒) 22 | * 23 | * @param msg 消息体对象 24 | */ 25 | public void send(Message msg); 26 | 27 | /** 28 | * 发送消息(定时发送) 29 | * 30 | * @param msg 消息体对象 31 | * @param expectAt 期望在何时发送(时间戳) 32 | */ 33 | public void send(Message msg, long expectAt); 34 | 35 | /** 36 | * 发送消息(定时发送) 37 | * 38 | * @param msg 消息体对象 39 | * @param expectAt 期望在何时发送(时间戳) 40 | */ 41 | public void send(Message msg, LocalDateTime expectAt); 42 | 43 | /** 44 | * 发送消息(定时发送) 45 | * 46 | * @param msg 消息体对象 47 | * @param expectAt 期望在何时发送(时间戳) 48 | * @param zoneOffset 拾取 49 | */ 50 | public void send(Message msg, LocalDateTime expectAt, ZoneOffset zoneOffset); 51 | } 52 | -------------------------------------------------------------------------------- /lisa-delay-queue-base/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | lisa-delay-queue 6 | org.lisa.stream 7 | 1.0.0-SNAPSHOT 8 | 9 | 4.0.0 10 | 11 | lisa-delay-queue-base 12 | 1.0.0-SNAPSHOT 13 | lisa-delay-queue-base 14 | 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-starter-data-redis 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter 24 | 25 | 26 | 27 | 28 | 29 | 30 | org.apache.maven.plugins 31 | maven-compiler-plugin 32 | 33 | 1.8 34 | 1.8 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /lisa-delay-queue-base/src/main/java/org/lisa/delayqueue/base/constant/Constant.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.base.constant; 2 | 3 | /** 4 | * @description: 5 | * @author: wuxu 6 | * @createDate: 2022/10/3 7 | */ 8 | public class Constant { 9 | 10 | public static final String ZSET_WAITING_QUEUE = "zset:waiting_queue:"; 11 | 12 | public static final String STREAM_READY_QUEUE = "stream:ready_queue:"; 13 | 14 | public static final String ZSET_RETRY_QUEUE = "zset:retry_queue:"; 15 | 16 | public static final String HASH_RETRY_COUNT = "hash:retry_count:"; 17 | 18 | public static final String SET_GARBAGE_KEY = "set:garbage_key:"; 19 | 20 | public static final String MESSAGE_BODY = "message:body:"; 21 | 22 | public static final String INIT_MESSAGE = "init message...remember lisa, cqy, stone, miles, liam by solid"; 23 | 24 | public static final String BEAN_NAME_SUFFIX_MESSAGE_PRODUCER = "_MessageProducer"; 25 | 26 | public static final String SERVER_NAME_PRODUCER = "lisa-delay-queue-producer"; 27 | 28 | public static final String SERVER_NAME_MANAGER = "lisa-delay-queue-manager"; 29 | 30 | public static final String SERVER_NAME_CONSUMER = "lisa-delay-queue-consumer"; 31 | 32 | /** 33 | * consumer服务配置项前缀 34 | */ 35 | public static final String LISA_DELAY_QUEUE_CONSUMER_SERVER_CONFIG_PREFIX = "lisa-delay-queue.consumer-server"; 36 | 37 | /** 38 | * producer服务配置项前缀 39 | */ 40 | public static final String LISA_DELAY_QUEUE_PRODUCER_SERVER_CONFIG_PREFIX = "lisa-delay-queue.producer-server"; 41 | 42 | /** 43 | * manager服务配置项前缀 44 | */ 45 | public static final String LISA_DELAY_QUEUE_MANAGER_SERVER_CONFIG_PREFIX = "lisa-delay-queue.manager-server"; 46 | } 47 | -------------------------------------------------------------------------------- /lisa-delay-queue-base/src/main/java/org/lisa/delayqueue/base/util/SpringContextUtil.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.base.util; 2 | 3 | import org.springframework.context.ApplicationContext; 4 | import org.springframework.context.ApplicationContextAware; 5 | import org.springframework.stereotype.Component; 6 | 7 | /** 8 | * @author solid 9 | */ 10 | @Component 11 | public final class SpringContextUtil implements ApplicationContextAware { 12 | 13 | /** 14 | * 以静态变量保存ApplicationContext,可在任意代码中取出ApplicaitonContext. 15 | */ 16 | private static ApplicationContext context; 17 | 18 | /** 19 | * 从静态变量ApplicationContext中取得Bean, 自动转型为所赋值对象的类型. 20 | */ 21 | public static T getBean(Class requiredType) { 22 | if (context == null) { 23 | return null; 24 | } 25 | return context.getBean(requiredType); 26 | } 27 | 28 | /** 29 | * 从静态变量ApplicationContext中取得Bean, 自动转型为所赋值对象的类型. 30 | */ 31 | public static T getBean(String name, Class requiredType) { 32 | if (context == null) { 33 | return null; 34 | } 35 | return context.getBean(name, requiredType); 36 | } 37 | 38 | /** 39 | * 从静态变量ApplicationContext中取得Bean 40 | */ 41 | public static Object getBean(String beanName) { 42 | if (context == null) { 43 | return null; 44 | } 45 | return context.getBean(beanName); 46 | } 47 | 48 | /** 49 | * 实现ApplicationContextAware接口的context注入函数, 将其存入静态变量. 50 | */ 51 | @Override 52 | public void setApplicationContext(ApplicationContext context) { 53 | if (SpringContextUtil.context == null) { 54 | SpringContextUtil.context = context; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lisa-delay-queue-manager-demo/src/test/java/org/lisa/delayqueue/manager/SimpleTest.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.manager; 2 | 3 | import org.lisa.delayqueue.manager.service.DemoService; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.data.redis.core.StringRedisTemplate; 9 | import org.springframework.test.context.junit4.SpringRunner; 10 | 11 | import javax.annotation.Resource; 12 | import java.util.Set; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | /** 16 | * @description: 17 | * @author: wuxu 18 | * @createDate: 2022/10/3 19 | */ 20 | @SpringBootTest 21 | @RunWith(SpringRunner.class) 22 | @Slf4j 23 | public class SimpleTest { 24 | 25 | @Resource 26 | private StringRedisTemplate stringRedisTemplate; 27 | 28 | @Resource 29 | private DemoService demoService; 30 | 31 | @Test 32 | public void testGet(){ 33 | demoService.sayHello(); 34 | } 35 | 36 | @Test 37 | public void testAdd2ZSet() throws InterruptedException { 38 | for(int i = 0; i < 10; i++){ 39 | stringRedisTemplate.opsForZSet().add("demo-key", "value_" + i, System.currentTimeMillis()); 40 | TimeUnit.SECONDS.sleep(1); 41 | } 42 | Set sets = stringRedisTemplate.opsForZSet().reverseRangeByScore("demo-key", 0, System.currentTimeMillis()); 43 | log.info("sets -> {}", sets); 44 | } 45 | 46 | @Test 47 | public void addAndDelZSet(){ 48 | stringRedisTemplate.opsForZSet().add("addAndDelZSet", "value1", -1); 49 | } 50 | 51 | @Test 52 | public void replaceTest(){ 53 | String url = "http://www.163.com"; 54 | url = url.replace("163", "h"); 55 | log.info("url -> {}", url); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lisa-delay-queue-consumer-demo/src/main/java/org/lisa/delayqueue/consumer/service/DemoService.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.consumer.service; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.alibaba.fastjson.TypeReference; 5 | import org.lisa.delayqueue.base.entity.Message; 6 | import org.lisa.delayqueue.producer.dto.OrderInfo; 7 | import org.lisa.delayqueue.producer.dto.User; 8 | import org.lisa.delayqueue.consumer.listener.MessageEvent; 9 | import lombok.SneakyThrows; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.context.ApplicationListener; 12 | import org.springframework.stereotype.Service; 13 | 14 | /** 15 | * @description: 16 | * @author: wuxu 17 | * @createDate: 2022/9/25 18 | */ 19 | @Slf4j 20 | @Service 21 | public class DemoService implements ApplicationListener { 22 | 23 | @SneakyThrows 24 | @Override 25 | public void onApplicationEvent(MessageEvent event) { 26 | log.info("[DemoService#onApplicationEvent], event -> {}", event); 27 | String topic = event.getTopic(); 28 | Object source = event.getSource(); 29 | log.info("topic:{}, source -> {}", topic, source); 30 | // 根据不同的topic处理不同的业务逻辑 31 | Message message = JSONObject.parseObject(String.valueOf(source), new TypeReference>(){ 32 | 33 | }); 34 | log.info("message -> {}", message); 35 | Class clazz = message.getClazz(); 36 | if(User.class.equals(clazz)){ 37 | User user = JSONObject.parseObject(message.getBody(), User.class); 38 | log.info("user -> {}", user); 39 | } 40 | if(OrderInfo.class.equals(clazz)){ 41 | OrderInfo orderInfo = JSONObject.parseObject(message.getBody(), OrderInfo.class); 42 | log.info("orderInfo -> {}", orderInfo); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lisa-delay-queue-producer-spring-boot-autoconfigure/src/main/java/org/lisa/delayqueue/producer/util/PublishMessageUtil.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.producer.util; 2 | 3 | import org.lisa.delayqueue.base.entity.Message; 4 | import org.lisa.delayqueue.producer.service.Producer; 5 | 6 | import java.time.LocalDateTime; 7 | import java.time.ZoneOffset; 8 | 9 | /** 10 | * @description: 发送消息工具类 11 | * @author: wuxu 12 | * @createDate: 2022/10/1 13 | */ 14 | public class PublishMessageUtil { 15 | 16 | /** 发送消息(相当于立刻发送) 17 | * @param topic Message Topic 18 | * @param msg Message Body 19 | * @param 消息体中的对象类型 20 | */ 21 | public static void sendMessage(String topic, Message msg) { 22 | sendMessage(topic, msg, System.currentTimeMillis()); 23 | } 24 | 25 | /** 26 | * 在指定时间发送消息 27 | * @param topic Message Topic 28 | * @param msg Message Body 29 | * @param expectAt 期望发送时间(毫秒数时间戳,默认当前系统时区) 30 | * @param 消息体中的对象类型 31 | */ 32 | public static void sendMessage(String topic, Message msg, long expectAt) { 33 | Producer.STREAM_MAP.get(topic).send(msg); 34 | } 35 | 36 | /** 37 | * 在指定时间发送消息 38 | * @param topic Message Topic 39 | * @param msg Message Body 40 | * @param expectAt 期望发送时间(默认当前系统时区) 41 | * @param 消息体中的对象类型 42 | */ 43 | public static void sendMessage(String topic, Message msg, LocalDateTime expectAt) { 44 | sendMessage(topic, msg, expectAt.toInstant(ZoneOffset.ofHours(8)).toEpochMilli()); 45 | } 46 | 47 | /** 48 | * 在指定时间发送消息 49 | * @param topic Message Topic 50 | * @param msg Message Body 51 | * @param expectAt 期望发送时间 52 | * @param zoneOffset 时区 53 | * @param 消息体中的对象类型 54 | */ 55 | public static void sendMessage(String topic, Message msg, LocalDateTime expectAt, ZoneOffset zoneOffset) { 56 | sendMessage(topic, msg, expectAt.toInstant(zoneOffset).toEpochMilli()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lisa-delay-queue-manager-spring-boot-autoconfigure/src/main/java/org/lisa/delayqueue/manager/scheduled/CleanStreamJob.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.manager.scheduled; 2 | 3 | import org.lisa.delayqueue.base.config.DelayQueueConfigProperties; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.data.redis.connection.stream.PendingMessagesSummary; 6 | import org.springframework.data.redis.core.StringRedisTemplate; 7 | import org.springframework.scheduling.annotation.Scheduled; 8 | 9 | import javax.annotation.Resource; 10 | 11 | import static org.lisa.delayqueue.base.constant.Constant.SERVER_NAME_MANAGER; 12 | import static org.lisa.delayqueue.base.constant.Constant.STREAM_READY_QUEUE; 13 | 14 | /** 15 | * @description: 定期清理消息,释放内存空间 16 | * @author: wuxu 17 | * @createDate: 2022/10/10 18 | */ 19 | @Slf4j 20 | public class CleanStreamJob { 21 | 22 | @Resource 23 | private StringRedisTemplate stringRedisTemplate; 24 | 25 | @Resource 26 | private DelayQueueConfigProperties delayQueueConfigProperties; 27 | 28 | // @Scheduled(cron = "0 0 3 * * ?") 29 | @Scheduled(cron = "${lisa-delay-queue.manager-server.crontab-clean-stream}") 30 | public void execute(){ 31 | log.info("[{}] CleanStreamJob", SERVER_NAME_MANAGER); 32 | delayQueueConfigProperties.getGroups() 33 | .stream() 34 | .forEach(delayQueueConfig -> { 35 | String streamKey = STREAM_READY_QUEUE + delayQueueConfig.getTopic(); 36 | // 获取group中的pending消息信息,本质上就是执行XPENDING指令 37 | PendingMessagesSummary pendingMessagesSummary = stringRedisTemplate.opsForStream().pending(streamKey, delayQueueConfig.getGroup()); 38 | // 所有pending消息的数量 39 | long totalPendingMessages = pendingMessagesSummary.getTotalPendingMessages(); 40 | log.info("[{}] totalPendingMessages:{}", SERVER_NAME_MANAGER, totalPendingMessages); 41 | stringRedisTemplate.opsForStream().trim(streamKey, totalPendingMessages + delayQueueConfig.getMaxLength()); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lisa-delay-queue-manager-spring-boot-autoconfigure/src/main/resources/script/lua/pending_msg_to_retry_queue.lua: -------------------------------------------------------------------------------- 1 | local streamKey = KEYS[1] 2 | local retryQueueKey = KEYS[2] 3 | local retryCountKey = KEYS[3] 4 | local garbageKey = KEYS[4] 5 | local group = ARGV[1] 6 | local rangeStart = ARGV[2] 7 | local rangeEnd = ARGV[3] 8 | local count = ARGV[4] 9 | local timeout = ARGV[5] 10 | local delayTime = ARGV[6] 11 | local maxRetryCount = ARGV[7] 12 | 13 | -- 命令举例:xpending stream:ready_queue:mystream group-1 - + 20 14 | local pendingMessages = redis.call('xpending', streamKey, group, rangeStart, rangeEnd, count) 15 | if (#(pendingMessages) > 0) then 16 | for i = 1,#pendingMessages,1 do 17 | local tables = pendingMessages[i] 18 | if(tables[3] > tonumber(timeout)) then 19 | local recordId = tables[1] 20 | -- 从stream中读取该消息, xrange stream:ready_queue:mystream 1665485172129-3 1665485172129-3 21 | local data = redis.call('xrange', streamKey, recordId, recordId) 22 | local msg = data[1][2][2] 23 | local position = string.find(msg, '|') 24 | local msgId = string.sub(msg, 1, position - 1) 25 | local score = string.sub(msg, position + 1) 26 | -- 从stream中移除该消息 27 | redis.call('xdel', streamKey, recordId) 28 | 29 | -- 从stream中ack掉该消息 30 | redis.call('xack', streamKey, group, recordId) 31 | 32 | -- 查询该消息的重试次数 33 | local retryCount = redis.call('hget', retryCountKey, msgId) 34 | if not(retryCount) then 35 | retryCount = maxRetryCount 36 | redis.call('hset', retryCountKey, msgId, retryCount) 37 | end 38 | 39 | if tonumber(retryCount) > 0 then 40 | -- 将该消息加入到retry queue(延迟delayTime毫秒后执行) 41 | local newScore = tonumber(score) + tonumber(delayTime) 42 | redis.call('zadd', retryQueueKey, newScore, msgId) 43 | redis.call('hincrby', retryCountKey, msgId, -1) 44 | else 45 | -- 超过最大重试次数,从hash中删掉,加入到garbageKeys中 46 | redis.call('hdel', retryCountKey, msgId) 47 | redis.call('sadd', garbageKey, msgId) 48 | end 49 | end 50 | end 51 | end -------------------------------------------------------------------------------- /lisa-delay-queue-producer-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | 8 | lisa-delay-queue 9 | org.lisa.stream 10 | 1.0.0-SNAPSHOT 11 | 12 | 13 | lisa-delay-queue-producer-demo 14 | 1.0.0 15 | 16 | lisa-delay-queue-producer-demo 17 | 18 | 19 | UTF-8 20 | 1.8 21 | 1.8 22 | 23 | 24 | 25 | 26 | org.lisa.stream 27 | lisa-delay-queue-producer-spring-boot-starter 28 | 1.0.0 29 | 30 | 31 | 32 | 33 | lisa-delay-queue-producer-demo 34 | 35 | 36 | src/main/resources 37 | true 38 | 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-maven-plugin 44 | 45 | 46 | org.lisa.delayqueue.producer.ProducerApplication 47 | ${project.parent.basedir}/target 48 | 49 | 50 | 51 | 52 | 53 | 54 | repackage 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /lisa-delay-queue-manager-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | 8 | lisa-delay-queue 9 | org.lisa.stream 10 | 1.0.0-SNAPSHOT 11 | 12 | 13 | lisa-delay-queue-manager-demo 14 | 1.0.0-SNAPSHOT 15 | 16 | lisa-delay-queue-manager-demo 17 | 18 | 19 | UTF-8 20 | 1.8 21 | 1.8 22 | 23 | 24 | 25 | 26 | org.lisa.stream 27 | lisa-delay-queue-manager-spring-boot-starter 28 | 1.0.0 29 | 30 | 31 | 32 | 33 | lisa-delay-queue-manager-demo 34 | 35 | 36 | src/main/resources 37 | true 38 | 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-maven-plugin 44 | 45 | 46 | org.lisa.delayqueue.manager.ManagerApplication 47 | ${project.parent.basedir}/target 48 | 49 | 50 | 51 | 52 | 53 | 54 | repackage 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /lisa-delay-queue-consumer-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | 8 | lisa-delay-queue 9 | org.lisa.stream 10 | 1.0.0-SNAPSHOT 11 | 12 | 13 | lisa-delay-queue-consumer-demo 14 | 1.0.0-SNAPSHOT 15 | 16 | lisa-delay-queue-consumer-demo 17 | 18 | 19 | UTF-8 20 | 1.8 21 | 1.8 22 | 23 | 24 | 25 | 26 | org.lisa.stream 27 | lisa-delay-queue-consumer-spring-boot-starter 28 | 1.0.0 29 | 30 | 31 | 32 | 33 | lisa-delay-queue-consumer-demo 34 | 35 | 36 | src/main/resources 37 | true 38 | 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-maven-plugin 44 | 45 | 46 | org.lisa.delayqueue.consumer.ConsumerApplication 47 | ${project.parent.basedir}/target 48 | 49 | 50 | 51 | 52 | 53 | 54 | repackage 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /lisa-delay-queue-consumer-demo/src/test/java/org/lisa/delayqueue/comsumer/SimpleTest.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.comsumer; 2 | 3 | import org.lisa.delayqueue.consumer.ConsumerApplication; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.lisa.delayqueue.consumer.config.DelayQueueConsumerServerConfig; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.test.context.junit4.SpringRunner; 10 | 11 | import javax.annotation.Resource; 12 | import java.time.Instant; 13 | import java.time.LocalDateTime; 14 | import java.time.ZoneId; 15 | import java.time.ZoneOffset; 16 | import java.util.Date; 17 | 18 | /** 19 | * @description: 20 | * @author: wuxu 21 | * @createDate: 2022/10/15 22 | */ 23 | @SpringBootTest(classes = ConsumerApplication.class) 24 | @RunWith(SpringRunner.class) 25 | @Slf4j 26 | public class SimpleTest { 27 | 28 | @Resource 29 | private DelayQueueConsumerServerConfig delayQueueConsumerServerConfig; 30 | 31 | @Test 32 | public void testDateTransfer() { 33 | long now = System.currentTimeMillis(); 34 | LocalDateTime ldt1a = new Date(now).toInstant().atOffset(ZoneOffset.of("+8")).toLocalDateTime(); 35 | log.info("LocalDateTime_1a -> {}", ldt1a); 36 | 37 | LocalDateTime ldt1b = new Date(now).toInstant().atZone(ZoneOffset.of("+8")).toLocalDateTime(); 38 | log.info("LocalDateTime_1b -> {}", ldt1b); 39 | 40 | LocalDateTime ldt2 = LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.of("+8")); 41 | log.info("LocalDateTime_2 -> {}", ldt2); 42 | 43 | LocalDateTime ldt3 = LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.ofHours(8)); 44 | log.info("LocalDateTime_3 -> {}", ldt3); 45 | 46 | LocalDateTime ldt4 = LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.ofHours(0)); 47 | log.info("LocalDateTime_4 -> {}", ldt4); 48 | 49 | LocalDateTime ldt5 = LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneId.systemDefault()); 50 | log.info("LocalDateTime_5 -> {}", ldt5); 51 | } 52 | 53 | @Test 54 | public void testConfig(){ 55 | log.info("Default zoneId -> {}", ZoneId.systemDefault()); 56 | log.info("pollTimeoutMillis -> {}", delayQueueConsumerServerConfig.getPollTimeoutMillis()); 57 | log.info("pollBatchSize -> {}", delayQueueConsumerServerConfig.getPollBatchSize()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lisa-delay-queue-producer-demo/src/main/java/org/lisa/delayqueue/producer/service/DemoService.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.producer.service; 2 | 3 | import org.lisa.delayqueue.base.entity.Message; 4 | import org.lisa.delayqueue.base.util.SpringContextUtil; 5 | import org.lisa.delayqueue.producer.dto.OrderInfo; 6 | import org.lisa.delayqueue.producer.dto.User; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.lisa.delayqueue.producer.util.PublishMessageUtil; 9 | import org.springframework.stereotype.Service; 10 | 11 | import javax.annotation.Resource; 12 | import java.time.*; 13 | import java.util.Random; 14 | 15 | import static org.lisa.delayqueue.base.constant.Constant.BEAN_NAME_SUFFIX_MESSAGE_PRODUCER; 16 | 17 | /** 18 | * @description: 19 | * @author: wuxu 20 | * @createDate: 2022/9/24 21 | */ 22 | @Slf4j 23 | @Service 24 | public class DemoService { 25 | 26 | @Resource(name = "mystream" + BEAN_NAME_SUFFIX_MESSAGE_PRODUCER) 27 | private Producer producer; 28 | 29 | // @Resource(name = "mystream2_Producer") 30 | // private Producer producer2; 31 | 32 | public void test(String msg) { 33 | // 创建消息记录, 以及指定stream 34 | producer.send(new Message<>(msg)); 35 | // producer2.send(new Message<>(msg)); 36 | 37 | User user = new User(); 38 | long now = System.currentTimeMillis(); 39 | long userId = now; 40 | user.setId(userId); 41 | user.setName("solid"); 42 | producer.send(new Message<>(user)); 43 | 44 | OrderInfo orderInfo = new OrderInfo(); 45 | orderInfo.setUserId(userId); 46 | orderInfo.setOrderNo(new Random().nextLong()); 47 | orderInfo.setCreateTime(LocalDateTime.now()); 48 | producer.send(new Message<>(orderInfo)); 49 | // producer2.send(new Message<>(orderInfo)); 50 | 51 | String topic = "mystream"; 52 | PublishMessageUtil.sendMessage(topic, new Message<>(user)); 53 | 54 | Producer producer3 = SpringContextUtil.getBean(topic + BEAN_NAME_SUFFIX_MESSAGE_PRODUCER, Producer.class); 55 | user.setName("producer3"); 56 | producer3.send(new Message<>(user)); 57 | LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneId.systemDefault()); 58 | LocalDateTime after10Seconds = localDateTime.plusSeconds(10); 59 | log.info("localDateTime -> {}, after10Seconds -> {}", localDateTime, after10Seconds); 60 | user.setId(after10Seconds.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); 61 | user.setName("after10Seconds"); 62 | producer3.send(new Message<>(user), after10Seconds); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lisa-delay-queue-producer-and-manager-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | 8 | lisa-delay-queue 9 | org.lisa.stream 10 | 1.0.0-SNAPSHOT 11 | 12 | 13 | lisa-delay-queue-producer-and-manager-demo 14 | 1.0.0 15 | 16 | lisa-delay-queue-producer-and-manager-demo 17 | 18 | 19 | UTF-8 20 | 1.8 21 | 1.8 22 | 23 | 24 | 25 | 26 | org.lisa.stream 27 | lisa-delay-queue-producer-spring-boot-starter 28 | 1.0.0 29 | 30 | 31 | org.lisa.stream 32 | lisa-delay-queue-manager-spring-boot-starter 33 | 1.0.0 34 | 35 | 36 | 37 | 38 | lisa-delay-queue-producer-and-manager-demo 39 | 40 | 41 | src/main/resources 42 | true 43 | 44 | 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-maven-plugin 49 | 50 | 51 | org.lisa.delayqueue.producer.ProducerApplication 52 | ${project.parent.basedir}/target 53 | 54 | 55 | 56 | 57 | 58 | 59 | repackage 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /lisa-delay-queue-producer-and-manager-demo/src/main/java/org/lisa/delayqueue/producer_and_manager/service/DemoService.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.producer_and_manager.service; 2 | 3 | import org.lisa.delayqueue.base.entity.Message; 4 | import org.lisa.delayqueue.base.util.SpringContextUtil; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.lisa.delayqueue.producer.service.Producer; 7 | import org.lisa.delayqueue.producer.util.PublishMessageUtil; 8 | import org.lisa.delayqueue.producer_and_manager.dto.OrderInfo; 9 | import org.lisa.delayqueue.producer_and_manager.dto.User; 10 | import org.springframework.stereotype.Service; 11 | 12 | import javax.annotation.Resource; 13 | import java.time.*; 14 | import java.util.Random; 15 | 16 | import static org.lisa.delayqueue.base.constant.Constant.BEAN_NAME_SUFFIX_MESSAGE_PRODUCER; 17 | 18 | /** 19 | * @description: 20 | * @author: wuxu 21 | * @createDate: 2022/9/24 22 | */ 23 | @Slf4j 24 | @Service 25 | public class DemoService { 26 | 27 | @Resource(name = "mystream" + BEAN_NAME_SUFFIX_MESSAGE_PRODUCER) 28 | private Producer producer; 29 | 30 | // @Resource(name = "mystream2_Producer") 31 | // private Producer producer2; 32 | 33 | public void test(String msg) { 34 | // 创建消息记录, 以及指定stream 35 | producer.send(new Message<>(msg)); 36 | // producer2.send(new Message<>(msg)); 37 | 38 | User user = new User(); 39 | long now = System.currentTimeMillis(); 40 | long userId = now; 41 | user.setId(userId); 42 | user.setName("solid"); 43 | producer.send(new Message<>(user)); 44 | 45 | OrderInfo orderInfo = new OrderInfo(); 46 | orderInfo.setUserId(userId); 47 | orderInfo.setOrderNo(new Random().nextLong()); 48 | orderInfo.setCreateTime(LocalDateTime.now()); 49 | producer.send(new Message<>(orderInfo)); 50 | // producer2.send(new Message<>(orderInfo)); 51 | 52 | String topic = "mystream"; 53 | PublishMessageUtil.sendMessage(topic, new Message<>(user)); 54 | 55 | Producer producer3 = SpringContextUtil.getBean(topic + BEAN_NAME_SUFFIX_MESSAGE_PRODUCER, Producer.class); 56 | user.setName("producer3"); 57 | producer3.send(new Message<>(user)); 58 | LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneId.systemDefault()); 59 | LocalDateTime after10Seconds = localDateTime.plusSeconds(10); 60 | log.info("localDateTime -> {}, after10Seconds -> {}", localDateTime, after10Seconds); 61 | user.setId(after10Seconds.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); 62 | user.setName("after10Seconds"); 63 | producer3.send(new Message<>(user), after10Seconds); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lisa-delay-queue-base/src/main/java/org/lisa/delayqueue/base/util/RedisScriptUtils.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.base.util; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.core.io.ClassPathResource; 5 | import org.springframework.data.redis.core.StringRedisTemplate; 6 | import org.springframework.data.redis.core.script.DefaultRedisScript; 7 | import org.springframework.data.redis.core.script.RedisScript; 8 | import org.springframework.scripting.support.ResourceScriptSource; 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * @description: 14 | * @author: wuxu 15 | * @createDate: 2022/10/8 16 | */ 17 | @Slf4j 18 | public class RedisScriptUtils { 19 | 20 | public static DefaultRedisScript getRedisScript(String resourceName, String luaName) { 21 | DefaultRedisScript redisScript = new DefaultRedisScript<>(); 22 | //resource目录下的scripts文件下的.lua文件 23 | redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(resourceName))); 24 | redisScript.setResultType(String.class); 25 | loadRedisScript(redisScript, luaName); 26 | return redisScript; 27 | } 28 | 29 | public static DefaultRedisScript getRedisScript(String resourceName, String luaName, Class clazz) { 30 | DefaultRedisScript redisScript = new DefaultRedisScript<>(); 31 | //resource目录下的scripts文件下的.lua文件 32 | redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(resourceName))); 33 | redisScript.setResultType(clazz); 34 | loadRedisScript(redisScript, luaName); 35 | return redisScript; 36 | } 37 | 38 | /** 39 | * 加载lua脚本到redis服务器 40 | * 41 | * @param redisScript 42 | * @param luaName 43 | */ 44 | public static void loadRedisScript(DefaultRedisScript redisScript, String luaName) { 45 | try { 46 | String sha1 = redisScript.getSha1(); 47 | log.debug("luaName -> {}, script sha1 -> {}", luaName, sha1); 48 | List results = SpringContextUtil.getBean(StringRedisTemplate.class).getConnectionFactory().getConnection().scriptExists(sha1); 49 | if (Boolean.FALSE.equals(results.get(0))) { 50 | String sha = SpringContextUtil.getBean(StringRedisTemplate.class).getConnectionFactory().getConnection().scriptLoad(scriptBytes(redisScript)); 51 | log.info("预加载lua脚本成功:{}, sha -> {}", luaName, sha); 52 | } 53 | } catch (Exception e) { 54 | log.error("预加载lua脚本异常:{}", luaName, e); 55 | } 56 | } 57 | 58 | /** 59 | * 序列化lua脚本 60 | * 61 | * @param script 62 | * @return 63 | */ 64 | public static byte[] scriptBytes(RedisScript script) { 65 | return SpringContextUtil.getBean(StringRedisTemplate.class).getStringSerializer().serialize(script.getScriptAsString()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lisa-delay-queue-manager-spring-boot-autoconfigure/src/main/java/org/lisa/delayqueue/manager/scheduled/ProcessPendingMessageJob.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.manager.scheduled; 2 | 3 | import org.lisa.delayqueue.base.config.DelayQueueConfigProperties; 4 | import com.google.common.collect.Lists; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.lisa.delayqueue.manager.config.ProcessPendingMessageConfig; 7 | import org.springframework.data.redis.core.StringRedisTemplate; 8 | import org.springframework.scheduling.annotation.Scheduled; 9 | 10 | import javax.annotation.Resource; 11 | 12 | import static org.lisa.delayqueue.base.constant.Constant.*; 13 | import static org.lisa.delayqueue.base.util.RedisScriptUtils.getRedisScript; 14 | 15 | /** 16 | * @description: 扫描并处理pending的消息 17 | * @author: wuxu 18 | * @createDate: 2022/10/10 19 | */ 20 | @Slf4j 21 | public class ProcessPendingMessageJob { 22 | 23 | @Resource 24 | private StringRedisTemplate stringRedisTemplate; 25 | 26 | @Resource 27 | private DelayQueueConfigProperties delayQueueConfigProperties; 28 | 29 | @Resource 30 | private ProcessPendingMessageConfig processPendingMessageConfig; 31 | 32 | public static final String RESOURCE_NAME = "script/lua/pending_msg_to_retry_queue.lua"; 33 | public static final String LUA_NAME = "pending_msg_to_retry_queue.lua"; 34 | 35 | 36 | // @Scheduled(cron = "0/5 * * * * ?") 37 | @Scheduled(cron = "${lisa-delay-queue.manager-server.crontab-process-pending-message}") 38 | public void execute() { 39 | log.info("[{}] 定时扫描阻塞的消息。processPendingMessageConfiguration -> {}", SERVER_NAME_MANAGER, processPendingMessageConfig); 40 | delayQueueConfigProperties 41 | .getGroups() 42 | .forEach(this::processPendingMessage); 43 | } 44 | 45 | private void processPendingMessage(DelayQueueConfigProperties.DelayQueueConfig delayQueueConfig){ 46 | String streamKey = STREAM_READY_QUEUE + delayQueueConfig.getTopic(); 47 | String retryQueueKey = ZSET_RETRY_QUEUE + delayQueueConfig.getTopic(); 48 | String retryCountKey = HASH_RETRY_COUNT + delayQueueConfig.getTopic(); 49 | String garbageKey = SET_GARBAGE_KEY + delayQueueConfig.getTopic(); 50 | 51 | stringRedisTemplate.execute(getRedisScript(RESOURCE_NAME, LUA_NAME, String.class), 52 | Lists.newArrayList(streamKey, retryQueueKey, retryCountKey, garbageKey), 53 | delayQueueConfig.getGroup(), 54 | processPendingMessageConfig.getRangeStart(), 55 | processPendingMessageConfig.getRangeEnd(), 56 | String.valueOf(processPendingMessageConfig.getCount()), 57 | String.valueOf(processPendingMessageConfig.getTimeout()), 58 | String.valueOf(processPendingMessageConfig.getDelayTime()), 59 | String.valueOf(processPendingMessageConfig.getMaxRetryCount()) 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lisa-delay-queue-consumer-spring-boot-autoconfigure/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | lisa-delay-queue 11 | org.lisa.stream 12 | 1.0.0-SNAPSHOT 13 | 14 | 4.0.0 15 | 16 | lisa-delay-queue-consumer-spring-boot-autoconfigure 17 | 1.0.0-SNAPSHOT 18 | lisa-delay-queue-consumer-spring-boot-autoconfigure 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-web 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-autoconfigure 34 | 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-configuration-processor 39 | true 40 | 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-cache 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-data-redis 50 | 51 | 52 | 53 | org.apache.commons 54 | commons-pool2 55 | 56 | 57 | 58 | org.lisa.stream 59 | lisa-delay-queue-base 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | org.apache.maven.plugins 68 | maven-compiler-plugin 69 | 70 | 1.8 71 | 1.8 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /lisa-delay-queue-manager-spring-boot-autoconfigure/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | lisa-delay-queue 11 | org.lisa.stream 12 | 1.0.0-SNAPSHOT 13 | 14 | 4.0.0 15 | 16 | lisa-delay-queue-manager-spring-boot-autoconfigure 17 | 1.0.0-SNAPSHOT 18 | lisa-delay-queue-manager-spring-boot-autoconfigure 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-web 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-autoconfigure 34 | 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-configuration-processor 39 | true 40 | 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-cache 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-data-redis 50 | 51 | 52 | 53 | org.apache.commons 54 | commons-pool2 55 | 56 | 57 | 58 | org.lisa.stream 59 | lisa-delay-queue-base 60 | 61 | 62 | 63 | com.alibaba 64 | fastjson 65 | 1.2.75 66 | 67 | 68 | 69 | com.google.guava 70 | guava 71 | 31.1-jre 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | org.apache.maven.plugins 80 | maven-compiler-plugin 81 | 82 | 1.8 83 | 1.8 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /lisa-delay-queue-producer-spring-boot-autoconfigure/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | lisa-delay-queue 11 | org.lisa.stream 12 | 1.0.0-SNAPSHOT 13 | 14 | 4.0.0 15 | 16 | lisa-delay-queue-producer-spring-boot-autoconfigure 17 | 1.0.0-SNAPSHOT 18 | lisa-delay-queue-producer-spring-boot-autoconfigure 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-web 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-autoconfigure 34 | 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-configuration-processor 39 | true 40 | 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-cache 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-data-redis 50 | 51 | 52 | 53 | org.apache.commons 54 | commons-pool2 55 | 56 | 57 | 58 | org.lisa.stream 59 | lisa-delay-queue-base 60 | 61 | 62 | 63 | com.alibaba 64 | fastjson 65 | 1.2.75 66 | 67 | 68 | 69 | com.google.guava 70 | guava 71 | 31.1-jre 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | org.apache.maven.plugins 80 | maven-compiler-plugin 81 | 82 | 1.8 83 | 1.8 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /lisa-delay-queue-manager-spring-boot-autoconfigure/src/main/java/org/lisa/delayqueue/manager/scheduled/MoveMessageToReadyQueueJob.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.manager.scheduled; 2 | 3 | import org.lisa.delayqueue.base.config.DelayQueueConfigProperties; 4 | import com.google.common.collect.Lists; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.data.redis.core.StringRedisTemplate; 7 | import org.springframework.scheduling.annotation.Scheduled; 8 | 9 | import javax.annotation.Resource; 10 | 11 | import java.util.List; 12 | 13 | import static org.lisa.delayqueue.base.constant.Constant.*; 14 | import static org.lisa.delayqueue.base.util.RedisScriptUtils.getRedisScript; 15 | 16 | /** 17 | * @description: 将消息从zset移动到stream定时任务 18 | * @author: wuxu 19 | * @createDate: 2022/10/8 20 | */ 21 | @Slf4j 22 | public class MoveMessageToReadyQueueJob { 23 | 24 | @Resource 25 | private StringRedisTemplate stringRedisTemplate; 26 | 27 | @Resource 28 | private DelayQueueConfigProperties delayQueueConfigProperties; 29 | 30 | public static final String RESOURCE_NAME = "script/lua/move_msg_to_ready_queue.lua"; 31 | public static final String LUA_NAME = "move_msg_to_ready_queue.lua"; 32 | 33 | /** 34 | * 每隔1秒钟,扫描一下有没有到时间的定时任务 35 | */ 36 | // @Scheduled(cron = "0/1 * * * * ?") 37 | @Scheduled(cron = "${lisa-delay-queue.manager-server.crontab-move-to-ready-queue}") 38 | public void execute() { 39 | long now = System.currentTimeMillis(); 40 | log.info("[{}] move message to ready queue. now -> {}", SERVER_NAME_MANAGER, now); 41 | delayQueueConfigProperties.getGroups().forEach(delayQueueConfig -> { 42 | moveMessageFromWaitingQueueToReadyQueue(delayQueueConfig, now); 43 | moveMessageFromRetryQueueToReadyQueue(delayQueueConfig, now); 44 | }); 45 | } 46 | 47 | private void moveMessageFromWaitingQueueToReadyQueue(DelayQueueConfigProperties.DelayQueueConfig delayQueueConfig, long now) { 48 | String waitingQueueKey = ZSET_WAITING_QUEUE + delayQueueConfig.getTopic(); 49 | String readyQueueKey = STREAM_READY_QUEUE + delayQueueConfig.getTopic(); 50 | long startScore = 0; 51 | List result = moveMessage(waitingQueueKey, readyQueueKey, startScore, now, delayQueueConfig.getMovingSize()); 52 | log.info("[{}] moveMessageFromWaitingQueueToReadyQueue result:{}", SERVER_NAME_MANAGER, result); 53 | } 54 | 55 | private void moveMessageFromRetryQueueToReadyQueue(DelayQueueConfigProperties.DelayQueueConfig delayQueueConfig, long now) { 56 | String retryQueueKey = ZSET_RETRY_QUEUE + delayQueueConfig.getTopic(); 57 | String readyQueueKey = STREAM_READY_QUEUE + delayQueueConfig.getTopic(); 58 | long startScore = 0; 59 | List result = moveMessage(retryQueueKey, readyQueueKey, startScore, now, delayQueueConfig.getMovingSize()); 60 | log.info("[{}] moveMessageFromRetryQueueToReadyQueue result:{}", SERVER_NAME_MANAGER, result); 61 | } 62 | 63 | private List moveMessage(String redisKeyZset, String readyQueueKey, long startScore, long endScore, int count) { 64 | log.info("[{}] processMessageQueue, redisKeyZset:{}, readyQueueKey:{}, startScore:{}, endScore:{}, count:{}", SERVER_NAME_MANAGER, redisKeyZset, readyQueueKey, startScore, endScore, count); 65 | try { 66 | return stringRedisTemplate.execute(getRedisScript(RESOURCE_NAME, LUA_NAME, List.class), Lists.newArrayList(redisKeyZset, readyQueueKey), String.valueOf(startScore), String.valueOf(endScore), String.valueOf(count)); 67 | } catch (Exception e) { 68 | return null; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lisa-delay-queue-producer-spring-boot-autoconfigure/src/main/java/org/lisa/delayqueue/producer/service/impl/ProducerImpl.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.producer.service.impl; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.google.common.collect.Lists; 5 | import lombok.Setter; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.lisa.delayqueue.base.config.DelayQueueConfigProperties; 8 | import org.lisa.delayqueue.base.entity.Message; 9 | import org.lisa.delayqueue.producer.service.Producer; 10 | import org.springframework.beans.factory.InitializingBean; 11 | import org.springframework.data.redis.core.StringRedisTemplate; 12 | 13 | import java.time.LocalDateTime; 14 | import java.time.ZoneId; 15 | import java.time.ZoneOffset; 16 | import java.util.UUID; 17 | 18 | import static org.lisa.delayqueue.base.constant.Constant.*; 19 | import static org.lisa.delayqueue.base.util.RedisScriptUtils.getRedisScript; 20 | 21 | /** 22 | * @description: 23 | * @author: wuxu 24 | * @createDate: 2022/9/24 25 | */ 26 | @Slf4j 27 | @Setter 28 | public class ProducerImpl implements Producer, InitializingBean { 29 | 30 | private DelayQueueConfigProperties.DelayQueueConfig delayQueueConfig; 31 | 32 | private StringRedisTemplate stringRedisTemplate; 33 | 34 | public static final String RESOURCE_NAME = "script/lua/push_msg.lua"; 35 | public static final String LUA_NAME = "push_msg.lua"; 36 | 37 | @Override 38 | public void send(Message msg) { 39 | LocalDateTime nowDateTime = LocalDateTime.now(); 40 | log.info("[{}] send msg. msg -> {}, nowDateTime -> {}", SERVER_NAME_PRODUCER, msg, nowDateTime); 41 | long now = nowDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); 42 | send(msg, now); 43 | } 44 | 45 | @Override 46 | public void send(Message msg, long expectAt) { 47 | String topic = delayQueueConfig.getTopic(); 48 | String waitingQueueKey = ZSET_WAITING_QUEUE + topic; 49 | String readyQueueKey = STREAM_READY_QUEUE + topic; 50 | String msgId = UUID.randomUUID().toString(); 51 | String score = String.valueOf(expectAt); 52 | String msgBodyKey = MESSAGE_BODY + topic + ":" + msgId; 53 | String msgBodyValue = JSONObject.toJSONString(msg); 54 | String now = String.valueOf(System.currentTimeMillis()); 55 | log.info("[{}] [开始]调用lua脚本添加延时消息。waitingQueueKey:{}, readyQueueKey:{}, msgId:{}, score:{}, msgBodyKey:{}, msgBodyValue:{}, now:{}", SERVER_NAME_PRODUCER, waitingQueueKey, readyQueueKey, msgId, score, msgBodyKey, msgBodyValue, now); 56 | Boolean result = stringRedisTemplate.execute(getRedisScript(RESOURCE_NAME, LUA_NAME, Boolean.class), Lists.newArrayList(waitingQueueKey, readyQueueKey), msgId, score, msgBodyKey, msgBodyValue, now); 57 | log.info("[{}] [结束]调用lua脚本添加延时消息。msgId:{}, result:{}", SERVER_NAME_PRODUCER, msgId, result); 58 | } 59 | 60 | @Override 61 | public void send(Message msg, LocalDateTime expectAt) { 62 | log.info("[{}] send msg with expectAt. msg -> {}, expectAt(LocalDateTime) -> {}", SERVER_NAME_PRODUCER, msg, expectAt); 63 | send(msg, expectAt.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); 64 | } 65 | 66 | @Override 67 | public void send(Message msg, LocalDateTime expectAt, ZoneOffset zoneOffset) { 68 | log.info("[{}] send msg with expectAt and zoneOffset. msg -> {}, expectAt(LocalDateTime) -> {}, zoneOffset -> {}", SERVER_NAME_PRODUCER, msg, expectAt, zoneOffset); 69 | send(msg, expectAt.toInstant(zoneOffset).toEpochMilli()); 70 | } 71 | 72 | @Override 73 | public void afterPropertiesSet() { 74 | String topic = delayQueueConfig.getTopic(); 75 | STREAM_MAP.put(topic, this); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lisa-delay-queue-producer-spring-boot-autoconfigure/src/main/java/org/lisa/delayqueue/producer/config/DelayQueueProducerAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.producer.config; 2 | 3 | import org.lisa.delayqueue.base.config.DelayQueueConfigProperties; 4 | import org.lisa.delayqueue.base.util.SpringContextUtil; 5 | import org.lisa.delayqueue.producer.service.impl.ProducerImpl; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.beans.BeansException; 8 | import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; 9 | import org.springframework.beans.factory.support.BeanDefinitionBuilder; 10 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 11 | import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; 12 | import org.springframework.beans.factory.support.DefaultListableBeanFactory; 13 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 14 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 15 | import org.springframework.boot.context.properties.bind.BindResult; 16 | import org.springframework.boot.context.properties.bind.Binder; 17 | import org.springframework.context.ApplicationContext; 18 | import org.springframework.context.ApplicationContextAware; 19 | import org.springframework.context.ConfigurableApplicationContext; 20 | import org.springframework.context.EnvironmentAware; 21 | import org.springframework.context.annotation.Configuration; 22 | import org.springframework.context.annotation.Import; 23 | import org.springframework.core.env.Environment; 24 | 25 | import javax.annotation.Resource; 26 | 27 | import static org.lisa.delayqueue.base.config.DelayQueueConfigProperties.LISA_DELAY_QUEUE_CONFIG_PREFIX; 28 | import static org.lisa.delayqueue.base.constant.Constant.BEAN_NAME_SUFFIX_MESSAGE_PRODUCER; 29 | 30 | /** 31 | * @description: 32 | * @author: wuxu 33 | * @createDate: 2022/9/24 34 | */ 35 | @ConditionalOnProperty(prefix = LISA_DELAY_QUEUE_CONFIG_PREFIX, name = "enabled", havingValue = "true") 36 | //检查依赖的lisa-delay-queue配置是否存在 37 | @Configuration 38 | @EnableConfigurationProperties(DelayQueueConfigProperties.class) 39 | @Import(SpringContextUtil.class) 40 | @Slf4j 41 | public class DelayQueueProducerAutoConfiguration implements EnvironmentAware, BeanDefinitionRegistryPostProcessor, ApplicationContextAware { 42 | 43 | private ApplicationContext applicationContext; 44 | 45 | @Resource 46 | private DelayQueueConfigProperties delayQueueConfigProperties; 47 | 48 | private static final String PROPERTY_NAME_DELAY_QUEUE_CONFIG = "delayQueueConfig"; 49 | 50 | private static final String PROPERTY_NAME_STRING_REDIS_TEMPLATE = "stringRedisTemplate"; 51 | 52 | @Override 53 | public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { 54 | delayQueueConfigProperties.getGroups() 55 | .forEach(delayQueueConfig -> { 56 | ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext; 57 | DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory(); 58 | BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(ProducerImpl.class); 59 | beanDefinitionBuilder.addPropertyValue(PROPERTY_NAME_DELAY_QUEUE_CONFIG, delayQueueConfig); 60 | beanDefinitionBuilder.addPropertyReference(PROPERTY_NAME_STRING_REDIS_TEMPLATE, PROPERTY_NAME_STRING_REDIS_TEMPLATE); 61 | defaultListableBeanFactory.registerBeanDefinition(delayQueueConfig.getTopic() + BEAN_NAME_SUFFIX_MESSAGE_PRODUCER, beanDefinitionBuilder.getRawBeanDefinition()); 62 | }); 63 | } 64 | 65 | @Override 66 | public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { 67 | 68 | } 69 | 70 | @Override 71 | public void setEnvironment(Environment environment) { 72 | BindResult bindResult = Binder.get(environment).bind(LISA_DELAY_QUEUE_CONFIG_PREFIX, DelayQueueConfigProperties.class); 73 | delayQueueConfigProperties = bindResult.get(); 74 | } 75 | 76 | @Override 77 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 78 | this.applicationContext = applicationContext; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 2.3.7.RELEASE 10 | 11 | 12 | 13 | 4.0.0 14 | org.lisa.stream 15 | lisa-delay-queue 16 | 1.0.0-SNAPSHOT 17 | 18 | lisa-delay-queue-producer-spring-boot-autoconfigure 19 | lisa-delay-queue-producer-spring-boot-starter 20 | lisa-delay-queue-producer-demo 21 | lisa-delay-queue-producer-and-manager-demo 22 | 23 | lisa-delay-queue-consumer-spring-boot-autoconfigure 24 | lisa-delay-queue-consumer-spring-boot-starter 25 | lisa-delay-queue-consumer-demo 26 | 27 | lisa-delay-queue-manager-spring-boot-autoconfigure 28 | lisa-delay-queue-manager-spring-boot-starter 29 | lisa-delay-queue-manager-demo 30 | 31 | lisa-delay-queue-base 32 | 33 | pom 34 | 35 | 36 | 37 | 38 | org.lisa.stream 39 | lisa-delay-queue-base 40 | 1.0.0-SNAPSHOT 41 | 42 | 43 | 44 | 45 | 46 | 47 | org.projectlombok 48 | lombok 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-test 53 | test 54 | 55 | 56 | com.alibaba 57 | fastjson 58 | 1.2.75 59 | 60 | 61 | com.google.guava 62 | guava 63 | 31.1-jre 64 | 65 | 66 | 67 | org.apache.commons 68 | commons-lang3 69 | 3.12.0 70 | 71 | 72 | 73 | 74 | 8 75 | 8 76 | UTF-8 77 | 3.8.1 78 | 2.22.2 79 | 80 | 81 | 82 | 83 | 84 | org.apache.maven.plugins 85 | maven-compiler-plugin 86 | ${maven.compiler.plugin.version} 87 | 88 | ${java.version} 89 | ${java.version} 90 | 91 | 92 | 93 | org.apache.maven.plugins 94 | maven-surefire-plugin 95 | ${maven.surefire.plugin.version} 96 | 97 | 98 | **/*Test.java 99 | 100 | 101 | **/TestCase.java 102 | 103 | false 104 | false 105 | true 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /lisa-delay-queue-consumer-spring-boot-autoconfigure/src/main/java/org/lisa/delayqueue/consumer/listener/DefaultStreamListener.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.consumer.listener; 2 | 3 | 4 | import org.lisa.delayqueue.base.config.DelayQueueConfigProperties; 5 | import com.google.common.collect.Lists; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.springframework.context.ApplicationEventPublisher; 9 | import org.springframework.context.ApplicationEventPublisherAware; 10 | import org.springframework.data.redis.connection.stream.ObjectRecord; 11 | import org.springframework.data.redis.connection.stream.RecordId; 12 | import org.springframework.data.redis.core.StringRedisTemplate; 13 | import org.springframework.data.redis.stream.StreamListener; 14 | 15 | import javax.annotation.Resource; 16 | 17 | import java.time.Instant; 18 | import java.time.LocalDateTime; 19 | import java.time.ZoneId; 20 | 21 | import static org.lisa.delayqueue.base.constant.Constant.*; 22 | import static org.lisa.delayqueue.base.util.RedisScriptUtils.getRedisScript; 23 | 24 | 25 | /** 26 | * @author solid 27 | */ 28 | @Slf4j 29 | public class DefaultStreamListener implements StreamListener>, ApplicationEventPublisherAware { 30 | 31 | @Resource 32 | private DelayQueueConfigProperties delayQueueConfigProperties; 33 | 34 | @Resource 35 | private StringRedisTemplate stringRedisTemplate; 36 | 37 | private ApplicationEventPublisher applicationEventPublisher; 38 | 39 | public static final String RESOURCE_NAME = "script/lua/ack_message.lua"; 40 | public static final String LUA_NAME = "ack_message.lua"; 41 | 42 | @Override 43 | public void onMessage(ObjectRecord message) { 44 | try { 45 | // 消息ID 46 | RecordId messageId = message.getId(); 47 | String value = message.getValue(); 48 | String stream = message.getStream(); 49 | // 通过stream,反推出topic 50 | String topic = StringUtils.replaceOnce(stream, STREAM_READY_QUEUE, ""); 51 | String group = delayQueueConfigProperties.getGroups() 52 | .stream() 53 | .filter(delayQueueConfig -> delayQueueConfig.getTopic().equals(topic)) 54 | .map(DelayQueueConfigProperties.DelayQueueConfig::getGroup) 55 | .findFirst() 56 | .get(); 57 | log.info("[{}] StreamMessageListener stream message。stream -> {}, messageId -> {}, topic -> {}, value -> {}", SERVER_NAME_CONSUMER, message.getStream(), messageId, topic, value); 58 | 59 | if(StringUtils.contains(value, INIT_MESSAGE)){ 60 | // 初始化的message,直接ack掉 61 | stringRedisTemplate.opsForStream().acknowledge(group, message); 62 | return; 63 | } 64 | // consumer消费的message包含msgId和预期执行时间 65 | String[] arrays = value.split("\\|"); 66 | String msgId = arrays[0]; 67 | long score = Long.parseLong(arrays[1]); 68 | long now = System.currentTimeMillis(); 69 | LocalDateTime scoreDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(score), ZoneId.systemDefault()); 70 | LocalDateTime nowDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneId.systemDefault()); 71 | log.info("[{}] msgId -> {}, score -> {}, now -> {}, scoreDateTime -> {}, nowDateTime -> {}", SERVER_NAME_CONSUMER, msgId, score, now, scoreDateTime, nowDateTime); 72 | 73 | // 从redis中再取出msgId对应的msgValue 74 | String msgBodyKey = MESSAGE_BODY + topic + ":" + msgId; 75 | String body = stringRedisTemplate.opsForValue().get(msgBodyKey); 76 | applicationEventPublisher.publishEvent(new MessageEvent(topic, body)); 77 | 78 | // 手动ACK 79 | ackMessage(topic, group, msgId, msgBodyKey, messageId); 80 | } catch (Exception e) { 81 | // 处理异常 82 | e.printStackTrace(); 83 | } 84 | } 85 | 86 | @Override 87 | public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { 88 | this.applicationEventPublisher = applicationEventPublisher; 89 | } 90 | 91 | private void ackMessage(String topic, String group, String msgId, String msgBodyKey, RecordId recordId){ 92 | log.info("[{}] Ack message start. topic -> {}, group -> {}, msgId -> {}, msgBodyKey -> {}, recordId -> {}", SERVER_NAME_CONSUMER, topic, group, msgId, msgBodyKey, recordId); 93 | String streamKey = STREAM_READY_QUEUE + topic; 94 | String retryCountKey = HASH_RETRY_COUNT + topic; 95 | String garbageKey = SET_GARBAGE_KEY + topic; 96 | Long count = stringRedisTemplate.execute(getRedisScript(RESOURCE_NAME, LUA_NAME, Long.class), Lists.newArrayList(streamKey, retryCountKey, garbageKey), group, msgId, msgBodyKey, recordId.getValue()); 97 | log.info("[{}] Ack message end. msgId -> {}, count -> {}", SERVER_NAME_CONSUMER, msgId, count); 98 | } 99 | } -------------------------------------------------------------------------------- /lisa-delay-queue-consumer-spring-boot-autoconfigure/src/main/java/org/lisa/delayqueue/consumer/config/DelayQueueConsumerAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.consumer.config; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 4 | import com.fasterxml.jackson.annotation.PropertyAccessor; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import org.lisa.delayqueue.base.config.DelayQueueConfigProperties; 7 | import org.lisa.delayqueue.base.util.SpringContextUtil; 8 | import org.lisa.delayqueue.consumer.listener.DefaultStreamListener; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 11 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Configuration; 14 | import org.springframework.context.annotation.Import; 15 | import org.springframework.data.redis.connection.RedisConnectionFactory; 16 | import org.springframework.data.redis.connection.stream.Consumer; 17 | import org.springframework.data.redis.connection.stream.ObjectRecord; 18 | import org.springframework.data.redis.connection.stream.ReadOffset; 19 | import org.springframework.data.redis.connection.stream.StreamOffset; 20 | import org.springframework.data.redis.core.RedisTemplate; 21 | import org.springframework.data.redis.core.StringRedisTemplate; 22 | import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; 23 | import org.springframework.data.redis.serializer.StringRedisSerializer; 24 | import org.springframework.data.redis.stream.StreamMessageListenerContainer; 25 | import org.springframework.scheduling.annotation.EnableScheduling; 26 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 27 | 28 | import javax.annotation.Resource; 29 | import java.time.Duration; 30 | import java.util.Map; 31 | import java.util.stream.Collectors; 32 | 33 | import static org.lisa.delayqueue.base.constant.Constant.SERVER_NAME_CONSUMER; 34 | import static org.lisa.delayqueue.base.constant.Constant.STREAM_READY_QUEUE; 35 | 36 | /** 37 | * @description: 38 | * @author: wuxu 39 | * @createDate: 2022/9/24 40 | */ 41 | @Slf4j 42 | @ConditionalOnProperty(prefix = DelayQueueConfigProperties.LISA_DELAY_QUEUE_CONFIG_PREFIX, name = "enabled", havingValue = "true") 43 | @EnableConfigurationProperties({DelayQueueConfigProperties.class, DelayQueueConsumerServerConfig.class}) 44 | @Configuration 45 | @EnableScheduling 46 | @Import(SpringContextUtil.class) 47 | public class DelayQueueConsumerAutoConfiguration { 48 | 49 | @Resource 50 | private ThreadPoolTaskExecutor threadPoolTaskExecutor; 51 | 52 | @Resource 53 | private DelayQueueConfigProperties delayQueueConfigProperties; 54 | 55 | @Resource 56 | private DelayQueueConsumerServerConfig delayQueueConsumerServerConfig; 57 | 58 | @Resource 59 | private StringRedisTemplate stringRedisTemplate; 60 | 61 | /** 62 | * 主要做的是将OrderStreamListener监听绑定消费者,用于接收消息 63 | * 64 | * @param connectionFactory 65 | * @param streamListener 66 | * @return 67 | */ 68 | @Bean 69 | public Map>> consumerListenerMap( 70 | RedisConnectionFactory connectionFactory, 71 | DefaultStreamListener streamListener) { 72 | log.info("[{}] delayQueueConsumerServerConfiguration -> {}", SERVER_NAME_CONSUMER, delayQueueConsumerServerConfig); 73 | return delayQueueConfigProperties.getGroups().stream() 74 | .collect( 75 | Collectors.toMap( 76 | DelayQueueConfigProperties.DelayQueueConfig::getTopic, 77 | streamConfig -> { 78 | StreamMessageListenerContainer> container = streamContainer(streamConfig, connectionFactory, streamListener); 79 | container.start(); 80 | return container; 81 | } 82 | ) 83 | ); 84 | } 85 | 86 | /** 87 | * @param delayQueueConfig redisStream流的配置项 88 | * @param connectionFactory 89 | * @param streamListener 绑定的监听类 90 | * @return 91 | */ 92 | private StreamMessageListenerContainer> streamContainer(DelayQueueConfigProperties.DelayQueueConfig delayQueueConfig, RedisConnectionFactory connectionFactory, org.springframework.data.redis.stream.StreamListener streamListener) { 93 | 94 | StreamMessageListenerContainer.StreamMessageListenerContainerOptions> options = 95 | StreamMessageListenerContainer.StreamMessageListenerContainerOptions 96 | .builder() 97 | .pollTimeout(Duration.ofMillis(delayQueueConsumerServerConfig.getPollTimeoutMillis())) // 拉取消息超时时间 98 | .batchSize(delayQueueConsumerServerConfig.getPollBatchSize()) // 批量抓取消息 99 | .targetType(String.class) // 传递的数据类型 100 | .executor(threadPoolTaskExecutor) 101 | .build(); 102 | StreamMessageListenerContainer> container = StreamMessageListenerContainer 103 | .create(connectionFactory, options); 104 | //指定消费最新的消息 105 | StreamOffset offset = StreamOffset.create(STREAM_READY_QUEUE + delayQueueConfig.getTopic(), ReadOffset.lastConsumed()); 106 | //创建消费者 107 | Consumer consumer = Consumer.from(delayQueueConfig.getGroup(), delayQueueConfig.getConsumer()); 108 | StreamMessageListenerContainer.StreamReadRequest streamReadRequest = StreamMessageListenerContainer.StreamReadRequest.builder(offset) 109 | .errorHandler((error) -> { 110 | }) 111 | .cancelOnError(e -> false) 112 | .consumer(consumer) 113 | //关闭自动ack确认 114 | .autoAcknowledge(false) 115 | .build(); 116 | //指定消费者对象 117 | container.register(streamReadRequest, streamListener); 118 | return container; 119 | } 120 | 121 | @Bean 122 | public RedisTemplate redisTemplate(RedisConnectionFactory factory) { 123 | RedisTemplate template = new RedisTemplate(); 124 | template.setConnectionFactory(factory); 125 | Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); 126 | ObjectMapper om = new ObjectMapper(); 127 | om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); 128 | om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); 129 | jackson2JsonRedisSerializer.setObjectMapper(om); 130 | StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); 131 | // key采用String的序列化方式 132 | template.setKeySerializer(stringRedisSerializer); 133 | // hash的key也采用String的序列化方式 134 | template.setHashKeySerializer(stringRedisSerializer); 135 | // value序列化方式采用jackson 136 | template.setValueSerializer(jackson2JsonRedisSerializer); 137 | // hash的value序列化方式采用jackson 138 | template.setHashValueSerializer(jackson2JsonRedisSerializer); 139 | template.afterPropertiesSet(); 140 | return template; 141 | } 142 | 143 | @Bean 144 | public DefaultStreamListener defaultStreamListener() { 145 | return new DefaultStreamListener(); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /lisa-delay-queue-manager-spring-boot-autoconfigure/src/main/java/org/lisa/delayqueue/manager/config/DelayQueueManagerAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.manager.config; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 5 | import com.fasterxml.jackson.annotation.PropertyAccessor; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import org.lisa.delayqueue.base.config.DelayQueueConfigProperties; 8 | import org.lisa.delayqueue.base.entity.Message; 9 | import org.lisa.delayqueue.base.util.SpringContextUtil; 10 | import org.lisa.delayqueue.manager.scheduled.CleanStreamJob; 11 | import org.lisa.delayqueue.manager.scheduled.MoveMessageToReadyQueueJob; 12 | import org.lisa.delayqueue.manager.scheduled.ProcessPendingMessageJob; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.beans.factory.InitializingBean; 15 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 16 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 17 | import org.springframework.context.annotation.Bean; 18 | import org.springframework.context.annotation.Configuration; 19 | import org.springframework.context.annotation.Import; 20 | import org.springframework.data.redis.connection.RedisConnectionFactory; 21 | import org.springframework.data.redis.connection.stream.StreamInfo; 22 | import org.springframework.data.redis.connection.stream.StreamRecords; 23 | import org.springframework.data.redis.connection.stream.StringRecord; 24 | import org.springframework.data.redis.core.RedisTemplate; 25 | import org.springframework.data.redis.core.StringRedisTemplate; 26 | import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; 27 | import org.springframework.data.redis.serializer.StringRedisSerializer; 28 | import org.springframework.scheduling.annotation.EnableScheduling; 29 | 30 | import javax.annotation.Resource; 31 | import java.util.Collections; 32 | 33 | import static org.lisa.delayqueue.base.constant.Constant.*; 34 | 35 | /** 36 | * @description: 37 | * @author: wuxu 38 | * @createDate: 2022/9/24 39 | */ 40 | @Slf4j 41 | @ConditionalOnProperty(prefix = DelayQueueConfigProperties.LISA_DELAY_QUEUE_CONFIG_PREFIX, name = "enabled", havingValue = "true") 42 | @EnableConfigurationProperties({DelayQueueConfigProperties.class, ProcessPendingMessageConfig.class}) 43 | @Configuration 44 | @Import(SpringContextUtil.class) 45 | @EnableScheduling 46 | public class DelayQueueManagerAutoConfiguration implements InitializingBean { 47 | 48 | @Resource 49 | private DelayQueueConfigProperties delayQueueConfigProperties; 50 | 51 | @Resource 52 | private StringRedisTemplate stringRedisTemplate; 53 | 54 | @Bean 55 | public RedisTemplate redisTemplate(RedisConnectionFactory factory) { 56 | RedisTemplate template = new RedisTemplate(); 57 | template.setConnectionFactory(factory); 58 | Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); 59 | ObjectMapper om = new ObjectMapper(); 60 | om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); 61 | om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); 62 | jackson2JsonRedisSerializer.setObjectMapper(om); 63 | StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); 64 | // key采用String的序列化方式 65 | template.setKeySerializer(stringRedisSerializer); 66 | // hash的key也采用String的序列化方式 67 | template.setHashKeySerializer(stringRedisSerializer); 68 | // value序列化方式采用jackson 69 | template.setValueSerializer(jackson2JsonRedisSerializer); 70 | // hash的value序列化方式采用jackson 71 | template.setHashValueSerializer(jackson2JsonRedisSerializer); 72 | template.afterPropertiesSet(); 73 | return template; 74 | } 75 | 76 | @Bean 77 | public MoveMessageToReadyQueueJob moveMessageToReadyQueueJob() { 78 | log.info("[{}] New MoveMessageToReadyQueueJob", SERVER_NAME_MANAGER); 79 | return new MoveMessageToReadyQueueJob(); 80 | } 81 | 82 | @Bean 83 | public CleanStreamJob cleanStreamJob() { 84 | log.info("[{}] New CleanStreamJob", SERVER_NAME_MANAGER); 85 | return new CleanStreamJob(); 86 | } 87 | 88 | @Bean 89 | public ProcessPendingMessageJob processPendingMessageJob() { 90 | log.info("[{}] New ProcessPendingMessageJob", SERVER_NAME_MANAGER); 91 | return new ProcessPendingMessageJob(); 92 | } 93 | 94 | @Override 95 | public void afterPropertiesSet() throws Exception { 96 | log.info("[{}] Init message queue data of topic", SERVER_NAME_MANAGER); 97 | delayQueueConfigProperties.getGroups() 98 | .forEach(delayQueueConfig -> { 99 | /** 100 | * 1. 初始化waiting queue (zset) 101 | * key = 前缀+topic 102 | * value = msgId 103 | * score = 消息触发时间戳 104 | */ 105 | initWaitingQueueZset(delayQueueConfig); 106 | 107 | /** 108 | * 2. 初始化ready queue (stream) 109 | * key = 前缀+topic 110 | * value = msgId|score 111 | */ 112 | initReadyQueueStream(delayQueueConfig); 113 | 114 | /** 115 | * 3. 初始化retry queue (zset) 116 | * key = 前缀+topic 117 | * value = msgId 118 | * score = 下次重试触发时间戳 119 | */ 120 | initRetryQueueZset(delayQueueConfig); 121 | 122 | /** 123 | * 4. 初始化retry count(hash) 124 | * key = 前缀+topic 125 | * field = msgId 126 | * value = 可以重试的次数 127 | */ 128 | initRetryCountHash(delayQueueConfig); 129 | 130 | /** 131 | * 5. 初始化garbage key(set) 132 | * key = 前缀+topic 133 | * value = msgId 134 | * score = 放入时间 135 | * 当超过重试次数一直无法消费掉的msgId,放入这里 136 | * 为了防止该垃圾回收膨胀,会定期清除一些数据(根据数量和时间清除) 137 | */ 138 | initGarbageKeySet(delayQueueConfig); 139 | }); 140 | 141 | } 142 | 143 | private void initWaitingQueueZset(DelayQueueConfigProperties.DelayQueueConfig delayQueueConfig) { 144 | initZset(ZSET_WAITING_QUEUE + delayQueueConfig.getTopic()); 145 | } 146 | 147 | private void initReadyQueueStream(DelayQueueConfigProperties.DelayQueueConfig delayQueueConfig) { 148 | String streamKey = STREAM_READY_QUEUE + delayQueueConfig.getTopic(); 149 | StringRecord stringRecord = StreamRecords.string(Collections.singletonMap("name", JSONObject.toJSONString(new Message<>(INIT_MESSAGE)))).withStreamKey(streamKey); 150 | stringRedisTemplate.opsForStream().add(stringRecord); 151 | StreamInfo.XInfoGroups xInfoGroups = stringRedisTemplate.opsForStream().groups(streamKey); 152 | if (xInfoGroups.isEmpty()) { 153 | stringRedisTemplate.opsForStream().createGroup(streamKey, delayQueueConfig.getGroup()); 154 | } 155 | } 156 | 157 | private void initRetryQueueZset(DelayQueueConfigProperties.DelayQueueConfig delayQueueConfig) { 158 | initZset(ZSET_RETRY_QUEUE + delayQueueConfig.getTopic()); 159 | } 160 | 161 | private void initRetryCountHash(DelayQueueConfigProperties.DelayQueueConfig delayQueueConfig) { 162 | // 无需初始化,使用时会自动初始化 163 | } 164 | 165 | private void initGarbageKeySet(DelayQueueConfigProperties.DelayQueueConfig delayQueueConfig) { 166 | // 无需初始化,使用时会自动初始化 167 | } 168 | 169 | private void initZset(String zsetKey){ 170 | stringRedisTemplate.opsForZSet().remove(zsetKey, INIT_MESSAGE); 171 | stringRedisTemplate.opsForZSet().add(zsetKey, INIT_MESSAGE, -1); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /lisa-delay-queue-producer-demo/src/test/java/org/lisa/delayqueue/producer/SimpleProducerTest.java: -------------------------------------------------------------------------------- 1 | package org.lisa.delayqueue.producer; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import org.lisa.delayqueue.base.util.RedisScriptUtils; 5 | import org.lisa.delayqueue.producer.dto.OrderInfo; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.assertj.core.util.Lists; 8 | import org.junit.Assert; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.springframework.boot.test.context.SpringBootTest; 12 | import org.springframework.data.redis.core.StringRedisTemplate; 13 | import org.springframework.data.redis.core.script.DefaultRedisScript; 14 | import org.springframework.test.context.junit4.SpringRunner; 15 | 16 | import javax.annotation.Resource; 17 | import java.util.List; 18 | import java.util.UUID; 19 | 20 | /** 21 | * @description: 22 | * @author: wuxu 23 | * @createDate: 2022/10/3 24 | */ 25 | @SpringBootTest 26 | @RunWith(SpringRunner.class) 27 | @Slf4j 28 | public class SimpleProducerTest { 29 | 30 | @Resource 31 | private StringRedisTemplate stringRedisTemplate; 32 | 33 | @Test 34 | public void testSetRedis() { 35 | String msgId = "msg:id:666888"; 36 | String msgValue = String.valueOf(System.currentTimeMillis()); 37 | stringRedisTemplate.opsForValue().set(msgId, msgValue); 38 | String valueFromRedis = stringRedisTemplate.opsForValue().get(msgId); 39 | log.info("valueFromRedis -> {}", valueFromRedis); 40 | } 41 | 42 | // @Test 43 | // public void testExecuteByConnection(){ 44 | // String resourceName = "script/lua/test_xpending.lua"; 45 | // String luaName = "xpending.lua"; 46 | // 47 | // DefaultRedisScript redisScript = getRedisScript(resourceName, luaName, List.class); 48 | // List list = Lists.newArrayList(); 49 | // list.toArray(new String[]); 50 | // Object result = stringRedisTemplate.getConnectionFactory().getConnection() 51 | // .evalSha(redisScript.getSha1(), ReturnType.VALUE, 1, "mystream", "group-1"); 52 | // log.info("result:{}", result); 53 | // } 54 | 55 | @Test 56 | public void testHGet() { 57 | String resourceName = "script/lua/test_hget.lua"; 58 | String luaName = "hget.lua"; 59 | String result = stringRedisTemplate.execute(getRedisScript(resourceName, luaName, String.class), Lists.newArrayList("test_hash"), "test_key_1"); 60 | log.info("Result from lua. result -> {}", result); 61 | } 62 | 63 | @Test 64 | public void testPending() { 65 | String resourceName = "script/lua/test_xpending.lua"; 66 | String luaName = "xpending.lua"; 67 | Object result = stringRedisTemplate.execute(getRedisScript(resourceName, luaName, List.class), Lists.newArrayList("mystream"), "group-1", "-", "+", "20"); 68 | log.info("Result from lua. result -> {}", result); 69 | } 70 | 71 | @Test 72 | public void testListResult(){ 73 | String resourceName = "script/lua/test_list_result.lua"; 74 | String luaName = "test_list_result.lua"; 75 | Object result = stringRedisTemplate.execute(getRedisScript(resourceName, luaName, List.class), Lists.newArrayList("mystream")); 76 | log.info("Result from lua. result -> {}", result); 77 | } 78 | 79 | @Test 80 | public void testTimeCompare() throws InterruptedException { 81 | String resourceName = "script/lua/test_time_compare.lua"; 82 | String luaName = "test_time_compare.lua"; 83 | long expectAt = System.currentTimeMillis(); 84 | Thread.sleep(1000); 85 | long now = System.currentTimeMillis(); 86 | Boolean result = stringRedisTemplate.execute(getRedisScript(resourceName, luaName, Boolean.class), Lists.newArrayList("mystream"), String.valueOf(expectAt), String.valueOf(now)); 87 | log.info("Result from lua. result -> {}", result); 88 | } 89 | 90 | @Test 91 | public void testZrangebyscoreV1() { 92 | zrangeByScoreAndRemove(); 93 | } 94 | 95 | @Test 96 | public void testZrangeByScore(){ 97 | String zsetKey = "zset:waiting_queue:mystream"; 98 | zrangeByScore(zsetKey); 99 | } 100 | 101 | private void zrangeByScore() { 102 | zrangeByScore("demo-key"); 103 | } 104 | 105 | private void zrangeByScore(String zsetKey) { 106 | log.info("----------------------------zrangeByScore----------------------------"); 107 | String resourceName = "script/lua/test_zrangebyscore_1.lua"; 108 | String luaName = "test_zrangebyscore_1.lua"; 109 | String now = String.valueOf(System.currentTimeMillis()); 110 | List result = stringRedisTemplate.execute(getRedisScript(resourceName, luaName, List.class), Lists.newArrayList(zsetKey), now, "15"); 111 | log.info("Result from lua. result -> {}", result); 112 | } 113 | 114 | private void zrangeByScoreAndRemove() { 115 | log.info("----------------------------zrangeByScoreAndRemove----------------------------"); 116 | String resourceName = "script/lua/test_zrangebyscore_3.lua"; 117 | String luaName = "test_zrangebyscore_3.lua"; 118 | String now = String.valueOf(System.currentTimeMillis()); 119 | List> results = stringRedisTemplate.execute(getRedisScript(resourceName, luaName, List.class), Lists.newArrayList("demo-key"), now, "15"); 120 | log.info("Result from lua. results -> {}", results); 121 | results.forEach(msg -> { 122 | log.info("msg[0] -> {}, msg[1] -> {}", msg.get(0), msg.get(1)); 123 | }); 124 | } 125 | 126 | @Test 127 | public void testAddAndRemove() { 128 | pushMessage(); 129 | zrangeByScoreAndRemove(); 130 | zrangeByScore(); 131 | } 132 | 133 | @Test 134 | public void testXrange(){ 135 | String resourceName = "script/lua/test_xrange.lua"; 136 | String luaName = "test_xrange.lua"; 137 | String recordId = "1665489021127-0"; 138 | String result = stringRedisTemplate.execute(getRedisScript(resourceName, luaName, String.class), Lists.newArrayList("stream:ready_queue:mystream"), recordId); 139 | log.info("Result from lua. result -> {}", result); 140 | } 141 | 142 | private void pushMessage() { 143 | log.info("----------------------------pushMessage----------------------------"); 144 | String resourceName = "script/lua/push_msg.lua"; 145 | String luaName = "push_msg.lua"; 146 | String now = String.valueOf(System.currentTimeMillis() - 10000); 147 | for (int i = 0; i < 3; i++) { 148 | String msgId = String.valueOf(UUID.randomUUID()); 149 | OrderInfo orderInfo = new OrderInfo(); 150 | orderInfo.setOrderNo((long) i); 151 | orderInfo.setUserId((long) i); 152 | Boolean result = stringRedisTemplate.execute(getRedisScript(resourceName, luaName, Boolean.class), Lists.newArrayList("demo-key"), msgId, now, JSONObject.toJSONString(orderInfo)); 153 | log.info("result -> {}", result); 154 | } 155 | } 156 | 157 | @Test 158 | public void testLoad() { 159 | // 在方法里调用执行 160 | // 第一个参数 对 lua 脚本对象的调用 161 | // 第二个参数 是 lua 脚本中 Keys 参数 162 | // 第三个参数 是 lua 脚本中 ARGV 参数 163 | String value = String.valueOf(System.currentTimeMillis()); 164 | String resourceName = "script/lua/test.lua"; 165 | String luaName = "test.lua"; 166 | String result = stringRedisTemplate.execute(getRedisScript(resourceName, luaName), Lists.newArrayList("lua-demo-key"), value); 167 | Assert.assertEquals("当前值与从redis取出的值相同", value, result); 168 | log.info("From lua. result -> {}", result); 169 | } 170 | 171 | private DefaultRedisScript getRedisScript(String resourceName, String luaName) { 172 | return RedisScriptUtils.getRedisScript(resourceName, luaName); 173 | } 174 | 175 | private DefaultRedisScript getRedisScript(String resourceName, String luaName, Class clazz) { 176 | return RedisScriptUtils.getRedisScript(resourceName, luaName, clazz); 177 | } 178 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lisa-delay-queue 2 | ## 项目介绍 3 | 4 | 基于redis-stream实现的延迟消息队列,可投入生产环境使用。 5 | 6 | 本来想取名redis stream delay queue的,不过太不个性了,因此给这个项目取名叫Lisa。 7 | 8 | 别问我Lisa是谁,Lisa是万里挑一的那个你我她。 9 | 10 | 11 | 12 | 该项目包含三个服务:`manager`、`producer`、`consumer`,各自分工明确。 13 | 14 | `manager`负责消息的调度,包括消息重试,队列长度修剪(因为stream是不会自动清理掉已经消费过的消息的,所以需要手动修剪) 15 | 16 | `producer`是消息的生产者。除了生产消息,什么都不做。 17 | 18 | `consumer`是消息的消费者。除了消费消息(也包括ack),什么都不做。 19 | 20 | 该项目分别封装了三个服务的boot-starter模块,所以只要引入maven依赖并且启动类注解加上`@SpringBootApplication`就可以正常使用了。 21 | 22 | ------ 23 | 24 | 项目依赖Redis,使用到Redis的数据结构有:zset、stream、hash、string。 25 | 26 | ##### 所有针对redis复杂的数据操作(例如一次操作中包含数据转移,删除等操作)都是基于redis脚本的,保证原子化操作。 27 | 28 | 数据结构如下: 29 | 30 | #### waiting queue(等待队列) 31 | 32 | 数据结构zset。field是msgId,score是延迟消息触发时间。 33 | 34 | `manager`会定时扫描waiting queue中的元素,当到达延迟消息发送时间后(即score小于等于当前时间戳),会将该记录从waiting queue中移除并加入到ready queue中。数据结构如下图: 35 | 36 | ![image-20221019164836734](README.assets/image-20221019164836734.png) 37 | 38 | 39 | 40 | #### ready queue(就绪队列) 41 | 42 | 数据结构stream。不做任何操作,完全交给consumer去消费,consumer关闭了自动ack机制,需要手动ack(没有异常就ack了)。数据结构如下图 43 | 44 | ![image-20221019165019235](README.assets/image-20221019165019235.png) 45 | 46 | 47 | 48 | #### retry queue(重试队列) 49 | 50 | 数据结构zset。 51 | 52 | `manager`会定期扫描一段时间ready queue中未ack的数据,然后从stream中移除,如果msgId的重试次数未不为0,则放入retry queue,如果为0,说明已经消耗完了所有的重试次数,msgId会被打入冷宫(移动到garbage中),用于复查问题。 53 | 54 | `manager`也会定期扫描到达重试时间的数据,将此类数据从重试队列中移除,放入ready queue中。 55 | 56 | 数据结构如下图: 57 | 58 | ![image-20221019165059870](README.assets/image-20221019165059870.png) 59 | 60 | 可以看到数据结构跟waiting queue的数据结构一样,只是Key的前缀不一样。 61 | 62 | 63 | 64 | #### retry count(重试次数) 65 | 66 | 数据结构hash。用于存储msgId剩余的重试次数。数据结构如下图 67 | 68 | ![image-20221019165158272](README.assets/image-20221019165158272.png) 69 | 70 | 71 | 72 | #### garbage key(垃圾key) 73 | 74 | 数据结构set。用于存储重试失败并被废弃的msgId。这里的数据不会再被使用,需要手动check。数据结构如下图: 75 | 76 | ![image-20221019165255782](README.assets/image-20221019165255782.png) 77 | 78 | 79 | 80 | #### K-V 81 | 82 | 存储关系:MessageId--MessageBody。 83 | 84 | 前几种数据格式仅仅存储MessageId(即msgId),而真正的消息对象是以KV形式存储的(在redis中的数据类型是string)。 85 | 86 | 当ready queue中的消息被正常消费并收到ack消息之后,msgId对应的对象才会被删除。 87 | 88 | ![image-20221019165355531](README.assets/image-20221019165355531.png) 89 | 90 | 用虚线是因为string类型跟其他数据类型不一样,并不是集合类型数据 91 | 92 | ------ 93 | 94 | ### 关于Manager(消息管理者) 95 | 96 | 该服务内置三个定时任务,分别如下: 97 | 98 | 1. 将消息从waiting queue或者retry queue移动到waiting queue; 99 | 2. 将pending的消息移动到retry queue; 100 | 3. 清理stream中已经消费完(已经ack)的数据。已经消费完的数据,redis是不会自动帮忙清理的,所以需要手动清理。 101 | 102 | 相关通讯图如下: 103 | 104 | ![image-20221019185748811](README.assets/image-20221019185748811.png) 105 | 106 | #### 描述如下(每一个分割线都是基于lua脚本的原子操作) 107 | 108 | - 1.1 producer生产消息,msgId存储到waiting queue 109 | - 1.2 msg body存储到 k-v object 110 | 111 | ------ 112 | 113 | - 2.1 manager扫描waiting queue中的数据,判断是否到达发送时间 114 | - 2.2 如果消息到达发送时间,将消息从waiting queue中移动到ready queue中 115 | 116 | ------ 117 | 118 | - 3 consumer消费ready queue的消息 119 | 120 | ------ 121 | 122 | - 4 如果成功消费,则进行ack处理 123 | 124 | ------ 125 | 126 | - 5.1 manager扫描ready queue中ack超时的pending数据 127 | 128 | - 5.2 检查pending数据的retry count 129 | - 5.3 如果retry count未用完,则将pending数据从ready queue移动到retry queue 130 | - 5.4 移动后,将pending数据的retry count做减1处理 131 | - 5.5 如果retry count已用完,则将该数据的msgId加入到garbage key中,并将msgId对应的retry count删除 132 | - 5.6 同时将该数据从retry queue中清理掉 133 | - 5.7 删除前缀+topic+msgId对应的对象 134 | 135 | ------ 136 | 137 | - 6.1 manager扫描retry queue中的数据,判断是否到达重试时间 138 | - 6.2 如果消息到达重试时间,将消息从retry queue中移动到ready queue中 139 | 140 | ------ 141 | 142 | 143 | 144 | ### 如何使用 145 | 146 | 为了方便上手,三个项目各有一个`demo`用于演示如何使用。感兴趣的同学可以跑一下demo。 147 | 148 | 另外,有的时候为了节省服务器资源,可以把producer和manager合并,参考`lisa-delay-queue-producer-and-manager`,该项目整合了`producer`和`manager`的配置项,可以部署到一个应用服务中。 149 | 150 | #### maven依赖 151 | 152 | 当服务端项目作为producer使用时,引入依赖如下: 153 | 154 | ```xml 155 | 156 | org.lisa.stream 157 | lisa-delay-queue-producer-spring-boot-starter 158 | 1.0.0 159 | 160 | ``` 161 | 162 | 163 | 164 | 当服务端作为consumer使用时,引入依赖如下: 165 | 166 | ```XML 167 | 168 | org.lisa.stream 169 | lisa-delay-queue-consumer-spring-boot-starter 170 | 1.0.0 171 | 172 | ``` 173 | 174 | 175 | 176 | 当服务端作为manager使用时,引入依赖如下: 177 | 178 | ```XML 179 | 180 | org.lisa.stream 181 | lisa-delay-queue-manager-spring-boot-starter 182 | 1.0.0 183 | 184 | ``` 185 | 186 | 187 | 188 | #### 生产消息 189 | 190 | producer提供了开箱即用的工具类,方法如下: 191 | 192 | ```java 193 | /** 194 | * @description: 发送消息工具类 195 | * @author: wuxu 196 | * @createDate: 2022/10/1 197 | */ 198 | public class PublishMessageUtil { 199 | 200 | /** 发送消息(相当于立刻发送) 201 | * @param topic Message Topic,即producer yaml文件里配置的topic,因为支持配置多个消息分组,因此使用哪个topic需要指定 202 | * @param msg Message Body 203 | * @param 消息体中的对象类型 204 | */ 205 | public static void sendMessage(String topic, Message msg) { 206 | sendMessage(topic, msg, System.currentTimeMillis()); 207 | } 208 | 209 | /** 210 | * 在指定时间发送消息 211 | * @param topic Message Topic,即producer yaml文件里配置的topic,因为支持配置多个消息分组,因此使用哪个topic需要指定 212 | * @param msg Message Body 213 | * @param expectAt 期望发送时间(毫秒数时间戳,默认当前系统时区) 214 | * @param 消息体中的对象类型 215 | */ 216 | public static void sendMessage(String topic, Message msg, long expectAt) { 217 | Producer.STREAM_MAP.get(topic).send(msg); 218 | } 219 | 220 | /** 221 | * 在指定时间发送消息 222 | * @param topic Message Topic,即producer yaml文件里配置的topic,因为支持配置多个消息分组,因此使用哪个topic需要指定 223 | * @param msg Message Body 224 | * @param expectAt 期望发送时间(默认当前系统时区) 225 | * @param 消息体中的对象类型 226 | */ 227 | public static void sendMessage(String topic, Message msg, LocalDateTime expectAt) { 228 | sendMessage(topic, msg, expectAt.toInstant(ZoneOffset.ofHours(8)).toEpochMilli()); 229 | } 230 | 231 | /** 232 | * 在指定时间发送消息 233 | * @param topic Message Topic,即producer yaml文件里配置的topic,因为支持配置多个消息分组,因此使用哪个topic需要指定 234 | * @param msg Message Body 235 | * @param expectAt 期望发送时间 236 | * @param zoneOffset 时区 237 | * @param 消息体中的对象类型 238 | */ 239 | public static void sendMessage(String topic, Message msg, LocalDateTime expectAt, ZoneOffset zoneOffset) { 240 | sendMessage(topic, msg, expectAt.toInstant(zoneOffset).toEpochMilli()); 241 | } 242 | } 243 | ``` 244 | 245 | 246 | 247 | #### 消费消息 248 | 249 | 基于spring event强大的解耦神器 250 | 251 | consumer的stream listener收到消息之后,会将消息封装成`MessageEvent`并发布spring event,代码片段:`applicationEventPublisher.publishEvent(new MessageEvent(topic, body));` 252 | 253 | 254 | 255 | 业务代码只要监听(实现)`ApplicationListener`即可,然后根据不同的`topic`处理不同的业务逻辑,参考如下: 256 | 257 | ```java 258 | @Slf4j 259 | @Service 260 | public class DemoService implements ApplicationListener { 261 | 262 | @SneakyThrows 263 | @Override 264 | public void onApplicationEvent(MessageEvent event) { 265 | log.info("[DemoService#onApplicationEvent], event -> {}", event); 266 | String topic = event.getTopic(); 267 | Object source = event.getSource(); 268 | log.info("topic:{}, source -> {}", topic, source); 269 | // 可以根据不同的topic处理不同的业务逻辑 270 | 271 | 272 | Message message = JSONObject.parseObject(String.valueOf(source), new TypeReference>(){ 273 | 274 | }); 275 | log.info("message -> {}", message); 276 | Class clazz = message.getClazz(); 277 | if(User.class.equals(clazz)){ 278 | User user = JSONObject.parseObject(message.getBody(), User.class); 279 | log.info("user -> {}", user); 280 | } 281 | if(OrderInfo.class.equals(clazz)){ 282 | OrderInfo orderInfo = JSONObject.parseObject(message.getBody(), OrderInfo.class); 283 | log.info("orderInfo -> {}", orderInfo); 284 | } 285 | } 286 | } 287 | ``` 288 | 289 | 290 | 291 | ------ 292 | 293 | ### 配置文件 294 | 295 | #### manager应用服务配置 296 | 297 | ```yaml 298 | lisa-delay-queue: 299 | manager-server: 300 | # manager将消息从waiting queue或者retry queue移动到ready queue的时间间隔,目前配置为1秒1次 301 | crontab-move-to-ready-queue: '0/1 * * * * ?' 302 | # manager修剪stream长度的定时任务,目前配置为1分钟1次 303 | crontab-clean-stream: '0 */1 * * * ?' 304 | # manager扫描pending消息(未及时ack)的时间间隔,目前配置为5秒1次 305 | crontab-process-pending-message: '0/5 * * * * ?' 306 | # 命令举例:xpending stream:ready_queue:mystream group-1 - + 20 307 | # range-start、range-end、count 就是上述命令中的 - + 20 308 | range-start: '-' 309 | range-end: '+' 310 | count: 20 311 | # pending多久后算超时,开始进行超时处理,目前配置为10000ms 312 | timeout: 10000 313 | # 对于pending的消息,延迟多久后重试,目前配置为20000ms 314 | delay-time: 20000 315 | # 最大重试次数 316 | max-retry-count: 10 317 | # enabed为true时才会在项目启动时运行 318 | enabled: true 319 | # 消息分组列表,支持多个分组 320 | groups: 321 | # 消息的topic 322 | - topic: mystream 323 | # 消息分组名称 324 | group: group-1 325 | # 消费者名称 326 | consumer: consumer-1 327 | # stream修剪时保留的长度 328 | max-length: 10000 329 | - topic: topic2 330 | group: group-2 331 | consumer: consumer-2 332 | max-length: 10000 333 | ``` 334 | 335 | 336 | 337 | #### producer应用服务配置 338 | 339 | ```yaml 340 | lisa-delay-queue: 341 | # enabed为true时才会在项目启动时运行 342 | enabled: true 343 | groups: 344 | - topic: mystream 345 | group: group-1 346 | - topic: topic2 347 | group: group-2 348 | ``` 349 | 350 | 351 | 352 | #### consumer应用服务配置 353 | 354 | ```yaml 355 | lisa-delay-queue: 356 | # enabed为true时才会在项目启动时运行 357 | enabled: true 358 | # consumer的server相关配置 359 | consumer-server: 360 | # 消息拉取超时时间(单位ms) 361 | pollTimeoutMillis: 5000 362 | # 批量抓取消息数量 363 | pollBatchSize: 10 364 | groups: 365 | # 消息的topic 366 | - topic: mystream 367 | # 消息的分组 368 | group: group-1 369 | # 消费者名称 370 | consumer: consumer-1 371 | - topic: topic2 372 | group: group-2 373 | consumer: consumer-2 374 | ``` 375 | 376 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------