分布式事务设计

分布式事务设计

一、 什么是分布式事务

  1. 一次大的操作由多个小操作组成,这些小操作分布各个独立的服务上,分布式事务需要保证这些小操作要么全部成功,要么全部失败.
  2. 保证不同数据库的数据一致性。
  3. 保证同一个数据库,在多个独立服务上的数据一致性。

二、分布式事务的产生的原因

2.1、数据库分库分表

  1. 当单表的数据超过1亿,这个时候为了查询性能,应该考虑活分库了(活跃,时间),就是将原来的一个数据库变成了多个数据库。这时候,如果一个操作既访问01库,又访问02库,而且要保证数据的一致性,那么就要用到分布式事务。
    在这里插入图片描述

2.2、应用SOA化

  1. SOA化:业务的服务化。
    1. 案例如下
      1. 以前一个单机支撑了整个电商网站,现在拆解分离出了订单中心、用户中心、库存中心,xxx中心等等。每个xxx中心,都有专门的数据库存储信息。这时候如果要同时对订单和库存进行操作,那么就会涉及到订单数据库和库存数据库,为了保证数据一致性,就需要用到分布式事务。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vXBIKLYB-1572784356829)
在这里插入图片描述

三、事务的ACID特性

3.1、原子性(A)

  • 整个事务中的所有操作,要么全部完成,要么全部不做,没有中间状态。
  • 事务在执行中发生错误,所有的操作都会被回滚。

3.2、一致性(C)

  1. 数据一致性:就是正常完成、没有异常的生成数据A,和事务执行成功的数据B,A和B是相同的。
    1. 打个比方说,A账户有500元,B账户有300元,如果在一个事务里A成功转给B 50元,那么不管发生什么,只要事务执行成功了,那么最后A账户一定是450元,B账户一定是350元。

3.3、隔离性(I)

  1. 事务与事务之间不会互相影响,一个事务的中间状态不会被其他事务感知。

3.4、持久性(D)

  1. 将数据持久化到硬盘中。

四、分布式事务解决方案

4.1 基于XA协议的两阶段提交(2PC)

4.1.1、XA协议:
  • 事务管理器:作为全局的调度者,负责各个本地资源的提交和回滚。
  • 本地资源管理器:往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口。
XA实现分布式事务的原理
  1. 准备阶段:协调者(Coordinator)询问参与者事务是否执行成功,参与者发回事务执行结果。
    • 只要参与者有返回no,协调者发送通知让参与者回滚事。
    • 只有参与者全部返回yes,才进入提交阶段

在这里插入图片描述
2. 提交阶段:如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。
在这里插入图片描述

4.1.2、2PC存在的问题
  1. 同步阻塞:
    • 所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。对高并发影响很大
  2. 单点问题:
    • 协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响。特别是在阶段二发生故障,所有参与者会一直等待状态,无法完成其它操作。
  3. 数据不一致:
    • 在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
  4. 仅支持商业数据库
    • XA目前在商业数据库(比如Oracle、DB2)支持的比较理想
    • 在mysql数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换回导致主库与备库数据不一致.
    • 很多nosql也没有支持XA。
  5. 没有完善的容错机制太过保守
    • 任意一个节点失败就会导致整个事务失败,没有完善的容错机制。
4.1.3、2PC存在的问题总结(针对当前项目:数据库oracle,单库,多个服务调用同一数据库)
  1. 不适合高并发情况
  2. 不适合mysql数据库

4.2 消息事务+最终一致性

4.2.1、消息事务:
  1. 基于消息中间件的两阶段提交。
  2. 由本地事务和消息中间件组成。
  3. 将本地事务和发消息放在了一个分布式事务里,保证本地操作成功并且对外发消息成功,否则只要有一个失败,就当做全部失败。
    1. 消息中间件来保证最终一致性。
    2. 本地事务和发消息放在了一个事务里,保证操作满足事务特性
  4. 开源的RocketMQ支持3的特性,具体原理如下:

在这里插入图片描述

4.2.2、步骤
  1. A系统(本地事务)向消息中间件发送一条预备消息(对应1)
    • 如果出错,则整个事务失败,不执行下一步
  2. 消息中间件保存预备消息并返回成功(对应1.1和1.2)
    • 如果出错,则整个事务失败,不执行下一步
  3. A 执行本地事务(对应2)
    • 执行错误,回滚预备消息
    • 预备消息:A系统实现一个消息中间件的回调接口,消息中间件会去不断执行回调接口,检查A本地事务执行是否执行失败,如果失败则回滚预备消息
  4. A发送提交消息给消息中间件(对应3)
    • 如果失败:这时候A的本地事务是成功的,不需要消息中间件要回滚A.
      • 通过回调接口,消息中间件能够检查到A执行成功了,这时候其实不需要A发提交消息了,消息中间件可以自己对消息进行提交,从而完成整个消息事务基于消息中间件的两阶段提交。
