rs = this.pool.getResource().brpop(0, "testList");
28 | // System.out.println(rs);// [testList, liucl]
29 | }
30 |
31 | @Test
32 | public void sremTest() {
33 | Long rs = this.pool.getResource().srem("worker1_queue_unique", "a509bd99-1071-4de1-9220-a280b0a4f47a");
34 | System.out.println(rs);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/queue-core/src/main/java/com/kingsoft/wps/mail/queue/config/Constant.java:
--------------------------------------------------------------------------------
1 | package com.kingsoft.wps.mail.queue.config;
2 |
3 | /**
4 | * Created by 刘春龙 on 2017/3/5.
5 | */
6 | public class Constant {
7 |
8 | // private static final String DISTR_LOCK_SUFFIX = "_lock";
9 | // 用于队列任务唯一性标记,redis set key
10 | public static final String UNIQUE_SUFFIX = "_unique";
11 |
12 | /**
13 | * 标记任务为正常执行状态
14 | */
15 | public static final String NORMAL = "normal";
16 |
17 | /**
18 | * 标记任务为重试执行状态
19 | */
20 | public static final String RETRY = "retry";
21 |
22 | /**
23 | * 任务的存活时间。单位:ms
24 | *
25 | * 注意,该时间是任务从创建({@code new Task(...)})到销毁的总时间
26 | *
27 | * 该值只针对安全队列起作用
28 | */
29 | @Deprecated
30 | public static final long ALIVE_TIMEOUT = 10 * 60 * 1000;
31 |
32 | /**
33 | * 任务执行的超时时间(一次执行)。单位:ms
34 | *
35 | * 该值只针对安全队列起作用
36 | *
37 | * TODO 后续会加入心跳健康检测
38 | */
39 | @Deprecated
40 | public static final long PROTECTED_TIMEOUT = 3 * 60 * 1000;
41 |
42 | /**
43 | * 任务重试次数
44 | */
45 | @Deprecated
46 | public static final int RETRY_TIMES = 3;
47 | }
48 |
--------------------------------------------------------------------------------
/queue-extension/src/main/java/com/kingsoft/wps/mail/queue/extension/monitor/AliveDetectHandler.java:
--------------------------------------------------------------------------------
1 | package com.kingsoft.wps.mail.queue.extension.monitor;
2 |
3 | import com.kingsoft.wps.mail.queue.Task;
4 |
5 | /**
6 | * Created by 刘春龙 on 2018/1/23.
7 | *
8 | * 健康检查
9 | *
10 | * 检查正在执行的任务是否还在执行(存活),
11 | * 为了防止耗时比较久的任务(任务的执行时间超出了通过队列管理器配置的任务执行超时时间 - 默认值:{@link com.kingsoft.wps.mail.queue.config.Constant#PROTECTED_TIMEOUT})
12 | * 会被备份队列监听器检测到并重新放入任务队列执行(因为备份队列监听器会把超出通过队列管理器配置的任务执行超时时间的任务当作是失败的任务(参考 https://github.com/fnpac/KMQueue#什么是失败任务?)并进行重试)。
13 | *
14 | * 通过这种检测机制,可以保证{@link #check(Task)}返回为true的任务(任务还在执行)不会被备份队列监听器重新放入任务队列重试。
15 | *
16 | * 这里只是提供一个接口,用户需要自己实现执行任务的健康检测。
17 | * 一个比较简单的实现方式就是起一个定时job,每隔n毫秒检查线程中正在执行任务的状态,在redis中以 "任务的id + {@link AliveDetectHandler#ALIVE_KEY_SUFFIX}" 为key,ttl 为 n+m 毫秒(m < n, m用于保证两次job的空窗期),标记正在执行的任务。
18 | * 然后{@link AliveDetectHandler}的实现类根据task去检查redis中是否存在该key,如果存在,返回true
19 | */
20 | public interface AliveDetectHandler {
21 |
22 | public static final String ALIVE_KEY_SUFFIX = "_alive";
23 |
24 | /**
25 | * 健康检查
26 | *
27 | * 任务正在执行返回true,否则(任务挂了、任务执行完成)返回false
28 | *
29 | * @param monitor 份队列监听器
30 | * @param task 要检查的任务
31 | * @return 检查结果,任务正在执行返回true,否则(任务挂了、任务执行完成)返回false
32 | */
33 | boolean check(BackupQueueMonitor monitor, Task task);
34 | }
35 |
--------------------------------------------------------------------------------
/queue-extension/src/test/java/com/kingsoft/wps/mail/MonitorTest.java:
--------------------------------------------------------------------------------
1 | package com.kingsoft.wps.mail;
2 |
3 | import com.kingsoft.wps.mail.queue.config.Constant;
4 | import com.kingsoft.wps.mail.queue.extension.monitor.BackupQueueMonitor;
5 | import com.kingsoft.wps.mail.utils.KMQUtils;
6 | import org.junit.Test;
7 |
8 | /**
9 | * Created by 刘春龙 on 2018/1/22.
10 | */
11 | public class MonitorTest {
12 |
13 | @Test
14 | public void monitorTaskTest() {
15 |
16 | // 健康检测
17 | MyAliveDetectHandler detectHandler = new MyAliveDetectHandler();
18 | // 任务彻底失败后的处理,需要实现Pipeline接口,自行实现处理逻辑
19 | MyPipeline pipeline = new MyPipeline();
20 | // 根据任务队列的名称构造备份队列的名称,注意:这里的任务队列参数一定要和KMQueueManager构造时传入的一一对应。
21 | String backUpQueueName = KMQUtils.genBackUpQueueName("worker1_queue", "worker2_queue:safe");
22 | // 构造Monitor监听器
23 | BackupQueueMonitor backupQueueMonitor = new BackupQueueMonitor.Builder("127.0.0.1", 6379, backUpQueueName)
24 | .setMaxWaitMillis(-1L)
25 | .setMaxTotal(600)
26 | .setMaxIdle(300)
27 | .setAliveTimeout(Constant.ALIVE_TIMEOUT)
28 | .setProtectedTimeout(Constant.PROTECTED_TIMEOUT)
29 | .setRetryTimes(Constant.RETRY_TIMES)
30 | .registerAliveDetectHandler(detectHandler)
31 | .setPipeline(pipeline).build();
32 | // 执行监听
33 | backupQueueMonitor.monitor();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/queue-core/src/main/java/com/kingsoft/wps/mail/utils/KMQUtils.java:
--------------------------------------------------------------------------------
1 | package com.kingsoft.wps.mail.utils;
2 |
3 | import com.kingsoft.wps.mail.queue.KMQueueAdapter;
4 | import sun.misc.BASE64Encoder;
5 |
6 | import java.io.UnsupportedEncodingException;
7 | import java.security.MessageDigest;
8 | import java.security.NoSuchAlgorithmException;
9 | import java.util.Arrays;
10 | import java.util.List;
11 |
12 | /**
13 | * Created by 刘春龙 on 2018/1/22.
14 | */
15 | public class KMQUtils {
16 |
17 | /**
18 | * 生成备份队列的名称
19 | *
20 | * @param queues 备份队列所对应的任务队列
21 | * @return 备份队列的名称
22 | */
23 | public static String genBackUpQueueName(String ...queues) {
24 | // 生成备份队列名称
25 | try {
26 | MessageDigest md5Digest = MessageDigest.getInstance("MD5");
27 | BASE64Encoder base64Encoder = new BASE64Encoder();
28 |
29 | // 获取队列名称
30 | StringBuilder queueNameMulti = new StringBuilder();
31 | // Stream 是支持并发操作的,为了避免竞争,对于reduce线程都会有独立的result,combiner的作用在于合并每个线程的result得到最终结果
32 | queueNameMulti = Arrays.stream(queues)
33 | .map(s -> s.trim().split(":")[0])
34 | .reduce(queueNameMulti,
35 | StringBuilder::append,
36 | StringBuilder::append);
37 | try {
38 | return KMQueueAdapter.BACK_UP_QUEUE_PREFIX + base64Encoder.encode(md5Digest.digest(queueNameMulti.toString().getBytes("UTF-8")));
39 | } catch (UnsupportedEncodingException e) {
40 | e.printStackTrace();
41 | }
42 | } catch (NoSuchAlgorithmException e) {
43 | e.printStackTrace();
44 | }
45 | return "";
46 | }
47 |
48 | public static String genBackUpQueueName(List queues) {
49 | return genBackUpQueueName((String[]) queues.toArray());
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/queue-core/src/main/java/com/kingsoft/wps/mail/exception/NestedException.java:
--------------------------------------------------------------------------------
1 | package com.kingsoft.wps.mail.exception;
2 |
3 | /**
4 | * Created by 刘春龙 on 2017/6/6.
5 | */
6 | public class NestedException extends RuntimeException {
7 |
8 | private static final long serialVersionUID = 1L;
9 |
10 | private ErrorMessage errorMessage;
11 |
12 | public NestedException() {
13 | super();
14 | }
15 |
16 | public NestedException(String message) {
17 | super(message);
18 | }
19 |
20 | public NestedException(String message, Throwable cause) {
21 | super(message, cause);
22 | }
23 |
24 | public NestedException(Throwable cause) {
25 | super(cause);
26 | }
27 |
28 | public NestedException(ErrorMessage errorMessage) {
29 | super(errorMessage.getErrorMsg());
30 | this.errorMessage = errorMessage;
31 | }
32 |
33 | public NestedException(ErrorMessage errorMessage, String message) {
34 | super(message);
35 | this.errorMessage = errorMessage;
36 | }
37 |
38 | public NestedException(ErrorMessage errorMessage, String message, Throwable cause) {
39 | super(message, cause);
40 | this.errorMessage = errorMessage;
41 | }
42 |
43 | public NestedException(ErrorMessage errorMessage, Throwable cause) {
44 | super(cause);
45 | this.errorMessage = errorMessage;
46 | }
47 |
48 | public ErrorMessage getErrorMessage() {
49 | return errorMessage;
50 | }
51 |
52 | public void setErrorMessage(ErrorMessage errorMessage) {
53 | this.errorMessage = errorMessage;
54 | }
55 |
56 | public Throwable getRootCause() {
57 | Throwable t = this;
58 | while (true) {
59 | Throwable cause = t.getCause();
60 | if (cause != null) {
61 | t = cause;
62 | } else {
63 | break;
64 | }
65 | }
66 | return t;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/queue-core/src/main/java/com/kingsoft/wps/mail/queue/KMQueueAdapter.java:
--------------------------------------------------------------------------------
1 | package com.kingsoft.wps.mail.queue;
2 |
3 | import com.kingsoft.wps.mail.utils.Assert;
4 | import redis.clients.jedis.Jedis;
5 | import redis.clients.util.Pool;
6 |
7 | /**
8 | * Created by 刘春龙 on 2018/1/19.
9 | */
10 | public abstract class KMQueueAdapter {
11 |
12 | // 队列模式:DEFAULT - 简单队列,SAFE - 安全队列
13 | public static final String DEFAULT = "default";
14 | public static final String SAFE = "safe";
15 | public static String BACK_UP_QUEUE_PREFIX = "back_up_queue_";// 备份队列名称前缀
16 |
17 | /**
18 | * 备份队列名称
19 | */
20 | protected String backUpQueueName;
21 |
22 | /**
23 | * redis连接池
24 | */
25 | protected Pool pool;
26 |
27 | /**
28 | * 获取备份队列的名称
29 | *
30 | * @return 备份队列的名称
31 | */
32 | public String getBackUpQueueName() {
33 | return this.backUpQueueName;
34 | }
35 |
36 | public abstract long getAliveTimeout();
37 |
38 | /**
39 | * 获取Jedis对象
40 | *
41 | * 使用完成后,必须归还到连接池中
42 | *
43 | * @return Jedis对象
44 | */
45 | public synchronized Jedis getResource() {
46 | Jedis jedis = this.pool.getResource();
47 | Assert.notNull(jedis, "Get jedis client failed");
48 | return jedis;
49 | }
50 |
51 | /**
52 | * 获取Jedis对象
53 | *
54 | * 使用完成后,必须归还到连接池中
55 | *
56 | * @param db Redis数据库序号
57 | * @return Jedis对象
58 | */
59 | public synchronized Jedis getResource(int db) {
60 | Jedis jedis = this.pool.getResource();
61 | Assert.notNull(jedis, "Get jedis client failed");
62 | jedis.select(db);
63 | return jedis;
64 | }
65 |
66 | /**
67 | * 归还Redis连接到连接池
68 | *
69 | * @param jedis Jedis对象
70 | */
71 | public synchronized void returnResource(Jedis jedis) {
72 | if (jedis != null) {
73 | // pool.returnResource(jedis);
74 | // from Jedis 3.0
75 | jedis.close();
76 | }
77 | }
78 |
79 | public synchronized void destroy() throws Exception {
80 | pool.destroy();
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/queue-core/src/test/java/com/kingsoft/wps/mail/QueueTest.java:
--------------------------------------------------------------------------------
1 | package com.kingsoft.wps.mail;
2 |
3 | import com.alibaba.fastjson.JSON;
4 | import com.alibaba.fastjson.JSONObject;
5 | import com.kingsoft.wps.mail.queue.KMQueueManager;
6 | import com.kingsoft.wps.mail.queue.Task;
7 | import com.kingsoft.wps.mail.queue.TaskQueue;
8 | import com.kingsoft.wps.mail.queue.config.Constant;
9 | import org.junit.Test;
10 |
11 | import java.util.logging.Logger;
12 |
13 | /**
14 | * Created by 刘春龙 on 2018/1/19.
15 | */
16 | public class QueueTest {
17 |
18 | private static final Logger logger = Logger.getLogger(QueueTest.class.getName());
19 |
20 | @Test
21 | public void pushTaskTest() {
22 | KMQueueManager kmQueueManager = new KMQueueManager.Builder("127.0.0.1", 6379, "worker1_queue", "worker2_queue:safe")
23 | .setMaxWaitMillis(-1L)
24 | .setMaxTotal(600)
25 | .setMaxIdle(300)
26 | .setAliveTimeout(Constant.ALIVE_TIMEOUT)
27 | .build();
28 | // 初始化队列
29 | kmQueueManager.init();
30 |
31 | // 1.获取队列
32 | TaskQueue taskQueue = kmQueueManager.getTaskQueue("worker1_queue");
33 | // 2.创建任务
34 | JSONObject ob = new JSONObject();
35 | ob.put("data", "mail proxy task");
36 | String data = JSON.toJSONString(ob);
37 | // 参数 uid:如果业务需要区分队列任务的唯一性,请自行生成uid参数,
38 | // 否则队列默认使用uuid生成策略,这会导致即使data数据完全相同的任务也会被当作两个不同的任务处理。
39 | // 参数 type:用于业务逻辑的处理,你可以根据不同的type任务类型,调用不同的handler去处理,可以不传。
40 | Task task = new Task(taskQueue.getName(), "a509bd99-1071-4de1-9220-a280b0a4f47a", true, "", data, new Task.TaskStatus());
41 | // 3.将任务加入队列
42 | Task rs = taskQueue.pushTask(task);
43 | logger.info("pushTask result:" + JSON.toJSONString(rs));
44 | }
45 |
46 | @Test
47 | public void popTaskTest() {
48 | KMQueueManager kmQueueManager = new KMQueueManager.Builder("127.0.0.1", 6379, "worker1_queue", "worker2_queue:safe")
49 | .setMaxWaitMillis(-1L)
50 | .setMaxTotal(600)
51 | .setMaxIdle(300)
52 | .setAliveTimeout(Constant.ALIVE_TIMEOUT)
53 | .build();
54 | // 初始化队列
55 | kmQueueManager.init();
56 |
57 | // 1.获取队列
58 | TaskQueue taskQueue = kmQueueManager.getTaskQueue("worker1_queue");
59 | // 2.获取任务
60 | Task task = taskQueue.popTask();
61 | // 业务处理放到TaskConsumersHandler里
62 | if (task != null) {
63 | task.doTask(kmQueueManager, MyTaskHandler.class);
64 | }
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | com.kingsoft.wps.mail
8 | queue
9 | pom
10 | 1.0-SNAPSHOT
11 |
12 | queue-core
13 | queue-extension
14 | distributed-lock
15 |
16 |
17 |
18 |
19 | 3.5.1
20 | 2.12.4
21 | 1.8
22 | UTF-8
23 | 2.9.0
24 | 2.7.1
25 | 1.2.16
26 | 3.0.0
27 |
28 |
29 |
30 |
31 |
32 | redis.clients
33 | jedis
34 | ${jedis.version}
35 |
36 |
37 |
38 | com.fasterxml.jackson.core
39 | jackson-databind
40 | ${jackson.version}
41 |
42 |
43 | com.fasterxml.jackson.jaxrs
44 | jackson-jaxrs-json-provider
45 | ${jackson.version}
46 |
47 |
48 |
49 | com.alibaba
50 | fastjson
51 | ${alibaba.fastjson.version}
52 |
53 |
54 | junit
55 | junit
56 | 4.12
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | org.apache.maven.plugins
65 | maven-compiler-plugin
66 | ${compiler.plugin.version}
67 |
68 | ${jdk.version}
69 | ${jdk.version}
70 | ${project.build.sourceEncoding}
71 | true
72 |
73 |
74 |
75 |
76 | org.apache.maven.plugins
77 | maven-source-plugin
78 | ${source.plugin.version}
79 |
80 | true
81 |
82 |
83 |
84 | compile
85 |
86 | jar
87 |
88 |
89 |
90 |
91 |
92 | org.apache.maven.plugins
93 | maven-surefire-plugin
94 | ${maven-surefire-plugin.version}
95 |
96 | true
97 |
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/queue-core/src/main/java/com/kingsoft/wps/mail/queue/backup/RedisBackupQueue.java:
--------------------------------------------------------------------------------
1 | package com.kingsoft.wps.mail.queue.backup;
2 |
3 | import com.alibaba.fastjson.JSON;
4 | import com.kingsoft.wps.mail.queue.KMQueueAdapter;
5 | import com.kingsoft.wps.mail.queue.Task;
6 | import com.kingsoft.wps.mail.queue.config.Constant;
7 | import redis.clients.jedis.Jedis;
8 | import redis.clients.jedis.Transaction;
9 |
10 | import java.util.List;
11 | import java.util.logging.Logger;
12 |
13 | /**
14 | * Created by 刘春龙 on 2017/3/5.
15 | *
16 | * 备份队列
17 | */
18 | public class RedisBackupQueue extends BackupQueue {
19 |
20 | private static final Logger logger = Logger.getLogger(RedisBackupQueue.class.getName());
21 |
22 | private static final int REDIS_DB_IDX = 0;
23 | public static final String MARKER = "marker";
24 |
25 | /**
26 | * 备份队列的名称
27 | */
28 | private final String name;
29 |
30 | /**
31 | * 队列管理器
32 | */
33 | private KMQueueAdapter kmQueueAdapter;
34 |
35 | public RedisBackupQueue(KMQueueAdapter kmQueueAdapter) {
36 | this.kmQueueAdapter = kmQueueAdapter;
37 | this.name = kmQueueAdapter.getBackUpQueueName();
38 | }
39 |
40 | /**
41 | * 初始化备份队列,添加备份队列循环标记
42 | */
43 | @Override
44 | public void initQueue() {
45 | Jedis jedis = null;
46 | try {
47 | jedis = kmQueueAdapter.getResource(REDIS_DB_IDX);
48 |
49 | // 创建备份队列循环标记
50 | Task.TaskStatus state = new Task.TaskStatus();
51 | Task task = new Task(this.name, null, RedisBackupQueue.MARKER, null, state);
52 |
53 | String taskJson = JSON.toJSONString(task);
54 |
55 | // 注意分布式问题,防止备份队列添加多个循环标记
56 | // 这里使用redis的事务&乐观锁
57 | jedis.watch(this.name);// 监视当前队列
58 | boolean isExists = jedis.exists(this.name);// 查询当前队列是否存在
59 |
60 | List backQueueData = jedis.lrange(this.name, 0, -1);
61 | logger.info("========================================");
62 | logger.info("Backup queue already exists! Queue name:" + this.name);
63 | logger.info("Backup queue[" + this.name + "]data:");
64 | backQueueData.forEach(logger::info);
65 | logger.info("========================================");
66 |
67 | Transaction multi = jedis.multi();// 开启事务
68 | if (!isExists) {// 只有当前队列不存在,才执行lpush
69 | multi.lpush(this.name, taskJson);
70 | List