分布式事务设计
一、 什么是分布式事务
- 一次大的操作由多个小操作组成,这些小操作分布各个独立的服务上,分布式事务需要保证这些小操作要么全部成功,要么全部失败.
- 保证不同数据库的数据一致性。
- 保证同一个数据库,在多个独立服务上的数据一致性。
二、分布式事务的产生的原因
2.1、数据库分库分表
- 当单表的数据超过1亿,这个时候为了查询性能,应该考虑活分库了(活跃,时间),就是将原来的一个数据库变成了多个数据库。这时候,如果一个操作既访问01库,又访问02库,而且要保证数据的一致性,那么就要用到分布式事务。
2.2、应用SOA化
- SOA化:业务的服务化。
- 案例如下
- 以前一个单机支撑了整个电商网站,现在拆解分离出了订单中心、用户中心、库存中心,xxx中心等等。每个xxx中心,都有专门的数据库存储信息。这时候如果要同时对订单和库存进行操作,那么就会涉及到订单数据库和库存数据库,为了保证数据一致性,就需要用到分布式事务。
- 案例如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vXBIKLYB-1572784356829)
三、事务的ACID特性
3.1、原子性(A)
- 整个事务中的所有操作,要么全部完成,要么全部不做,没有中间状态。
- 事务在执行中发生错误,所有的操作都会被回滚。
3.2、一致性(C)
- 数据一致性:就是正常完成、没有异常的生成数据A,和事务执行成功的数据B,A和B是相同的。
- 打个比方说,A账户有500元,B账户有300元,如果在一个事务里A成功转给B 50元,那么不管发生什么,只要事务执行成功了,那么最后A账户一定是450元,B账户一定是350元。
3.3、隔离性(I)
- 事务与事务之间不会互相影响,一个事务的中间状态不会被其他事务感知。
3.4、持久性(D)
- 将数据持久化到硬盘中。
四、分布式事务解决方案
4.1 基于XA协议的两阶段提交(2PC)
4.1.1、XA协议:
- 事务管理器:作为全局的调度者,负责各个本地资源的提交和回滚。
- 本地资源管理器:往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口。
XA实现分布式事务的原理
- 准备阶段:协调者(Coordinator)询问参与者事务是否执行成功,参与者发回事务执行结果。
- 只要参与者有返回no,协调者发送通知让参与者回滚事。
- 只有参与者全部返回yes,才进入提交阶段
2. 提交阶段:如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。
4.1.2、2PC存在的问题
- 同步阻塞:
- 所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。对高并发影响很大
- 单点问题:
- 协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响。特别是在阶段二发生故障,所有参与者会一直等待状态,无法完成其它操作。
- 数据不一致:
- 在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
- 仅支持商业数据库
- XA目前在商业数据库(比如Oracle、DB2)支持的比较理想
- 在mysql数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换回导致主库与备库数据不一致.
- 很多nosql也没有支持XA。
- 没有完善的容错机制太过保守
- 任意一个节点失败就会导致整个事务失败,没有完善的容错机制。
4.1.3、2PC存在的问题总结(针对当前项目:数据库oracle,单库,多个服务调用同一数据库)
- 不适合高并发情况
- 不适合mysql数据库
4.2 消息事务+最终一致性
4.2.1、消息事务:
- 基于消息中间件的两阶段提交。
- 由本地事务和消息中间件组成。
- 将本地事务和发消息放在了一个分布式事务里,保证本地操作成功并且对外发消息成功,否则只要有一个失败,就当做全部失败。
- 消息中间件来保证最终一致性。
- 本地事务和发消息放在了一个事务里,保证操作满足事务特性
- 开源的RocketMQ支持3的特性,具体原理如下:
4.2.2、步骤
- A系统(本地事务)向消息中间件发送一条预备消息(对应1)
- 如果出错,则整个事务失败,不执行下一步
- 消息中间件保存预备消息并返回成功(对应1.1和1.2)
- 如果出错,则整个事务失败,不执行下一步
- A 执行本地事务(对应2)
- 执行错误,回滚预备消息
- 预备消息:A系统实现一个消息中间件的回调接口,消息中间件会去不断执行回调接口,检查A本地事务执行是否执行失败,如果失败则回滚预备消息
- A发送提交消息给消息中间件(对应3)
- 如果失败:这时候A的本地事务是成功的,不需要消息中间件要回滚A.
- 通过回调接口,消息中间件能够检查到A执行成功了,这时候其实不需要A发提交消息了,消息中间件可以自己对消息进行提交,从而完成整个消息事务基于消息中间件的两阶段提交。
- 如果失败:这时候A的本地事务是成功的,不需要消息中间件要回滚A.
4.2.3、试用场景
-
试用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。
-
原理如下:
-
步骤
- 消息中间件发送消息给B系统(对应3.2)
- B系统收到消息,执行本地事务(对应图3.2.1)
- 如果执行本地事务失败,消息重投,直到B执行本地事务成功。
- 风险:数据一致性
- 假设B就是不成功,那么会破会数据一致性。这个时候应该考虑,怎么去修复这种数据一致性
-
对消息事务+最终一致性的总结
- 数据不是强一致性,而是最终一致性,换来高并发的性能提升
- 数据一致性风险还是存在(B事务一直执行不成功),在设计的时候要考虑到这一点。
4.3 补偿事务(TCC)
-
是两阶段提交的一个变种
-
通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,复杂度也不一样。
- 对现有的系统(当前公司),代码的改造变的复杂。维护成本变高。
- 这种模式并不能很好地被复用
-
提供了一个编程框架,将整个业务逻辑分为三块:Try、Confirm和Cancel三个操作。
- 核心思想:针对每个操作(Try),都要注册一个与其对应的确认(Confirm)和补偿(Cancel)操作。
- 默认Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
4.3.1 案例(在线下单)
- Try阶段:去扣库存
- Confirm阶段:更新订单状态
- 更新订单失败–>进入Cancel阶段。
- Cancel阶段:会去恢复库存。
4.3.2 优缺点
- 高并发性能比2PC高
- 数据一致性比2PC差
- Confirm阶段失败,Cancel阶段也失败,这里会存在数据一致性问题。
4.3.3 建议
- 不建议使用。
- 代码维护成本很高,不同业务需要多做很多工作。
- 数据一致性问题缺陷一直存在
- 相比消息事务+最终一致性
- 代码维护成功高
- 数据一致性没有消息事务+最终一致性的强
4.4 最大努力通知型方案
####4.4.1. 主要用在与第三方系统通讯
- 比如:微信或支付宝支付成功后的支付回调接口(支付成功,修改订单状态),就是保证数据的最终一致性。
- A服务(本地服务)调用B服务这种简单的两者关系。因为B服务会调用A服务的回调地址,如果一个大的功能中,存在多个服务,那么A服务需要维护的回调地址就很变多,系统代码就会变得复杂,且数据一致性的风险相应的也会提高。
####4.4.2. 举例:A订单服务,B库存服务
- A服务调用B服务
- B将处理结果返回给A
- B执行成功,给MQ发送一条消息
- B消费MQ的消息,调用A的回调接口。
- 返回成功,结束
- 返回失败:继续在最大通知次数调用A的回调接口。
- B将处理结果返回给A
五、分布式事务框架
5.1、ShardingSphere
https://github.com/seata/seata-workshop
1. 两阶段事务-XA
1. 不适合高并发情况
2 Saga柔性事务
1. 本质是TCC,对代码改动太大
3 Seata柔性事务
- 需要Seata、Dubbo和Nacos 集成才能使用。
5.2、LCN框架
- 需要 eureka和redis。
https://www.cnblogs.com/sky-chen/archive/2019/01/31/10342207.html
5.3、Fescar
- 需要 Dubbo 才能使用。
六、我们选择
- 目前的主流框架支持的是微服务项目,非我们这样传统项目,通过http调用第三方接口
- 我们只针对一些特殊的业务做分布式事务。
- 采用消息事务+最终一致性实现
本来想采纳下面这种设计的,不过后来发现有更简单的方法。
更简单的方法
7、设计(零钱系统+业务系统)
-
保证本地事务和消息日志在同一事务中。
- 让本地事务和本地消息日志在同一个数据库中。在业务代码中,他们在同一个事务里。这样就保证如果本地事务成功,一定有对应的消息日志。
-
怎么保证本地事务成功,一定会给MQ发送消息。
- 我们对事务进行监控,只有事务提交后,我们才发送消息(保证消息日志和本地事务一致)
- 发送消息的时候,我们判断是否发送成功(ACK),成功就更改消息日志的状态为已成功发送(stats=1)。
- 定时刷消息日志未发送的消息(stats=0),并将他们发送。
-
消息消费端就去消费消息好了。
-
对于数据不一致的问题(消费端不消费消息,概率很低),建议人工修复
8、代码设计(零钱系统+业务系统)
/**
* @Author feizhou
* @Description 本地消息定时任务
* @Date 15:29 2019/10/30
* @Param null
* @return
**/
public class LocalMessageLogSchedule extends QuartzJobBean {
@Autowired
private ILocalMessageLogService localMessageLogService;
@Autowired
private LocalMessageLogMessageSender messageSender;
// @Before
public void before() {
if (null == localMessageLogService) {
localMessageLogService = ContextUtil.getApplicationContext().getBean(ILocalMessageLogService.class);
}
}
//定时扫描记录表,将发送状态为0的消息再次发送,甚至可以记录重发次数,必要时人工干预,生产环境中需要单独部署定时任务,保证本地事务完成,消息一定发成功
@Override
protected void executeInternal(JobExecutionContext arg0)
throws JobExecutionException {
// 是否启用定时
String isOpen = ContextUtil.getInitConfig("refresh.local.message.log.isOpen");
//默认开启
if (isOpen != null && "false".equals(isOpen)) {
return;
}
before();
try {
Map<String, Object> queryMap = new HashMap<>();
queryMap.put("f_state", 0);
List<Local_message_log> messages = localMessageLogService.queryAllNotGrroup(queryMap, null);
for (Local_message_log message : messages) {
message.setCurrent_retry_time(message.getCurrent_retry_time() + 1);
if (message.getCurrent_retry_time() <= message.getMax_retry_time()) {
//更新消息的重发次数
localMessageLogService.update(message);
System.out.println("重发消息:" + message.toString());
messageSender.send(LocalMessageLogConstant.LOCAL_MESSAGE_LOG_EXCHANGE, LocalMessageLogConstant.LOCAL_MESSAGE_LOG_ROUTEKEY, message);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* @Author feizhou
* @Description MQ 用户零线配置
* @Date 13:56 2019/10/29
* @Param null
* @return
**/
public interface LocalMessageLogConstant {
/**
* @Author feizhou
* @Description 本地消息交换机名称
* @Date 13:57 2019/10/29
* @Param null
* @return
**/
String LOCAL_MESSAGE_LOG_EXCHANGE = "local.message.log.exchange";
/**
* @Author feizhou
* @Description 本地消息路由键
* @Date 13:57 2019/10/29
* @Param null
* @return
**/
String LOCAL_MESSAGE_LOG_ROUTEKEY = "local.message.log.routeKey";
/**
* @Author feizhou
* @Description 本地消息队列
* @Date 13:57 2019/10/29
* @Param null
* @return
**/
String LOCAL_MESSAGE_LOG_QUEUE = "local.message.log.queue";
/*配置死信*/
/**
* @Author feizhou
* @Description 本地消息死信交换机名称
* @Date 13:57 2019/10/29
* @Param null
* @return
**/
String LOCAL_MESSAGE_LOG_DEAD_EXCHANGE = "local.message.log.dead.exchange";
/**
* @Author feizhou
* @Description 本地消息死信路由键
* @Date 13:57 2019/10/29
* @Param null
* @return
**/
String LOCAL_MESSAGE_LOG_DEAD_ROUTEKEY = "local.message.log.dead.routeKey";
/**
* @Author feizhou
* @Description 本地消息死信队列
* @Date 13:57 2019/10/29
* @Param null
* @return
**/
String LOCAL_MESSAGE_LOG_DEAD_QUEUE = "local.message.log.dead.queue";
}
/**
* @Author feizhou
* @Description 本地消息
* @Date 15:29 2019/10/30
* @Param null
* @return
**/
public interface ILocalMessageLogService extends IBaseService<Local_message_log> {
void saveAndSentMessage(Local_message_log localMessageLog);
}
/**
* @Author feizhou
* @Description 本地消息
* @Date 15:29 2019/10/30
* @Param null
* @return
**/
@Data
public class Local_message_log extends BaseEntity {
private Long id;
private Integer state;// 状态(0没有发送成功,1发送成功),
private String body;// 消息体(json字符串),
private String message_id;// '消息标识'
private Integer type;// '消息类型(0:零钱更新)'
private Integer current_retry_time;//'当前重试次数'
private Integer max_retry_time;// '最大重试次数'
private Long create_id;//'操作者id'
private Date create_time;// '创建时间'
}
/**
* @Author feizhou
* @Description 本地消息定时任务
* @Date 15:29 2019/10/30
* @Param null
* @return
**/
public class LocalMessageLogSchedule extends QuartzJobBean {
@Autowired
private ILocalMessageLogService localMessageLogService;
@Autowired
private LocalMessageLogMessageSender messageSender;
// @Before
public void before() {
if (null == localMessageLogService) {
localMessageLogService = ContextUtil.getApplicationContext().getBean(ILocalMessageLogService.class);
}
}
//定时扫描记录表,将发送状态为0的消息再次发送,甚至可以记录重发次数,必要时人工干预,生产环境中需要单独部署定时任务,保证本地事务完成,消息一定发成功
@Override
protected void executeInternal(JobExecutionContext arg0)
throws JobExecutionException {
// 是否启用定时
String isOpen = ContextUtil.getInitConfig("refresh.local.message.log.isOpen");
//默认开启
if (isOpen != null && "false".equals(isOpen)) {
return;
}
before();
try {
Map<String, Object> queryMap = new HashMap<>();
queryMap.put("f_state", 0);
List<Local_message_log> messages = localMessageLogService.queryAllNotGrroup(queryMap, null);
for (Local_message_log message : messages) {
message.setCurrent_retry_time(message.getCurrent_retry_time() + 1);
if (message.getCurrent_retry_time() <= message.getMax_retry_time()) {
//更新消息的重发次数
localMessageLogService.update(message);
System.out.println("重发消息:" + message.toString());
messageSender.send(LocalMessageLogConstant.LOCAL_MESSAGE_LOG_EXCHANGE, LocalMessageLogConstant.LOCAL_MESSAGE_LOG_ROUTEKEY, message);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* @Author feizhou
* @Description 本地消息
* @Date 15:29 2019/10/30
* @Param null
* @return
**/
@Service
@Transactional
public class LocalMessageLogService extends BaseServiceImpl<Local_message_log> implements ILocalMessageLogService {
@Autowired
private IBaseDao dao;
@Autowired
private ApplicationEventPublisher publisher;
@Autowired
private LocalMessageLogMessageSender messageSender;
@Override
public void saveAndSentMessage(Local_message_log localMessageLog) {
//校验
save(localMessageLog);
publisher.publishEvent(localMessageLog);
}
}
public class DefaultReceiver implements Receiver {
@Override
public void Handler(Local_message_log LocalMessageLog) {
}
}
public interface Receiver {
void Handler(Local_message_log LocalMessageLog);
}
public class UserShopReceiver implements Receiver {
private IEcsAUsersShopService ecsAUsersShopService;
// 构造代码块,每个构造方法执行前,首先执行构造代码块。
{
if (ecsAUsersShopService == null) {
ecsAUsersShopService = (IEcsAUsersShopService) ContextUtil.getBean("ecsAUsersShopService");
}
}
@Override
public void Handler(Local_message_log LocalMessageLog) {
Gson gson = new Gson();//http://blog.csdn.net/u011240877/article/details/52625942
UserShopBean userShopBean=gson.fromJson(LocalMessageLog.getBody(),UserShopBean.class);
ecsAUsersShopService.updateUserMoney(userShopBean.getPayOrderNum(), userShopBean.getChangeDesc(), userShopBean.getChangeType(), userShopBean.getMoneyType(), userShopBean.getUserId(), userShopBean.getCanWithdrawMoney(), userShopBean.getCanNotWithdrawMoney(), userShopBean.getTargetType(), userShopBean.getTargetId(), userShopBean.getCreate());
}
@Getter
@Setter
class UserShopBean {
private String psign;
private Long time;
private String payOrderNum;
private String changeDesc;
private Integer changeType;
private Integer moneyType;
private Long userId;
private Double canWithdrawMoney;
private Double canNotWithdrawMoney;
private Integer targetType;
private Long targetId;
private Long create;
public UserShopBean() {
}
}
}
/**
* @Author feizhou
* @Description 单机配置
* @Date 17:07 2019/10/30
* @Param null
* @return
**/
@Component
public class LocalMessageLogReceiver implements ChannelAwareMessageListener {
protected static Logger logger = Logger.getLogger(LocalMessageLogReceiver.class);
@Override
public void onMessage(Message message, Channel channel) throws Exception {
RedisBaseDao redisDao = RedisUtil.getRedisDao();
String key=null;
try {
String msg = new String(message.getBody());
logger.info(">>>>>>>>>>>本地消息接收:" + msg);
JSONObject jsonObject = JSONObject.fromObject(msg);
Local_message_log localMessage = (Local_message_log) JSONObject.toBean(jsonObject, Local_message_log.class);
key =localMessage.getId().toString();
Boolean isSet = redisDao.setNx(key, key, 90 * 12 * 3600 * 1000L);
//保证幂等性
if (isSet) {
Receiver messageReceiver = MessageReceiverFactory.createMessageReceiver(localMessage.getType());
messageReceiver.Handler(localMessage);
}
//告诉服务器这条消息已消费了,可以在队列删掉;否则消息服务器以为这条消息没处理掉 后续还会在发
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
e.printStackTrace();
redisDao.deleteKeyForSameValue(key,key);
//要求Mq重新派发
// 参数1:消息标识
// 参数2: true:将一次性拒绝所有小于deliveryTag的消息。
// 参数3:被拒绝的是否重新入队列 true:是
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
}
public class MessageReceiverFactory {
public static Receiver createMessageReceiver(int type) {
Receiver receiver = null;
switch (type) {
case 0://零线
receiver = new UserShopReceiver();
break;
default:
receiver = new DefaultReceiver();
break;
}
return receiver;
}
}
public interface Receiver {
void Handler(Local_message_log LocalMessageLog);
}
@Component
public class LocalMessageLogMessageSender implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
protected static Logger logger = Logger.getLogger(LocalMessageLogMessageSender.class);
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private ILocalMessageLogService localMessageLogService;;
public void send(String exchange, String routingKey, Local_message_log localMessageLog) {
// if(1==1){
// throw new RuntimeException("分布式事务异常测试");
// }
// 必须设置为 true,不然当 发送到交换器成功,但是没有匹配的队列,不会触发 ReturnCallback 回调
// 而且 ReturnCallback 比 ConfirmCallback 先回调,意思就是 ReturnCallback 执行完了才会执行 ConfirmCallback
this.rabbitTemplate.setMandatory(true);
// 设置 ConfirmCallback 回调
this.rabbitTemplate.setConfirmCallback(this);
this.rabbitTemplate.setReturnCallback(this);
this.rabbitTemplate.convertAndSend(exchange, routingKey, Json2Util.obj2String(localMessageLog),new CorrelationData(localMessageLog.getId().toString()));
}
/**
* 在消息持久化完成后ack= true,否则false
* 如果发送到交换器都没有成功(比如说删除了交换器),ack 返回值为 false
* 如果发送到交换器成功,但是没有匹配的队列(比如说取消了绑定), ack 返回值为还是 true (这是一个坑,需要注意),目前通过returnedMessage重发来解决
*
* @param correlationData
* @param ack
* @param cause
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (!ack) {
logger.error("零钱发送消息失败, cause = " + cause);
} else {
logger.error("零钱发送消息成功");
//更新
Local_message_log localMessageLog=new Local_message_log();
localMessageLog.setId(Long.valueOf(correlationData.getId()));
localMessageLog.setState(1);
localMessageLogService.update(localMessageLog);
}
}
/**
* 消息找不到对应的Exchange会先触发此方法,MQ 集群,这个问题不会出现,会出现也会自动修复
*
* @param message
* @param replyCode
* @param replyText
* @param exchange
* @param routingKey
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.error("零钱更新发送消息失败:" + replyCode + "," + replyText+",重发");
//重发
rabbitTemplate.send(message);
}
}
@Component
public class LocalMessageLogTransactionEventListener {
protected static Logger logger = Logger.getLogger(LocalMessageLogTransactionEventListener.class);
@Autowired
private LocalMessageLogMessageSender messageSender;
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void beforeCommit(PayloadApplicationEvent<Local_message_log> event) {
logger.info("事务提交前");
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)//事务提交后
public void afterCommit(PayloadApplicationEvent<Local_message_log> event) {
logger.info("事务提交后");
messageSender.send(LocalMessageLogConstant.LOCAL_MESSAGE_LOG_EXCHANGE, LocalMessageLogConstant.LOCAL_MESSAGE_LOG_ROUTEKEY, event.getPayload());
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)//事务完成后(提交或者回滚)
public void afterCompletion(PayloadApplicationEvent<Local_message_log> event) {
logger.info("事务完成后");
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)//事务回滚后
public void afterRollback(PayloadApplicationEvent<Local_message_log> event) {
logger.info("事务回滚后");
}
}
CREATE TABLE `t_local_message_log` (
`f_id` bigint(20) NOT NULL AUTO_INCREMENT,
`f_state` int(1) NOT NULL DEFAULT '0' COMMENT '状态(0没有发送成功,1发送成功)',
`f_body` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '消息体(json字符串)',
`f_message_id` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '消息标识(唯一)',
`f_type` int(20) NOT NULL COMMENT '消息类型(0:零钱更新)',
`f_current_retry_time` int(2) NOT NULL DEFAULT '0' COMMENT '当前重试次数',
`f_max_retry_time` int(2) NOT NULL DEFAULT '3' COMMENT '最大重试次数',
`f_create_id` bigint(20) NOT NULL COMMENT '操作者id',
`f_create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`f_id`) USING BTREE,
UNIQUE KEY `f_message_id` (`f_message_id`),
KEY `normal_state` (`f_state`) USING BTREE,
KEY `normal_message_id` (`f_message_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=193486 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='本地消息日志';
<!-- 定时器设置 -->
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<ref bean="freshLocalMessageTrigger" />
</list>
</property>
</bean>
<!-- 分布式事务 -->
<bean id="freshLocalMessageTrigger"
class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail">
<ref bean="freshLocalMessageSchedule" />
</property>
<property name="cronExpression">
<value>0/30 * * * * ?</value>
</property>
</bean>
<!--刷新本地消息-->
<bean name="freshLocalMessageSchedule"
class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="jobClass"
value="com.lolaage.common.distributedTransaction.localMessageLog.LocalMessageLogSchedule" />
</bean>
rabbitmq.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<context:annotation-config />
<!-- rabbitMQ配置 -->
<bean id="rabbitConnectionFactory"
class="org.springframework.amqp.rabbit.connection.CachingConnectionFactory">
<property name="addresses" value="${mq.host}"/>
<property name="username" value="${mq.user.name}"/>
<property name="password" value="${mq.password}"/>
<property name="channelCacheSize" value="${mq.channel.cache.size}"/>
<!-- 发布确认必须配置在CachingConnectionFactory上 -->
<property name="publisherConfirms" value="${mq.publisher.confirms}"/>
</bean>
<rabbit:admin connection-factory="rabbitConnectionFactory"/>
<!-- 创建rabbitTemplate 消息模板类 -->
<bean id="rabbitTemplate" class="org.springframework.amqp.rabbit.core.RabbitTemplate">
<constructor-arg ref="rabbitConnectionFactory"></constructor-arg>
<!--消息确认回调 -->
<!-- <property name="confirmCallback" ref="confirmCallback"/>
<property name="returnCallback" ref="sendReturnCallback"/>-->
</bean>
<!--消息对象json转换类-->
<bean id="jsonMessageConverter" class="org.springframework.amqp.support.converter.JsonMessageConverter" />
<!--这里只做监控-->
<!--定义queue 接收数据 -->
<rabbit:queue id="local.message.log.queue" name="local.message.log.queue" />
<!-- 队列监听 -->
<rabbit:listener-container>
<!--connection-factory="rabbitConnectionFactory" acknowledge="auto" >-->
<rabbit:listener queues="local.message.log.queue" ref="localMessageLogReceiver" />
</rabbit:listener-container>
<!-- 消息监听器 -->
<bean id="localMessageLogReceiver" class="com.lolaage.common.distributedTransaction.receiver.LocalMessageLogReceiver" />
</beans>