前言
在项目中使用到了rocketmq, 同时为了保证一些重要的消息不丢失,就算发送失败也能够溯源,于是在生产者端对mq的消息状态进行判断,如果发送失败或者发送后出现了异常,将错误消息存入redis,然后建立定时任务从reids中拿到错误消息,进行重试,建立了这样一套为了完全保证rocketmq生产者端不丢失运用流程。但是,在使用的过程中发现有一些重复的功能代码,导致项目中关于rocketmq的使用的代码非常的凌乱,于是把这些公共代码抽取为util类,设计优化了下。
代码
生产者端
MqKeyGenerator
MqKeyGenerator 主要用于一些业务键的生成,这里主要有3个键值
//生产者端消息业务key
private static final String BIZ_OPERATION_PREFIX = "mm_biz_";
//消费者端幂等处理key
private final static String PREFIX_IDEMPOTENT = "mm_idempotent_";
//事务key
private static final String TRANS_PREFIX = "mm_trans_";
代码如下:
package com.iunicorn.mall.middle.starter.rocketmq.utils;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.iunicorn.mall.middle.starter.rocketmq.RocketMQProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
/**
* @Title: MqKeyGenerator
* @Description: 生成业务key
* @date : 2020/11/4 15:31
*/
@Component
public class MqKeyGenerator {
private volatile String bizCacheKey;
private volatile String transCacheKey;
private volatile String consumerCacheKey;
private static final String BIZ_OPERATION_PREFIX = "mm_biz_";
private final static String PREFIX_IDEMPOTENT = "mm_idempotent_";
private static final String TRANS_PREFIX = "mm_trans_";
private final String separator ="_";
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RocketMQProperties properties;
@Value("${spring.application.name}")
private String applicationName;
private String getBizCacheKey(String producerGroup) {
if (bizCacheKey == null) {
synchronized (this) {
if (bizCacheKey == null) {
this.bizCacheKey = BIZ_OPERATION_PREFIX + producerGroup + separator;
}
}
}
return bizCacheKey;
}
public String getConsumerCacheKey(String producerGroup){
if(consumerCacheKey == null){
synchronized (this){
if(consumerCacheKey == null){
consumerCacheKey = PREFIX_IDEMPOTENT + producerGroup + separator;
}
}
}
return consumerCacheKey;
}
private String getTransCacheKey(String producerGroup) {
if (transCacheKey == null) {
synchronized (this) {
if (transCacheKey == null) {
transCacheKey = TRANS_PREFIX + producerGroup + separator;
}
}
}
return transCacheKey;
}
/**
* 业务操作的id,全局唯一,防止消费者重复消费
*
* @return
*/
public String getBizOperationId() {
String producerGroup = getDefaultProducerGroupName();
String bizCacheKey = getBizCacheKey(producerGroup);
return bizCacheKey + redisTemplate.opsForValue().increment(bizCacheKey);
}
/**
* 获取消费者端锁的key
* @param bizId
* @return
*/
public String getConsumerLockKey(String bizId) {
String producerGroup = getDefaultProducerGroupName();
return getConsumerCacheKey(producerGroup) + bizId;
}
/**
* 在同一producer获得新的事务Id,用于检查事务状态全局唯一
*
* @return
*/
private String getNewTransactionId() {
String producerGroup = getDefaultProducerGroupName();
String transCacheKey = getTransCacheKey(producerGroup);
return transCacheKey + redisTemplate.opsForValue().increment(transCacheKey);
}
private String getDefaultProducerGroupName(){
String producerGroup = properties.getProducerGroup();
if(producerGroup == null){
producerGroup = applicationName;
}
return producerGroup;
}
}
MessageUtils
MessageUtils:封装构建消息
/**
* @Title: MessageUtils
* @Description: 消息工具类
* @date : 2020/11/4 15:15
*/
@Component
public class MessageUtils {
@Autowired
private MqKeyGenerator keyGenerator;
public Message buildMessage(String msg){
Message<String> message = MessageBuilder.withPayload(msg).build();
return message;
}
public Message buildMessage(BasePayload payload) {
payload.setBizId(keyGenerator.getBizOperationId());
Message msg = MessageBuilder.withPayload(payload).build();
return msg;
}
public Message buildMessage(BasePayload payload, String keys) {
payload.setBizId(keyGenerator.getBizOperationId());
MessageBuilder builder = getBuilder(payload, keys);
return builder.build();
}
public Message buildTransactionMessage(BasePayload payload, String keys, String transactionId) {
payload.setBizId(keyGenerator.getBizOperationId());
MessageBuilder builder = getBuilder(payload, keys);
builder.setHeader(RocketMQHeaders.TRANSACTION_ID, transactionId);
return builder.build();
}
private MessageBuilder getBuilder(BasePayload payload, String keys){
MessageBuilder<BasePayload> builder = MessageBuilder.withPayload(payload);
if(StringUtils.isNotBlank(keys)){
builder.setHeader(RocketMQHeaders.KEYS, keys);
}
return builder;
}
}
RocketMQProductUtils
RocketMQProductUtils:生产者工具类,包装rocketmq发送消息方法
/**
* @Title: RocketMQProductUtils
* @Description: rocketMq 生产者工具类
* @date : 2020/11/4 15:05
*/
@Slf4j
public class RocketMQProductUtils {
public static String SEPARATOR_COLON = ":";
@Autowired
private RocketMQTemplate template;
@Autowired
private MessageUtils messageUtils;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 单向发送, 消息无应答
* @param topic
* @param message
*/
public void sendOneWay(String topic, String message){
template.sendOneWay(topic, messageUtils.buildMessage(message));
}
public void sendOneWay(String topic, BasePayload payload){
template.sendOneWay(topic, messageUtils.buildMessage(payload));
}
public void syncSend(String topic, BasePayload payload){
Message message = messageUtils.buildMessage(payload);
log.info("topic_" + topic + " send msg: " + JSON.toJSONString(message));
SendResult sendResult = template.syncSend(topic, message);
syncResultHandle(topic, payload, sendResult);
}
public void syncSend(String topic, String keys, BasePayload payload){
Message message = messageUtils.buildMessage(payload, keys);
log.info("topic_" + topic + " send msg: " + JSON.toJSONString(message));
SendResult sendResult = template.syncSend(topic, message);
syncResultHandle(topic, payload, sendResult);
}
public void syncSend(String topic, String keys, String tags, BasePayload payload){
Message message = messageUtils.buildMessage(payload, keys);
log.info("topic_" + topic + " send msg: " + JSON.toJSONString(message));
SendResult sendResult = template.syncSend(topic + SEPARATOR_COLON + tags, message);
syncResultHandle(topic, payload, sendResult);
}
private void syncResultHandle(String topic, BasePayload payload, SendResult sendResult){
log.info("topic_" + topic + " response received msg: " + JSON.toJSONString(sendResult));
if(SendStatus.SEND_OK != sendResult.getSendStatus()){
stringRedisTemplate.opsForHash().put(MqConstant.TOPIC_PREFIX + topic, payload.getBizId(), JSON.toJSONString(payload));
}
}
public void asyncSend(String topic, BasePayload payload){
Message message = messageUtils.buildMessage(payload);
log.info("topic_" + topic + " send msg: " + JSON.toJSONString(message));
template.asyncSend(topic, message, getSendCallback(topic, payload));
}
public void asyncSend(String topic, String keys, BasePayload payload){
Message message = messageUtils.buildMessage(payload, keys);
log.info("topic_" + topic + " send msg: " + JSON.toJSONString(message));
template.asyncSend(topic, message, getSendCallback(topic, payload));
}
public void asyncSend(String topic, String keys, String tags, BasePayload payload){
Message message = messageUtils.buildMessage(payload, keys);
log.info("topic_" + topic + " send msg: " + JSON.toJSONString(message));
template.asyncSend(topic + SEPARATOR_COLON + tags, messageUtils.buildMessage(payload, keys), getSendCallback(topic, payload));
}
private SendCallback getSendCallback(String topic, BasePayload payload){
return new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("topic_" + topic + " response received msg: " + JSON.toJSONString(sendResult));
}
@Override
public void onException(Throwable e) {
e.printStackTrace();
stringRedisTemplate.opsForHash().put(MqConstant.TOPIC_PREFIX + topic, payload.getBizId(), JSON.toJSONString(payload));
}
};
}
public TransactionSendResult sendMessageInTransaction(String txProducerGroup, String topic, String keys, String tags, BasePayload payload, Object arg){
String transactionId = IdWorker.get32UUID();
Message message = messageUtils.buildTransactionMessage(payload, keys, transactionId);
log.info("topic_" + topic + " send transaction msg: " + JSON.toJSONString(message));
String destination = topic;
if(StringUtils.isNotBlank(tags)){
destination = topic + SEPARATOR_COLON + tags;
}
TransactionSendResult transactionSendResult = template.sendMessageInTransaction(txProducerGroup, destination, message, arg);
log.info("topic_" + topic + " response received localTransactionState={}, transactionId={}, msg: {}", transactionSendResult.getLocalTransactionState().toString(), transactionId, JSON.toJSONString(transactionSendResult));
return transactionSendResult;
}
}
MqScheduleTaskTemplate
MqScheduleTaskTemplate: 生产者端定时任务模板,实现对在redis缓存中消息的重试。
@Slf4j
@Async
public abstract class MqScheduleTaskTemplate {
/**
* 需要检查的topic名称
*/
public List<String> topicList = new ArrayList<>();
/**
* 默认加锁的key值
*/
public String lockKey;
/**
* 默认加锁30分钟
*/
public long LOCK_TIME = RedisConfig.DEFAULT_LOCK_EXPIRED_TIME;
private static String MQ_TASK_LOCK_PREFIX = "mq_task_lock_";
@Autowired
private RedisLock redisLock;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RocketMQTemplate template;
public void failMessageRetry() {
if(StringUtils.isBlank(lockKey)){
throw new RuntimeException("please set lockKey value");
}
log.info("fail mq message retry start!");
String value = IdWorker.getIdStr();
boolean lock = redisLock.lock(MQ_TASK_LOCK_PREFIX + lockKey, value, LOCK_TIME, RedisConfig.DEFAULT_TRY_LOCK_TIMEOUT);
if (!lock) {
log.error("fail to get mqLockKey");
return;
}
try {
this.reTry();
} catch (Exception e) {
log.error("message retry error, errMsg:{}", e.getMessage(), e);
} finally {
redisLock.unlock(MQ_TASK_LOCK_PREFIX + lockKey, value);
}
log.info("fail mq message retry success!");
}
private void reTry() {
for(String topic : topicList){
List<Object> failMsgList = stringRedisTemplate.opsForHash().values(MqConstant.TOPIC_PREFIX +topic);
log.info("fail mq message : " + JSON.toJSONString(failMsgList));
if(!CollectionUtils.isEmpty(failMsgList)) {
for(Object failMsg : failMsgList){
BasePayload basePayload = JSON.parseObject(failMsg.toString(), BasePayload.class);
SendResult sendResult = template.syncSend(topic, basePayload);
if(SendStatus.SEND_OK == sendResult.getSendStatus()){
stringRedisTemplate.opsForHash().delete(MqConstant.TOPIC_PREFIX +topic, basePayload.getBizId());
}
}
}
}
}
}
消费者端
RocketMQListenerAwareNew
RocketMQListenerAwareNew:对消费者端消息的幂等处理
@Slf4j
public abstract class RocketMQListenerAwareNew<E> implements RocketMQListener<MessageExt>, InitializingBean {
private Class messageType;
private final String charset = "UTF-8";
@Autowired
private MqKeyGenerator keyGenerator;
@Autowired
private RedisLock redisLock;
@Override
public void onMessage(MessageExt message) {
Object resultObj = JSON.parseObject(new String(message.getBody(), Charset.forName(charset)), messageType);
//多实例时实现消息幂等
String bizId = ((BasePayload) resultObj).getBizId();
String lockKey = keyGenerator.getConsumerLockKey(bizId);
String value = IdWorker.getIdStr();
boolean lock = redisLock.lock(lockKey, value, RedisConfig.DEFAULT_LOCK_EXPIRED_TIME, 0L);
if(!lock){
throw new DuplicateConsumptionException("Duplicate consumption: "+bizId);
}
try{
consumerMessage((E) resultObj);
}catch (Exception e){
e.printStackTrace();
}finally {
redisLock.unlock(lockKey, value);
}
}
/**
* 消费消息
* 注意: 一定要自己实现消息的幂等,rocketMQ只保证消息一定会被投递,不能保证消息只被投递一次
* 1、建议通过message keys实现 2、数据库对业务key设置唯一索引
* @param message
*/
public abstract void consumerMessage(E message);
@Override
public void afterPropertiesSet() {
this.messageType = getMessageType();
log.debug("RocketMQ messageType: {}", messageType.getName());
}
private Class getMessageType() {
return (Class) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
}
生产者事务消息
RocketMQLocalTransactionListenerAware
/**
* RocketMq本地事务监听器实现,监听一个txProducerGroup,不同的txProducerGroup需要自己实现不同的监听器
* 实现了以下功能:
* 1.MQ 在消息状态异常情况下对本地事务执行状态进行检查
* 2.在同一producer获得新的全局唯一
*/
@Slf4j
public abstract class RocketMQLocalTransactionListenerAware<E> implements RocketMQLocalTransactionListener, InitializingBean{
private Class messageType;
@Autowired
RedisTemplate<String, Object> redisTemplate;
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
MessageHeaders headers = msg.getHeaders();
String transId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);
Object statusObj = redisTemplate.opsForValue().get(transId);
int status = 0;
if (statusObj != null) {
status = Integer.parseInt(statusObj.toString());
}
RocketMQLocalTransactionState transactionState;
switch (status) {
case 1:
transactionState = RocketMQLocalTransactionState.COMMIT;
break;
case 2:
transactionState = RocketMQLocalTransactionState.ROLLBACK;
break;
default:
transactionState = RocketMQLocalTransactionState.UNKNOWN;
break;
}
log.info("--- The local transaction was executed once, transactionId={}, status={}, transactionState={} ---", transId, status, transactionState);
return transactionState;
}
@Override
public RocketMQLocalTransactionState executeLocalTransaction(final Message msg, final Object arg) {
MessageHeaders headers = msg.getHeaders();
String transId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);
try {
//执行本地事务(业务逻辑代码)
byte[] array = (byte[])msg.getPayload();
Object message = JSON.parseObject(new String(array), messageType);
execute((E)message, arg);
redisTemplate.opsForValue().set(transId, 1,30, TimeUnit.MINUTES);
log.info("local transaction was successfully executed, transactionId={}, msg={}", transId, msg.getPayload());
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
log.error("execute local transactionId={}, error msg[{}]", transId, e.getMessage());
redisTemplate.opsForValue().set(transId, 2,30, TimeUnit.MINUTES);
return RocketMQLocalTransactionState.ROLLBACK;
}
}
/**
* 执行本地事务
*
* @param msg
* @param arg
*/
protected abstract void execute(final E msg, final Object arg);
@Override
public void afterPropertiesSet() {
this.messageType = getMessageType();
log.debug("RocketMQ messageType: {}", messageType.getName());
}
private Class getMessageType() {
return (Class) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
}
RedisLock
RedisLock:redis锁的简单实现
@Slf4j
public class RedisLock implements InitializingBean {
private static final String LUA_LOCK = " local key=KEYS[1] "
+ "local expireTime =tonumber(ARGV[2]) "
+ "local value = ARGV[1] "
+ "local result = tonumber(redis.call('setnx',key ,value)) "
+ "if result == 1 then "
+ " redis.call('pexpire',key,expireTime) "
+ "end "
+ "return result";
private static final String LUA_UNLOCK = " if redis.pcall('get', KEYS[1]) == ARGV[1] then "
+ " return redis.pcall('del', KEYS[1]) "
+ " else return 0 end";
/*
* *
* 限流
*/
private static final String LUA_PRECISE_LIMIT = " local timeNow = tonumber(ARGV[3]) "
+ " if tonumber(redis.call('llen',KEYS[1]))>= tonumber(ARGV[1]) then "
+ " local tiemOld = tonumber(redis.call('lpop',KEYS[1])) "
+ " redis.call('rpush',KEYS[1],timeNow) "
+ " if timeNow - tonumber(tiemOld) < tonumber(ARGV[2]) then "
+ " return 0 "
+ " else "
+ " return 1 "
+ " end "
+ " return 1"
+ " end "
+ " redis.call('rpush',KEYS[1],timeNow) "
+ " return 1 ";
private static final String IP_RATE_LIMIT = " local key = KEYS[1] "
+ " local limit = tonumber(ARGV[1]) "
+ " local expireTime = ARGV[2] "
+ " local is_exists = redis.call('EXISTS', key) "
+ " if is_exists == 1 then "
+ " if redis.call('INCR', key) > limit then "
+ " return 0 "
+ " else "
+ " return 1 "
+ " end "
+ " else "
+ " redis.call('SET', key, 1) "
+ " redis.call('EXPIRE', key, expireTime) "
+ " return 1 "
+ " end ";
private DefaultRedisScript<Long> lockRedisScript;
private DefaultRedisScript<Long> unlockRedisScript;
private DefaultRedisScript<Long> limitRedisScript;
private DefaultRedisScript<Long> ipLimitRedisScript;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void afterPropertiesSet() throws Exception {
lockRedisScript = new DefaultRedisScript<Long>();
lockRedisScript.setScriptText(LUA_LOCK);
lockRedisScript.setResultType(Long.class);
unlockRedisScript = new DefaultRedisScript<Long>();
unlockRedisScript.setScriptText(LUA_UNLOCK);
unlockRedisScript.setResultType(Long.class);
limitRedisScript = new DefaultRedisScript<Long>();
limitRedisScript.setScriptText(LUA_PRECISE_LIMIT);
limitRedisScript.setResultType(Long.class);
ipLimitRedisScript = new DefaultRedisScript<Long>();
ipLimitRedisScript.setScriptText(IP_RATE_LIMIT);
ipLimitRedisScript.setResultType(Long.class);
}
/**
* 获取redis分布式锁
**/
public boolean lock(String key, String owner, long lockExpireTime, long tryLockTimeout) {
long timestamp = System.currentTimeMillis();
// 在超时之前,循环尝试拿锁
while (tryLockTimeout == 0 || ((System.currentTimeMillis() - timestamp) < tryLockTimeout)) {
Long result = redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
Long evalResult = connection.eval(LUA_LOCK.getBytes(), ReturnType.INTEGER, 1, key.getBytes(), owner.getBytes(), String.valueOf(lockExpireTime).getBytes());
log.debug("lock eval lua result [{}] ", evalResult);
return evalResult;
}
});
if (result == 1) {
return true;
} else {
try {
// 获取锁失败,睡眠50毫秒继续重试(自旋锁)
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
return false;
}
/**
* 释放redis分布式锁
*
* @param key 锁名
* @param owner 锁的拥有者
**/
public void unlock(String key, String owner) {
redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
Long evalResult = connection.eval(LUA_UNLOCK.getBytes(), ReturnType.INTEGER, 1, key.getBytes(), owner.getBytes());
log.debug("unlock eval lua result [{}] ", evalResult);
return evalResult;
}
});
}
/**
* 限制接口在时间范围调用次数
*
* @param key
* @param limit 访问次数
* @param expireTime 时间范围(单位为毫秒)
* @return boolean
*/
@Deprecated
public boolean limit(String key, long limit, long expireTime) {
long result = redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
Long evalResult = connection.eval(LUA_PRECISE_LIMIT.getBytes(), ReturnType.INTEGER, 1, key.getBytes(), String.valueOf(limit).getBytes(), String.valueOf(expireTime).getBytes(), String.valueOf(System.currentTimeMillis()).getBytes());
return evalResult;
}
});
return result == 1;
}
/**
* 限制接口在时间范围调用次数
*
* @param key
* @param limit 访问次数
* @param expireTime 时间范围(单位为毫秒)
* @return boolean
*/
public boolean ipLimit(String key, long limit, long expireTime) {
long result = redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
Long evalResult = connection.eval(IP_RATE_LIMIT.getBytes(), ReturnType.INTEGER, 1, key.getBytes(), String.valueOf(limit).getBytes(), String.valueOf(expireTime).getBytes());
log.debug("ipLimit eval lua result [{}] ", evalResult);
return evalResult;
}
});
return result == 1;
}
}
至此,就是关于rocketmq的简单封装!!!
示例
一般不重要的消息,如日志消息,直接使用单向发送就可以了。封装后使用示例:
生产者端:
第一步:注入RocketMQProductUtils
第二步:调用相关封装方法
第三步:启用定时任务去检查redis,并将失败消息进行重试
以下这些值都可以被自定义
消费者端:
消费消息只需要继承 RocketMQListenerAwareNew 类,并实现 一下方法就行:
示例:
生产者端事务消息
第一步:调用 sendMessageInTransaction()方法,这里需要定义一个事务生产者组
productUtils.sendMessageInTransaction("test_rule_group", "topic_rule", ruleCode, "111TAG", ruleMsg, ruleRequest);
第二步:继承 RocketMQLocalTransactionListenerAware 类, 并指定与上面同样的事务生产者组,再在其中执行自己的业务方法,注意:需要保证自己的业务方法有事务
以上所有代码都经过测试,请放心使用。如果觉得有帮助的话,请点个赞再走,你的鼓励是对我最大的支持!!!