4.2.3、试用场景
  1. 试用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。

  2. 原理如下:
    在这里插入图片描述

  3. 步骤

    1. 消息中间件发送消息给B系统(对应3.2)
    2. B系统收到消息,执行本地事务(对应图3.2.1)
      1. 如果执行本地事务失败,消息重投,直到B执行本地事务成功。
      2. 风险:数据一致性
        1. 假设B就是不成功,那么会破会数据一致性。这个时候应该考虑,怎么去修复这种数据一致性
  4. 对消息事务+最终一致性的总结

    1. 数据不是强一致性,而是最终一致性,换来高并发的性能提升
    2. 数据一致性风险还是存在(B事务一直执行不成功),在设计的时候要考虑到这一点。

4.3 补偿事务(TCC)

  1. 是两阶段提交的一个变种

  2. 通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,复杂度也不一样。

    1. 对现有的系统(当前公司),代码的改造变的复杂。维护成本变高。
    2. 这种模式并不能很好地被复用
  3. 提供了一个编程框架,将整个业务逻辑分为三块:Try、Confirm和Cancel三个操作。

    1. 核心思想:针对每个操作(Try),都要注册一个与其对应的确认(Confirm)和补偿(Cancel)操作。
    2. 默认Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
4.3.1 案例(在线下单)
  1. Try阶段:去扣库存
  2. Confirm阶段:更新订单状态
    1. 更新订单失败–>进入Cancel阶段。
  3. Cancel阶段:会去恢复库存。
4.3.2 优缺点
  1. 高并发性能比2PC高
  2. 数据一致性比2PC差
    1. Confirm阶段失败,Cancel阶段也失败,这里会存在数据一致性问题。
4.3.3 建议
  1. 不建议使用。
    1. 代码维护成本很高,不同业务需要多做很多工作。
    2. 数据一致性问题缺陷一直存在
    3. 相比消息事务+最终一致性
      1. 代码维护成功高
      2. 数据一致性没有消息事务+最终一致性的强

4.4 最大努力通知型方案

####4.4.1. 主要用在与第三方系统通讯

  1. 比如:微信或支付宝支付成功后的支付回调接口(支付成功,修改订单状态),就是保证数据的最终一致性。
  2. A服务(本地服务)调用B服务这种简单的两者关系。因为B服务会调用A服务的回调地址,如果一个大的功能中,存在多个服务,那么A服务需要维护的回调地址就很变多,系统代码就会变得复杂,且数据一致性的风险相应的也会提高。

####4.4.2. 举例:A订单服务,B库存服务

  1. A服务调用B服务
    1. B将处理结果返回给A
      1. B执行成功,给MQ发送一条消息
      2. B消费MQ的消息,调用A的回调接口。
        1. 返回成功,结束
        2. 返回失败:继续在最大通知次数调用A的回调接口。

五、分布式事务框架

5.1、ShardingSphere

https://github.com/seata/seata-workshop

1. 两阶段事务-XA
1. 不适合高并发情况
2 Saga柔性事务
1. 本质是TCC,对代码改动太大
3 Seata柔性事务
  1. 需要Seata、Dubbo和Nacos 集成才能使用。

5.2、LCN框架

  1. 需要 eureka和redis。
    https://www.cnblogs.com/sky-chen/archive/2019/01/31/10342207.html

5.3、Fescar

  1. 需要 Dubbo 才能使用。

六、我们选择

  1. 目前的主流框架支持的是微服务项目,非我们这样传统项目,通过http调用第三方接口
  2. 我们只针对一些特殊的业务做分布式事务。
  3. 采用消息事务+最终一致性实现

本来想采纳下面这种设计的,不过后来发现有更简单的方法。
在这里插入图片描述

更简单的方法

在这里插入图片描述

7、设计(零钱系统+业务系统)

  1. 保证本地事务和消息日志在同一事务中。

    1. 让本地事务和本地消息日志在同一个数据库中。在业务代码中,他们在同一个事务里。这样就保证如果本地事务成功,一定有对应的消息日志。
  2. 怎么保证本地事务成功,一定会给MQ发送消息。

    1. 我们对事务进行监控,只有事务提交后,我们才发送消息(保证消息日志和本地事务一致)
    2. 发送消息的时候,我们判断是否发送成功(ACK),成功就更改消息日志的状态为已成功发送(stats=1)。
    3. 定时刷消息日志未发送的消息(stats=0),并将他们发送。
  3. 消息消费端就去消费消息好了。

  4. 对于数据不一致的问题(消费端不消费消息,概率很低),建议人工修复

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>    

9. 测试代码

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值