二非事务型MQ的最终一致性事务方案
二 非事务型MQ的最终一致性事务方案
二 非事务型MQ的最终一致性事务方案
2.1 非事务型MQ的方案及流程
对于非事务型MQ,使用该消息中间件实现最终一致性事务的方案,参照第一部分的设计思路:
2.2 开发案例
本文以一个简单的关于user用户的业务,为开发案例,进行展示开发过程。
step1 定义本地的事务消息表
CREATE TABLE `mq_trans_message` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_code` varchar(64) NOT NULL DEFAULT 'system' COMMENT '租户唯一编码,system-系统级别,跨租户共享',
`topic` varchar(128) NOT NULL DEFAULT '' COMMENT 'topic',
`tag` varchar(128) NOT NULL DEFAULT '' COMMENT 'tag',
`message_key` varchar(256) NOT NULL DEFAULT '' COMMENT '消息key',
`message` text NOT NULL COMMENT '消息',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=10000 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='事务消息记录表';
主要信息包括事务消息的id、topic、tag、message-key消息的key、message消息体、create/updateTime消息的创建和更新时间。
step2 本地事务流程
首先创建controller控制器,响应request请求:
(controller层调用service层,service层实现interface接口,并提供@Transactional注解的事务方法)
@RestController
public class HelloController {
@Autowired
private UserService userService;
@GetMapping("/trans/test")
public Boolean transTest() throws Exception {
//调用本地的事务方法
return userService.transMessageSuccess();
}
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> implements UserService {
private static final Logger LOGGER = LoggerFactory.getLogger(UserServiceImpl.class);
@Autowired
private UserMapper userMapper;
@Autowired
private MqTransMessageService mqTransMessageService;
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean save(UserEntity userEntity) {
return super.insert(userEntity);
}
/**…………………………………………………………………………………………service层,提供的本地事务方法…………………………………………………………………………………… */
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean transMessageSuccess() {
//保存用户信息 --1 本地事务操作一:saveUser存入数据库,自身调用,save方法的注解失效,但是会加入当前事务
saveUser();
LOGGER.info("begin send trans message");
//2 本地事务操作二:将事务消息存入本地数据库
mqTransMessageService.transSendMsg(MqConstant.Top.USER_ORDER_TOPIC, MqConstant.Tag.USER_TAG,
"{\"userName\": \"WillJoSuccess\"}");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
LOGGER.error(e.getMessage());
}
LOGGER.info(" end send tran message");
return Boolean.TRUE;
}
当前mqTransMessageService.transSendMsg方法,只是将事务消息存入本地事务信息数据库、并存入MessageQueue.PriorityQueue中(spring启动,while执行本地消息发送到RocketMQ上,减少定时任务的压力)
-----关于事务:此处当前方法没有被事务注解,但是由于是不同类之间被@Transactional的事务方法调用,因此当前方法默认会加入事务中,保证事务的一致性。
@Service
public class MqTransMessageServiceImpl extends
ServiceImpl<MessageMapper, MqTransMessageEntity> implements
MqTransMessageService {
@Autowired
private MessageMapper messageMapper;
private static final int MAX_MESSAGE_NUM = 1000;
//当前方法未用@Transactional注释,但是仍旧会加入存在的事务
@Override
public Boolean transSendMsg(String topic, String tag, String content) {
if (StringUtils.isBlank(topic)) {
throw new IllegalArgumentException("topic 不能为空");
}
if (StringUtils.isBlank(content)) {
throw new IllegalArgumentException("content 不能为空");
}
MqTransMessageEntity msg = new MqTransMessageEntity();
msg.setTopic(topic)
.setTag(tag)
.setMessage(content)
.setCreateTime(new Date());
//将MqTransMessageEntity存入本地数据库表
super.insert(msg);
//将MqTransMessageEntity存入MessageQueue.PriorityQueue中
return MessageQueue.putInPriorityQueue(msg);
}
}
上述过程,完成本地事务的核心步骤,如下:
step3 本地事务消息发送到MQ(优化)
在本地事务执行过程中,由于事务信息不停的加入本地事务消息数据库中,由后台任务定时的发送消息到MQ上,为了减少后台任务的工作量、加快传递本地事务消息,会配置一个自动执行的方法,执行while循环,将本地事务消息发送给MQ。如果没有发送成功的,交给后台定时任务托底。
首先,在项目中自定义一个对象
public class MessageQueue {
/**
* 优先级最高的
*/
public static BlockingQueue<MqTransMessage> priorityQueue = new LinkedBlockingDeque<>();
/**
* 延迟队列
*/
public static DelayQueue<MqTransMessageDelay> delayQueue = new DelayQueue<>();
public static boolean putInPriorityQueue(MqTransMessageEntity mqTransMessageEntity) {
return priorityQueue.add(MqTransMessage.instance(mqTransMessageEntity));
}
public static boolean putInDelayQueue(MqTransMessage transMessage) {
transMessage.setFailCount(transMessage.getFailCount()+1);
return delayQueue.add(MqTransMessageDelay.instance(transMessage));
}
}
该对象是step3的本地事务方法中添加入MessageQueue这一内存的。
然后,定义两个监听器类,监听到在spring项目启动时,自动执行onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) 方法
public class TransMessageRunner implements ApplicationListener<ApplicationReadyEvent> {
private static final Logger logger = LoggerFactory.getLogger(TransMessageRunner.class);
@Autowired
private RocketMqProducerService rocketMqProducerService;
@Autowired
private MqTransMessageService mqTransMessageService;
/**
* 事务最大等待时间,单位为秒
*/
public static final int TRANS_MAX_WAITING_TIME = 30;
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
System.out.println("run message send thread");
new Thread(() -> {
while (true) {
MqTransMessage message = null;
try {
//step3.1 获取优先队列中的message对象
message = MessageQueue.priorityQueue.take();
} catch (InterruptedException e) {
}
if (Objects.isNull(message)) {
continue;
}
SendResult sendResult = null;
try {
String key = MessageFormat.format(MessageLock.LOCK_PREFIX, message.getId());
//key.intern()获取jvm中的唯一对象
synchronized (key.intern()) {
//step3.2 获取本地数据库中的事务消息
// 查询数据库确保是有值
MqTransMessageEntity mqTransMessageEntity = mqTransMessageService.selectById(message.getId());
//step3.3.1 如果本地数据库消息null
if (Objects.isNull(mqTransMessageEntity)) {
// 事务数据库表无值,有三种可能姓 ,一种是事务没结束,一种事务没成功,或者已经被定时任务发送了
long time = System.currentTimeMillis() - message.getCreateTime().getTime();
if(time / 1000 > TRANS_MAX_WAITING_TIME) {
// 超过30秒还是查不到,就直接丢弃了,后面有定时任务兜底
logger.info(" due to over 30 second, discard message for messageId={}", message.getId());
} else {
// 放到延迟队列处理
logger.info(" add message to delayQueue for messageId={}", message.getId());
MessageQueue.putInDelayQueue(message);
}
continue;
}
//step3.3.2 如果本地数据库消息不为null
else {
//查询事务库表有值,然后rocketMQ同步发送信息到broker,获取发送结果sendResult
sendResult = rocketMqProducerService.synSend(message.getTopic(), message.getTag(),
message.getMessage());
if (Objects.nonNull(sendResult) && SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
//执行成功,从数据库删除
mqTransMessageService.deleteById(message.getId());
} else {
//执行失败,继续放入优先队列执行
// 网路抖动等原因,继续放在优先队列进行发送
MessageQueue.priorityQueue.put(message);
}
}
}
} catch (Exception e) {
logger.warn("mq send fail,message={}",e.getMessage(),e);
MessageQueue.putInDelayQueue(message);
}
}
},"transMessage").start();
}
}
上述监听器的逻辑如下:
step3.1 获取优先队列中的message对象
String key = MessageFormat.format(MessageLock.LOCK_PREFIX, message.getId());
//key.intern()获取jvm中的唯一对象
synchronized (key.intern())-------加锁-注意此处加锁逻辑
step3.2 获取本地数据库中的事务消息
step3.3.1 如果本地数据库消息null
// 事务数据库表无值,有三种可能姓 ,一种是事务没结束,一种事务没成功,或者已经被定时任务发送了
case1:超过30秒还是查不到,就直接丢弃了,后面有定时任务兜底
case2:放到延迟队列处理 MessageQueue.putInDelayQueue(message)
step3.3.2 如果本地数据库消息不为null
1 然后rocketMQ同步发送信息到broker
case1:执行成功,从数据库删除
case2:执行失败,继续放入优先队列执行MessageQueue.priorityQueue.put(message)
这里主要是加快本地事务消息传递到MQ,保证本地消息传送的即时性。
然后,还有一个延迟消息的监听器,不断的把延迟消息重新放入优先队列中
public class TransDelayMessageRunner implements ApplicationListener<ApplicationReadyEvent> {
private static final Logger logger = LoggerFactory.getLogger(TransDelayMessageRunner.class);
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
new Thread(() -> {
while (true) {
try {
/** 将延迟队列delayQueue中的信息重新添加入优先队列priorityQueue */
MqTransMessageDelay messageDelay = MessageQueue.delayQueue.take();
logger.info("delay message poll ,message={}", messageDelay);
MessageQueue.priorityQueue.put(messageDelay);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"delayMessage").start();
}
}
注意:
当前两个监听器类,需要添加到spring容器中。本项目中,定制@Enable注解,会在项目启动时,加载这些类。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({
RocketMqProperties.class,
RocketMqConsumerRunner.class,
RocketMqProducerService.class, //包装了RocketMQ的发送消息的服务类
RocketMqProperties.class,
RocketMqFactoryBeanConfig.class,
TransMessageRunner.class, //延迟队列---发送本地事务消息到MQ
TransDelayMessageRunner.class, //优先队列
MonitorQueue.class
})
public @interface EnableRocketMq {
}
step4 后台任务发送事务消息到MQ
@Component
public class MqTransMessageTask {
private static final Logger logger = LoggerFactory.getLogger(MqTransMessageTask.class);
@Autowired
private MqTransMessageService messageService;
@Autowired
private RocketMqProducerService rocketMqProducerService;
/**
* 每次获取消息数量
*/
private static final int MAX_MESSAGE_NUM = 1000;
//spring的定时任务注解,需要再启动类上@EnableScheduling
@Scheduled(fixedDelay = 5 * 1000)
public void sendMessage() {
// logger.info("====开始执行任务=====");
List<MqTransMessageEntity> list = messageService.list(MAX_MESSAGE_NUM);
LinkedBlockingDeque<Long> successIds = new LinkedBlockingDeque<>();
// 如果执行期间宕机,那么这里会导致消息重发,单消费端必须要保证幂等
list.parallelStream().forEach(messageEntity -> {
//消息锁,构建string的key
String key = MessageFormat.format(MessageLock.LOCK_PREFIX, messageEntity.getId());
synchronized (key.intern()) {
SendResult sendResult = rocketMqProducerService
.synSend(messageEntity.getTopic(), messageEntity.getTag(),
messageEntity.getMessage());
if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
successIds.add(messageEntity.getId());
}
}
});
// 发送成功删除
if (!CollectionUtils.isEmpty(successIds)) {
messageService.del(successIds);
}
}
}
这里使用list.parallelStream().forEach的循环操作,对从本地消息数据库中查到的messageList,进行逐个发送。
step5 消费者消费消息
对于消费者,也可以通过ApplicationRunner的run方法,在spring容器启动时候,自动执行
@Slf4j
public class RocketMqConsumerRunner implements ApplicationRunner {
@Autowired
private ApplicationContext context;
@Override
public void run(ApplicationArguments args) throws Exception {
// 需要执行的逻辑代码,当spring容器初始化完成后就会执行该方法。
RocketMqConsumer consumer = context.getBean(RocketMqConsumer.class);
consumer.start();
}
}
当前启动类,也需要通过加入容器,这里仍旧采用自定义注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({
RocketMqProperties.class,
RocketMqConsumerRunner.class,
在消费消息时,需要执行如下的事务方法:
1 查询是否做过当前操作;
2 扣减库存;
3 在本地事务消息表中,记录操作已经做过(redis或者mysql);
4 发送ack;
针对消息的消费,有并发和有序消费,此处以并发消费为例:
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(consumerGroup);
//设置名称服务器地址
consumer.setNamesrvAddr(this.configuration.getNamesrvAddr());
consumer.subscribe(topic, tag); //设置consumer订阅消息的topic和tag
//注册消费回调
consumer.registerMessageListener((MessageListenerConcurrently) (msgList, context) -> {
try {
for (MessageExt msg : msgList) {
MessageListener listener = (MessageListener) entry.getValue();
MqAction action = listener.consume(msg, context);
switch (action) {
case ReconsumeLater:
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
default:
}
}
} catch (Exception e) {
LOGGER.error("消费失败", e);
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
此处,待完善。应该在MessageListenerConcurrently的监听器中,执行本地消费消息的事务逻辑。
public interface MessageListenerConcurrently extends MessageListener {
ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> var1, ConsumeConcurrentlyContext var2);
}