一个简单的消息队列的实现(支持延时消息,支持持久化,保证唯一消费)
主要的消息管理者对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | package com.rynk.mugua.trading.biz.service.impl; import java.util.concurrent.DelayQueue; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.rynk.commons.entity.QueueMessage; import com.rynk.mugua.trading.biz.commons.RedisKeyResolver; import com.rynk.mugua.trading.biz.commons.lock.DistributedLockHandler; import com.rynk.mugua.trading.biz.eth.DelayedTack; import lombok.extern.slf4j.Slf4j; /** * 延时消息管理员 * @author ZHANGYUKUNUP * */ @Component @Slf4j public class QueueManger { MessagePersistent messagePersistent; /** * 延时消息队列 */ private DelayQueue<DelayedTack> dQueue = new DelayQueue<>(); /** * 消息任务处理线程 */ private Thread taskThread; @Autowired DistributedLockHandler lock; public QueueManger() { taskThread = new TaskThread(); taskThread.start(); } /** * 任务线程 * @author ZHANGYUKUNUP * */ class TaskThread extends Thread{ @Override public void run() { while ( true ) { try { DelayedTack delayedTack = dQueue.take(); QueueMessage queueMessage = delayedTack.getQueueMessage(); if ( queueMessage == null ) { return ; } //简单的加个锁保证消息不被重复消费(需要保证解锁前 数据被提交到数据库,否者会出同步问题 ,也就是说不能有更加大的 事务范围 包裹当前方法 ) if ( lock.tryLock( RedisKeyResolver.getMsgrKey( queueMessage.getId() ) ) ) { //如果这个消息被正常消费,那么久标记消费成功,如果异常消费,那么久重试这个消息 try { if ( QueueManger. this .messageDispense(delayedTack.getQueueMessage()) ) { messagePersistent.succeed( queueMessage ); } else { QueueManger. this .reTry( queueMessage ); } } catch (Exception e) { e.printStackTrace(); QueueManger. this .reTry(queueMessage); } finally { lock.unLock( RedisKeyResolver.getMsgrKey( queueMessage.getId() ) ); } } } catch (Exception e) { e.printStackTrace(); } } } } /** * 重试 * @param queueMessage */ protected void reTry(QueueMessage queueMessage) { messagePersistent.reTry(queueMessage); } /** * 分发消息 * @param queueMessage */ protected boolean messageDispense(QueueMessage queueMessage) { return messagePersistent.consume(queueMessage); } /** * 添加一个延时消息 * @param delayedTack */ public void put(DelayedTack delayedTack) { dQueue.put(delayedTack); } /** * 查询未处理的延时消息数量 * @return */ public int size() { return dQueue.size(); } /** * 消息处理线程存活状态 * @return */ public boolean isAlive() { return taskThread.isAlive(); } } |
消息对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | package com.rynk.commons.entity; import java.util.Date; import org.springframework.data.mongodb.core.mapping.Document; import com.rynk.commons.entity.em.QueueMessageType; import com.rynk.commons.entity.em.TransferRecordStatus; import com.rynk.commons.util.SnGeneratorUtil; import lombok.Data; @Data @Document (collection = "mg_queue_message" ) public class QueueMessage extends BaseEntity { /** * 唤醒时间 */ private Date awakenDate; /** * 处理状态 */ private TransferRecordStatus transferRecordStatus; /** * 消息体 */ private String body; /** * 消息体类型 */ private QueueMessageType type; /** * 重试次数 */ private Integer tryTimes; /** * 最后一次核对时间 */ private Date lastCheckDate; /** * * @param body 消息体来源类型 * @param type 消息类型 * @param delayed 延时 * @return */ public static QueueMessage newInstance( String body , QueueMessageType type , long delayed ) { QueueMessage item = new QueueMessage(); item.setId( SnGeneratorUtil.getId().toString() ); item.setCreateDate( new Date() ); item.setDr( false ); item.setTransferRecordStatus( TransferRecordStatus.WAIT ); item.setTryTimes( 1 ); item.setBody(body); item.setType(type); item.setAwakenDate( new Date( System.currentTimeMillis()+delayed )); item.setLastCheckDate( item.getAwakenDate() ); return item; } } |
基于redis 的 分布式锁对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | package com.rynk.mugua.trading.biz.commons.lock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import javax.annotation.Resource; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.TimeUnit; /** * 分布式锁 * * @author ZHANGYUKUN * */ @Component public class DistributedLockHandler { private static final Logger logger = LoggerFactory.getLogger(DistributedLockHandler. class ); /** * 最大持有锁的时间(毫秒) */ private final static long LOCK_EXPIRE = 30 * 1000L; /** * 尝试获取锁的时间间隔(毫秒) */ private final static long LOCK_TRY_INTERVAL = 30L; /** * 获取锁最大等待时间( 毫秒 ) */ private final static long LOCK_TRY_TIMEOUT = 20 * 1000L; @Resource // (name = "customRedisTemplate") private RedisTemplate<String, String> template; /** * 尝试获取 分布式锁 * * @param lockKey * 锁名 * @return true 得到了锁 ,false 获取锁失败 */ public boolean tryLock(String lockKey) { return getLock(lockKey, LOCK_TRY_TIMEOUT, LOCK_TRY_INTERVAL, LOCK_EXPIRE); } /** * 尝试获取 分布式锁(不自动释放锁) * * @param lockKey * 锁名 * @return true 得到了锁 ,false 获取锁失败 */ public boolean tryLockNotAutoRelease(String lockKey) { return getLock(lockKey, LOCK_TRY_TIMEOUT, LOCK_TRY_INTERVAL, - 1 ); } /** * 尝试获取 分布式锁 * * @param lockKey * 锁名 * @param timeout * 获取锁最大等待时间 * @return true 得到了锁 ,false 获取锁失败 */ public boolean tryLock(String lockKey, long timeout) { return getLock(lockKey, timeout, LOCK_TRY_INTERVAL, LOCK_EXPIRE); } /** * 尝试获取 分布式锁(不自动释放锁) * * @param lockKey * 锁名 * @param timeout * 获取锁最大等待时间 * @return true 得到了锁 ,false 获取锁失败 */ public boolean tryLockNotAutoRelease(String lockKey, long timeout) { return getLock(lockKey, timeout, LOCK_TRY_INTERVAL, - 1 ); } /** * 尝试获取 分布式锁 * * @param lockKey * 锁名 * @param timeout * 获取锁最大等待时间 * @param tryInterval * 获取锁尝试 时间间隔 * @return true 得到了锁 ,false 获取锁失败 */ public boolean tryLock(String lockKey, long timeout, long tryInterval) { return getLock(lockKey, timeout, tryInterval, LOCK_EXPIRE); } /** * 尝试获取 分布式锁(不释放锁) * * @param lockKey * 锁名 * @param timeout * 获取锁最大等待时间 * @param tryInterval * 获取锁尝试 时间间隔 * @return true 得到了锁 ,false 获取锁失败 */ public boolean tryLockNotAutoRelease(String lockKey, long timeout, long tryInterval) { return getLock(lockKey, timeout, tryInterval, - 1 ); } /** * 尝试获取 分布式锁 * * @param lockKey * 锁名 * @param timeout * 获取锁最大等待时间 * @param tryInterval * 获取锁尝试 时间间隔 * @param lockExpireTime * 锁最大持有时间 * @return true 得到了锁 ,false 获取锁失败 */ public boolean tryLock(String lockKey, long timeout, long tryInterval, long lockExpireTime) { return getLock(lockKey, timeout, tryInterval, lockExpireTime); } /** * 获取分布式锁 * * @param lockKey * 锁名 * @param timeout * 获取锁最大等待时间 * @param tryInterval * 获取锁尝试 时间间隔 * @param lockExpireTime * 锁最大持有时间 * @return true 得到了锁 ,false 获取锁失败 */ public boolean getLock(String lockKey, long timeout, long tryInterval, long lockExpireTime) { try { if (StringUtils.isEmpty(lockKey)) { return false ; } long startTime = System.currentTimeMillis(); do { ValueOperations<String, String> ops = template.opsForValue(); SimpleDateFormat sd = new SimpleDateFormat( "yyyy-mm-dd HH:mm:ss" ); if (ops.setIfAbsent(lockKey, sd.format( new Date()) )) { if (lockExpireTime > 0 ) { template.expire(lockKey, lockExpireTime, TimeUnit.MILLISECONDS); } return true ; } Thread.sleep(tryInterval); } while (System.currentTimeMillis() - startTime < timeout); } catch (InterruptedException e) { logger.error(e.getMessage()); return false ; } return false ; } /** * 释放锁 * * @param lockKey */ public void unLock(String lockKey) { if (!StringUtils.isEmpty(lockKey)) { template.delete(lockKey); } } } |
延时任务对象: 用来分装延时消息的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | package com.rynk.mugua.trading.biz.eth; import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; import com.rynk.commons.entity.QueueMessage; import com.rynk.mugua.trading.biz.mqMessage.ChainMessageDelayTimeLevel; /** * 延时任务 * @author zhangyukun * */ public class DelayedTack implements Delayed{ /** * 执行的时间 */ Long runTime; /** * 消息对象 */ QueueMessage queueMessage; public QueueMessage getQueueMessage() { return queueMessage; } public void setQueueMessage(QueueMessage queueMessage) { this .queueMessage = queueMessage; } /** * * @param delay 延时毫秒数 * @param queueMessage 消息体 */ public DelayedTack( QueueMessage queueMessage ) { if ( queueMessage.getTryTimes() == 1 ) { this .runTime = queueMessage.getAwakenDate().getTime(); } else { this .runTime =System.currentTimeMillis() + ChainMessageDelayTimeLevel.getDelayTimeLevel( queueMessage.getTryTimes() )* 1000 ; } this .queueMessage = queueMessage; } @Override public long getDelay(TimeUnit unit) { return unit.convert( runTime - System.currentTimeMillis() , TimeUnit.MILLISECONDS); } @Override public int compareTo(Delayed o) { return ( int ) ( this .getDelay(TimeUnit.MILLISECONDS) -o.getDelay(TimeUnit.MILLISECONDS)); } } |
持久化消息的对象: 三个 方式 按照自己的实现做就是了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | package com.rynk.mugua.trading.biz.service.impl; import com.rynk.commons.entity.QueueMessage; public class MessagePersistent { /** * 消费这个消息要处理的业务逻辑 * @param queueMessage * @return */ public boolean consume(QueueMessage queueMessage) { return false ; } /** * 标记这个消息已经被正常消费 * @param queueMessage */ public void succeed(QueueMessage queueMessage) { } /** * 重试消息(标记数据库的状态,然后把它重新放到延时队列中) * @param queueMessage */ public void reTry(QueueMessage queueMessage) { } } |
备注: 上面的 消息队列,基于Java的 延时消息,带有持久,保证唯一消费,但是有一个明显的 问题 ,所有的消息都在内存,如果有1亿条消息,内存肯定是要爆炸的 ,
如果要求 消息负载大,那么我们可以考虑 吧部分消息 放在 内存。
1 同上 所有消息都要持久化,保证不丢失,
2 只有最近一段时间需要消费的消息才加入到内存( 比如一分钟 )
3 加入消息的时候 ,判断这个消息的执行执行时间,如果是 1 分钟以内,持久化以后,直接放入内存队列, 1 分钟以外,直接 持久化,不放入内存。
4 每隔 一段时间 ( 1 分钟 ),载入一个,并且可以指定载入条数。
5 如果是 多节点,考虑分布式环境的消息消费,可以 通过 消息Id 模 节点编号,来指定分配消息( 数据分片原理, 之歌分片不能随意改动,改动为题也不大,这是知识消费,避免 重复消费就是了,但是 如果一个节点 挂了,可能 部分消息不消费 )
能耍的时候就一定要耍,不能耍的时候一定要学。
--天道酬勤,贵在坚持posted on 2020-01-07 17:40 zhangyukun 阅读(1459) 评论(0) 编辑 收藏 举报
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步