当前位置:   article > 正文

Spring系列第48篇:通过Spring事务实现MQ中的事务消息_spring mq

spring mq

1、本文2个目的

1、讨论一下消息投递的5种方式

2、带你手写代码,实现事务消息的投递

2、讨论一下消息投递的5种方式

2.1、业务场景

电商中有这样的一个场景:商品下单之后,需给用户送积分,订单表和积分表分别在不同的db中,涉及到分布式事务的问题。

我们通过可靠消息来解决这个问题:

  1. 商品下单成功之后送积分的操作,我们使用mq来实现

  2. 商品下单成功之后,投递一条消息到mq,积分系统消费消息,给用户增加积分

我们主要讨论一下,商品下单及投递消息到mq的操作,如何实现?每种方式优缺点?

2.2、方式一

过程
  • step1:开启本地事务

  • step2:生成购物订单

  • step3:投递消息到mq

  • step4:提交本地事务

这种方式是将发送消息放在了事务提交之前。

可能存在的问题
  • step3发生异常:导致step4失败,商品下单失败,直接影响到商品下单业务

  • step4发生异常,其他step成功:商品下单失败,消息投递成功,给用户增加了积分

2.3、方式二

下面我们换种方式,我们将发送消息放到事务之后进行。

过程
  • step1:开启本地事务

  • step2:生成购物订单

  • step3:提交本地事务

  • step4:投递消息到mq

可能会出现的问题

step4发生异常,其他step成功:导致商品下单成功,投递消息失败,用户未增加积分

上面两种是比较常见的做法,也是最容易出错的。

2.4、方式三

  • step1:开启本地事务

  • step2:生成购物订单

  • step3:本地库中插入一条需要发送消息的记录t_msg_record

  • step3:提交本地事务

  • step5:新增一个定时器,轮询t_msg_record,将待发送的记录投递到mq中

这种方式借助了数据库的事务,业务和消息记录作为了一个原子操作,业务成功之后,消息日志必定是存在的。解决了前两种方式遇到的问题。如果我们的业务系统比较单一,可以采用这种方式。

对于微服务化的情况,上面这种方式不是太好,每个服务都需要上面的操作;也不利于扩展。

2.5、方式四

增加一个消息服务消息库,负责消息的落库、将消息发送投递到mq。

  • step1:开启本地事务

  • step2:生成购物订单

  • step3:当前事务库插入一条日志:生成一个唯一的业务id(msg_order_id),将msg_order_id和订单关联起来保存到当前事务所在的库中

  • step4:调用消息服务:携带msg_order_id,将消息先落地入库,此时消息的状态为待发送状态,返回消息id(msg_id)

  • step5:提交本地事务

  • step6:如果上面都成功,调用消息服务,将消息投递到mq中;如果上面有失败的情况,则调用消息服务取消消息的发送

能想到上面这种方式,已经算是有很大进步了,我们继续分析一下可能存在的问题:

  1. 系统中增加了一个消息服务,商品下单操作依赖于该服务,业务对该服务依赖性比较高,当消息服务不可用时,整个业务将不可用。

  2. 若step6失败,消息将处于待发送状态,此时业务方需要提供一个回查接口(通过msg_order_id查询),验证业务是否执行成功;消息服务需新增一个定时任务,对于状态为待发送状态的消息做补偿处理,检查一下业务是否处理成功;从而确定消息是投递还是取消发送

  3. step4依赖于消息服务,如果消息服务性能不佳,会导致当前业务的事务提交时间延长,容易产生死锁,并导致并发性能降低。我们通常是比较忌讳在事务中做远程调用处理的,远程调用的性能和时间往往不可控,会导致当前事务变为一个大事务,从而引发其他故障。

2.6、方式五

在以上方式中,我们继续改进,进而出现了更好的一种方式:

  • step1:生成一个全局唯一业务消息id(bus_msg_id),调用消息服务,携带bus_msg_id,将消息先落地入库,此时消息的状态为待发送状态,返回消息id(msg_id)

  • step2:开启本地事务

  • step3:生成购物订单

  • step4:当前事务库插入一条日志(将step3中的业务和bus_msg_id关联起来)

  • step5:提交本地事务

  • step6:分2种情况:如果上面都成功,调用消息服务,将消息投递到mq中;如果上面有失败的情况,则调用消息服务取消消息的发送

若step6失败,消息将处于待发送状态,此时业务方需要提供一个回查接口(通过bus_msg_id查询),验证业务是否执行成功;

消息服务需新增一个定时任务,对于状态为待发送状态的消息做补偿处理,检查一下业务是否处理成功;从而确定消息是投递还是取消发送。

方式五和方式四对比,比较好的一个地方:将调用消息服务,消息落地操作,放在了事务之外进行,这点小的改进其实算是一个非常好的优化,减少了本地事务的执行时间,从而可以提升并发量,阿里有个消息中间件RocketMQ就支持方式5这种,大家可以去用用。

图片

下面我们通过代码来实现方式4。

3、方式4代码实现

3.1、准备数据库

3张表:

t_user:业务库中的用户表,一会用来模拟用户注册,注册成功之后投递消息。

t_msg_order:消息订单表,这个表放在业务库中,业务操作中若需要发送消息,则在业务操作的事务中同时向t_msg_order表插入一条数据,若业务操作成功,那么t_msg_order表肯定也会成功插入一条数据,发送消息的时候携带上t_msg_order的id,消息服务可以通过这个id去业务库回查t_msg_order中的记录,如果记录存在,则说明业务操作成功了。

t_msg:消息表,所有发送的消息信息放在这个表中,主要字段有:消息内容,msg_order_id:来源于t_msg_order表的id

  1. DROP DATABASE IF EXISTS javacode2018;
  2. CREATE DATABASE if NOT EXISTS javacode2018;
  3. USE javacode2018;
  4. DROP TABLE IF EXISTS t_user;
  5. CREATE TABLE t_user(
  6.   id INT PRIMARY KEY AUTO_INCREMENT,
  7.   name VARCHAR(256NOT NULL DEFAULT '' COMMENT '姓名'
  8. );
  9. DROP TABLE IF EXISTS t_msg;
  10. CREATE TABLE t_msg(
  11.   id INT PRIMARY KEY AUTO_INCREMENT,
  12.   msg VARCHAR(256NOT NULL DEFAULT '' COMMENT '消息内容,可以json格式的数据',
  13.   msg_order_id BIGINT NOT NULL DEFAULT 0 COMMENT '消息订单id',
  14.   status SMALLINT NOT NULL DEFAULT 0 COMMENT '消息状态,0:待投递,1:已发送,2:取消发送'
  15. ) COMMENT '消息表';
  16. DROP TABLE IF EXISTS t_msg_order;
  17. CREATE TABLE t_msg_order(
  18.   id INT PRIMARY KEY AUTO_INCREMENT,
  19.   ref_type BIGINT NOT NULL DEFAULT 0 COMMENT '关联业务类型',
  20.   ref_id VARCHAR(256NOT NULL DEFAULT '' COMMENT '关联业务id(ref_type & ref_id 唯一)'
  21. ) COMMENT '消息订单表,放在业务库中';
  22. alter table t_msg_order add UNIQUE INDEX idx1 (ref_type,ref_id);

3.2、关键java代码

配置类MainConfig11
  1. package com.javacode2018.tx.demo11;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.ComponentScan;
  4. import org.springframework.jdbc.core.JdbcTemplate;
  5. import org.springframework.jdbc.datasource.DataSourceTransactionManager;
  6. import org.springframework.transaction.PlatformTransactionManager;
  7. import org.springframework.transaction.annotation.EnableTransactionManagement;
  8. import javax.sql.DataSource;
  9. /**
  10.  * 公众号:路人甲Java,工作10年的前阿里P7分享Java、算法、数据库方面的技术干货!
  11.  * <a href="http://www.itsoku.com">个人博客</a>
  12.  */
  13. @ComponentScan
  14. @EnableTransactionManagement
  15. public class MainConfig11 {
  16.     @Bean
  17.     public DataSource dataSource() {
  18.         org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
  19.         dataSource.setDriverClassName("com.mysql.jdbc.Driver");
  20.         dataSource.setUrl("jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8");
  21.         dataSource.setUsername("root");
  22.         dataSource.setPassword("root123");
  23.         dataSource.setInitialSize(5);
  24.         return dataSource;
  25.     }
  26.     //定义一个jdbcTemplate
  27.     @Bean
  28.     public JdbcTemplate jdbcTemplate(DataSource dataSource) {
  29.         return new JdbcTemplate(dataSource);
  30.     }
  31.     //定义事务管理器transactionManager
  32.     @Bean
  33.     public PlatformTransactionManager transactionManager(DataSource dataSource) {
  34.         return new DataSourceTransactionManager(dataSource);
  35.     }
  36. }
MsgModel
  1. package com.javacode2018.tx.demo11;
  2. import lombok.*;
  3. /**
  4.  * 公众号:路人甲Java,工作10年的前阿里P7分享Java、算法、数据库方面的技术干货!
  5.  * <a href="http://www.itsoku.com">个人博客</a>
  6.  */
  7. @Getter
  8. @Setter
  9. @Builder
  10. @NoArgsConstructor
  11. @AllArgsConstructor
  12. @ToString
  13. public class MsgModel {
  14.     private Long id;
  15.     //消息内容
  16.     private String msg;
  17.     //消息订单id
  18.     private Long msg_order_id;
  19.     //消息状态,0:待投递,1:已发送,2:取消发送
  20.     private Integer status;
  21. }
MsgOrderModel
  1. package com.javacode2018.tx.demo11;
  2. import lombok.*;
  3. /**
  4.  * 公众号:路人甲Java,工作10年的前阿里P7分享Java、算法、数据库方面的技术干货!
  5.  * <a href="http://www.itsoku.com">个人博客</a>
  6.  */
  7. @Getter
  8. @Setter
  9. @Builder
  10. @NoArgsConstructor
  11. @AllArgsConstructor
  12. public class MsgOrderModel {
  13.     private Long id;
  14.     //关联业务类型
  15.     private Integer ref_type;
  16.     //关联业务id(ref_type & ref_id 唯一)
  17.     private String ref_id;
  18. }
MsgOrderService

提供了对t_msg_order表的一些操作,2个方法,一个用来插入数据,一个用来查询。

  1. package com.javacode2018.tx.demo11;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.jdbc.core.BeanPropertyRowMapper;
  4. import org.springframework.jdbc.core.JdbcTemplate;
  5. import org.springframework.stereotype.Component;
  6. import org.springframework.transaction.annotation.Transactional;
  7. import java.util.List;
  8. import java.util.Objects;
  9. /**
  10.  * 公众号:路人甲Java,工作10年的前阿里P7分享Java、算法、数据库方面的技术干货!
  11.  * <a href="http://www.itsoku.com">个人博客</a>
  12.  */
  13. @Component
  14. public class MsgOrderService {
  15.     @Autowired
  16.     private JdbcTemplate jdbcTemplate;
  17.     /**
  18.      * 插入消息订单
  19.      *
  20.      * @param ref_type
  21.      * @param ref_id
  22.      * @return
  23.      */
  24.     @Transactional
  25.     public MsgOrderModel insert(Integer ref_typeString ref_id) {
  26.         MsgOrderModel msgOrderModel = MsgOrderModel.builder().ref_type(ref_type).ref_id(ref_id).build();
  27.         //插入消息
  28.         this.jdbcTemplate.update("insert into t_msg_order (ref_type,ref_id) values (?,?)",
  29.                 ref_type,
  30.                 ref_id
  31.         );
  32.         //获取消息订单id
  33.         msgOrderModel.setId(this.jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class));
  34.         return msgOrderModel;
  35.     }
  36.     /**
  37.      * 根据消息id获取消息
  38.      *
  39.      * @param id
  40.      * @return
  41.      */
  42.     public MsgOrderModel getById(Long id) {
  43.         List<MsgOrderModel> list = this.jdbcTemplate.query("select * from t_msg_order where id = ? limit 1", new BeanPropertyRowMapper<MsgOrderModel>(MsgOrderModel.class), id);
  44.         return Objects.nonNull(list) && !list.isEmpty() ? list.get(0) : null;
  45.     }
  46. }
MsgService

消息服务,提供了对t_msg表的一些操作以及消息投递的一些方法

方法说明
addMsg添加消息,消息会落库,处于待发送状态
confirmSendMsg确定投递消息,事务成功后可以调用
cancelSendMsg取消投递消息,事务回滚可以调用

代码:

  1. package com.javacode2018.tx.demo11;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.jdbc.core.BeanPropertyRowMapper;
  4. import org.springframework.jdbc.core.JdbcTemplate;
  5. import org.springframework.stereotype.Component;
  6. import org.springframework.transaction.annotation.Propagation;
  7. import org.springframework.transaction.annotation.Transactional;
  8. import java.util.List;
  9. import java.util.Objects;
  10. /**
  11.  * 公众号:路人甲Java,工作10年的前阿里P7分享Java、算法、数据库方面的技术干货!
  12.  * <a href="http://www.itsoku.com">个人博客</a>
  13.  */
  14. @Component
  15. public class MsgService {
  16.     //添加一条消息(独立的事务中执行)
  17.     @Transactional(propagation = Propagation.REQUIRES_NEW)
  18.     public Long addMsg(String msg, Long msg_order_id, boolean isSend) {
  19.         MsgModel msgModel = MsgModel.builder().msg(msg).msg_order_id(msg_order_id).status(0).build();
  20.         //先插入消息
  21.         Long msg_id = this.insert(msgModel).getId();
  22.         if (isSend) {
  23.             //如果需要投递,则调用投递的方法
  24.             this.confirmSendMsg(msg_id);
  25.         }
  26.         return msg_id;
  27.     }
  28.     /**
  29.      * 确认消息投递(不需要事务)
  30.      *
  31.      * @param msg_id 消息id
  32.      */
  33.     @Transactional(propagation = Propagation.NOT_SUPPORTED)
  34.     public void confirmSendMsg(Long msg_id) {
  35.         MsgModel msgModel = this.getById(msg_id);
  36.         //向mq中投递消息
  37.         System.out.println(String.format("投递消息:%s", msgModel));
  38.         //将消息状态置为已投递
  39.         this.updateStatus(msg_id, 1);
  40.     }
  41.     /**
  42.      * 取消消息投递(不需要事务)
  43.      *
  44.      * @param msg_id 消息id
  45.      */
  46.     @Transactional(propagation = Propagation.NOT_SUPPORTED)
  47.     public void cancelSendMsg(Long msg_id) {
  48.         MsgModel msgModel = this.getById(msg_id);
  49.         System.out.println(String.format("取消投递消息:%s", msgModel));
  50.         //将消息状态置为取消投递
  51.         this.updateStatus(msg_id, 2);
  52.     }
  53.     @Autowired
  54.     private JdbcTemplate jdbcTemplate;
  55.     /**
  56.      * 插入消息
  57.      *
  58.      * @param msgModel
  59.      * @return
  60.      */
  61.     private MsgModel insert(MsgModel msgModel) {
  62.         //插入消息
  63.         this.jdbcTemplate.update("insert into t_msg (msg,msg_order_id,status) values (?,?,?)",
  64.                 msgModel.getMsg(),
  65.                 msgModel.getMsg_order_id(),
  66.                 msgModel.getStatus());
  67.         //获取消息id
  68.         msgModel.setId(this.jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class));
  69.         System.out.println("插入消息:" + msgModel);
  70.         return msgModel;
  71.     }
  72.     /**
  73.      * 根据消息id获取消息
  74.      *
  75.      * @param id
  76.      * @return
  77.      */
  78.     private MsgModel getById(Long id) {
  79.         List<MsgModel> list = this.jdbcTemplate.query("select * from t_msg where id = ? limit 1", new BeanPropertyRowMapper<MsgModel>(MsgModel.class), id);
  80.         return Objects.nonNull(list) && !list.isEmpty() ? list.get(0) : null;
  81.     }
  82.     /**
  83.      * 更新消息状态
  84.      *
  85.      * @param id
  86.      * @param status
  87.      */
  88.     private void updateStatus(long id, int status) {
  89.         this.jdbcTemplate.update("update t_msg set status = ? where id = ?"status, id);
  90.     }
  91. }
消息投递器MsgSender

消息投递器,给业务方使用,内部只有一个方法,用来发送消息。

若上下文没有事务,则消息落地之后立即投递;若存在事务,则消息投递分为2步走:消息先落地,事务执行完毕之后再确定是否投递,用到了事务扩展点:TransactionSynchronization,事务执行完毕之后会回调TransactionSynchronization接口中的afterCompletion方法,在这个方法中确定是否投递消息。对事务扩展点TransactionSynchronization不熟悉的建议先看一下这篇文章:Spring系列第47篇:spring事务源码解析

  1. package com.javacode2018.tx.demo11;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.stereotype.Component;
  4. import org.springframework.transaction.support.TransactionSynchronization;
  5. import org.springframework.transaction.support.TransactionSynchronizationAdapter;
  6. import org.springframework.transaction.support.TransactionSynchronizationManager;
  7. /**
  8.  * 公众号:路人甲Java,工作10年的前阿里P7分享Java、算法、数据库方面的技术干货!
  9.  * <a href="http://www.itsoku.com">个人博客</a>
  10.  * 消息发送器,所有使用者调用send方法发送消息
  11.  */
  12. @Component
  13. public class MsgSender {
  14.     @Autowired
  15.     private MsgOrderService msgOrderService;
  16.     @Autowired
  17.     private MsgService msgService;
  18.     //发送消息
  19.     public void send(String msg, int ref_typeString ref_id) {
  20.         MsgOrderModel msgOrderModel = this.msgOrderService.insert(ref_typeref_id);
  21.         Long msg_order_id = msgOrderModel.getId();
  22.         //TransactionSynchronizationManager.isSynchronizationActive 可以用来判断事务同步是否开启了
  23.         boolean isSynchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();
  24.         /**
  25.          * 若事务同步开启了,那么可以在事务同步中添加事务扩展点,则先插入消息,暂不发送,则在事务扩展点中添加回调
  26.          * 事务结束之后会自动回调扩展点TransactionSynchronizationAdapter的afterCompletion()方法
  27.          * 咱们在这个方法中确定是否投递消息
  28.          */
  29.         if (isSynchronizationActive) {
  30.             final Long msg_id = this.msgService.addMsg(msg, msg_order_id, false);
  31.             TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
  32.                 @Override
  33.                 public void afterCompletion(int status) {
  34.                     //代码走到这里时,事务已经完成了(可能是回滚了、或者是提交了)
  35.                     //看一下消息关联的订单是否存在,如果存在,说明事务是成功的,业务是执行成功的,那么投递消息
  36.                     if (msgOrderService.getById(msg_order_id) != null) {
  37.                         System.out.println(String.format("准备投递消息,{msg_id:%s}", msg_id));
  38.                         //事务成功:投递消息
  39.                         msgService.confirmSendMsg(msg_id);
  40.                     } else {
  41.                         System.out.println(String.format("准备取消投递消息,{msg_id:%s}", msg_id));
  42.                         //事务是不:取消投递消息
  43.                         msgService.cancelSendMsg(msg_id);
  44.                     }
  45.                 }
  46.             });
  47.         } else {
  48.             //无事务的,直接插入并投递消息
  49.             this.msgService.addMsg(msg, msg_order_id, true);
  50.         }
  51.     }
  52. }

3.3、测试(3种场景)

3.3.1、场景1:业务成功,消息投递成功
UserService

下面的register方法是有事务的,内部会插入一条用户信息,然后会投递一条消息

  1. package com.javacode2018.tx.demo11;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.jdbc.core.JdbcTemplate;
  4. import org.springframework.stereotype.Component;
  5. import org.springframework.transaction.annotation.Propagation;
  6. import org.springframework.transaction.annotation.Transactional;
  7. /**
  8.  * 公众号:路人甲Java,工作10年的前阿里P7分享Java、算法、数据库方面的技术干货!
  9.  * <a href="http://www.itsoku.com">个人博客</a>
  10.  */
  11. @Component
  12. public class UserService {
  13.     @Autowired
  14.     private JdbcTemplate jdbcTemplate;
  15.     //消息投递器
  16.     @Autowired
  17.     private MsgSender msgSender;
  18.     /**
  19.      * 模拟用户注册成功,顺便发送消息
  20.      */
  21.     @Transactional
  22.     public void register(Long user_id, String user_name) {
  23.         //先插入用户
  24.         this.jdbcTemplate.update("insert into t_user(id,name) VALUES (?,?)", user_id, user_name);
  25.         System.out.println(String.format("用户注册:[user_id:%s,user_name:%s]", user_id, user_name));
  26.         //发送消息
  27.         String msg = String.format("[user_id:%s,user_name:%s]", user_id, user_name);
  28.         //调用投递器的send方法投递消息
  29.         this.msgSender.send(msg, 1, user_id.toString());
  30.     }
  31. }
测试类
  1. package com.javacode2018.tx.demo11;
  2. import org.junit.Before;
  3. import org.junit.Test;
  4. import org.springframework.context.annotation.AnnotationConfigApplicationContext;
  5. import org.springframework.jdbc.core.JdbcTemplate;
  6. /**
  7.  * 公众号:路人甲Java,工作10年的前阿里P7分享Java、算法、数据库方面的技术干货!
  8.  * <a href="http://www.itsoku.com">个人博客</a>
  9.  */
  10. public class Demo11Test {
  11.     private AnnotationConfigApplicationContext context;
  12.     private UserService userService;
  13.     private JdbcTemplate jdbcTemplate;
  14.     @Before
  15.     public void before() {
  16.         this.context = new AnnotationConfigApplicationContext(MainConfig11.class);
  17.         userService = context.getBean(UserService.class);
  18.         this.jdbcTemplate = context.getBean("jdbcTemplate", JdbcTemplate.class);
  19.         jdbcTemplate.update("truncate table t_user");
  20.         jdbcTemplate.update("truncate table t_msg");
  21.         jdbcTemplate.update("truncate table t_msg_order");
  22.     }
  23.     @Test
  24.     public void test1() {
  25.         this.userService.register(1L, "路人");
  26.     }
  27. }
运行输出
  1. 用户注册:[user_id:1,user_name:路人]
  2. 插入消息:MsgModel(id=1, msg=[user_id:1,user_name:路人], msg_order_id=1status=0)
  3. 准备投递消息,{msg_id:1}
  4. 投递消息:MsgModel(id=1, msg=[user_id:1,user_name:路人], msg_order_id=1status=0)
3.3.2、场景2:业务失败,消息取消投递
UserService中添加代码

手动抛出异常,让事务回滚。

  1. /**
  2.  * 模拟用户注册失败,咱们通过弹出异常让事务回滚,结果也会导致消息发送被取消
  3.  *
  4.  * @param user_id
  5.  * @param user_name
  6.  */
  7. @Transactional
  8. public void registerFail(Long user_id, String user_name) {
  9.     this.register(user_id, user_name);
  10.     throw new RuntimeException("故意失败!");
  11. }
Demo11Test添加用例
  1. @Test
  2. public void test2() {
  3.     this.userService.registerFail(1L, "张三");
  4. }
运行输出

弹出了异常,信息比较多,我们截了关键的部分,如下,可以看出事务被回滚了,消息被取消投递了。

  1. 用户注册:[user_id:1,user_name:张三]
  2. 插入消息:MsgModel(id=1, msg=[user_id:1,user_name:张三], msg_order_id=1status=0)
  3. 准备取消投递消息,{msg_id:1}
  4. 取消投递消息:MsgModel(id=1, msg=[user_id:1,user_name:张三], msg_order_id=1status=0)
  5. java.lang.RuntimeException: 故意失败!
  6.  at com.javacode2018.tx.demo11.UserService.registerFail(UserService.java:44)
  7.  at com.javacode2018.tx.demo11.UserService$$FastClassBySpringCGLIB$$5dd21f5c.invoke(<generated>)
3.3.3、嵌套事务

事务发送是跟随当前所在的事务的,当前事务提交了,消息一定会被投递出去,当前事务是不,消息会被取消投递。

下面看嵌套事务的代码

UserService中添加代码

注意下面方法的事务传播行为是:REQUIRES_NEW,当前如果有事务,会重启一个事务。

  1. //事务传播属性是REQUIRES_NEW,会在独立的事务中运行
  2. @Transactional(propagation = Propagation.REQUIRES_NEW)
  3. public void registerRequiresNew(Long user_id, String user_name) {
  4.     this.register(user_id, user_name);
  5. }
添加一个类UserService1
  1. package com.javacode2018.tx.demo11;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.stereotype.Component;
  4. import org.springframework.transaction.annotation.Transactional;
  5. /**
  6.  * 公众号:路人甲Java,工作10年的前阿里P7分享Java、算法、数据库方面的技术干货!
  7.  * <a href="http://www.itsoku.com">个人博客</a>
  8.  */
  9. @Component
  10. public class UserService1 {
  11.     @Autowired
  12.     private UserService userService;
  13.     @Autowired
  14.     private MsgSender msgSender;
  15.     //嵌套事务案例
  16.     @Transactional
  17.     public void nested() {
  18.         this.msgSender.send("消息1"2"1");
  19.         //registerRequiresNew事务传播属性是REQUIRES_NEW:会在一个新事务中运行
  20.         this.userService.registerRequiresNew(1L, "张三");
  21.         //registerFail事务传播属性是默认的,会在当前事务中运行,registerFail弹出异常会导致当前事务回滚
  22.         this.userService.registerFail(2L, "李四");
  23.     }
  24. }

nested 是外围方法,这个方法上有@Transactional,运行的时候会开启一个事务,内部3行代码:

@1:发送消息,会在当前事务中执行

@2:registerRequiresNew事务传播行为是REQUIRES_NEW,所以会重启一个事务

@3:registerFail事务传播行为是默认的REQUIRED,会参与到nested()开启的事务中运行,registerFail方法内部会抛出一个异常,最终会导致外部方法事务回滚。

上面方法需要投递3条消息,而@1和@3投递的消息由于事务回滚会导致消息被回滚,而@2在独立的事务中执行,@2的消息会投递成功,下面来看看执行结果,是不是和分析的一致。

Demo11Test添加用例
  1. @Test
  2. public void test3() {
  3.     UserService1 userService1 = this.context.getBean(UserService1.class);
  4.     userService1.nested();
  5. }
运行输出
  1. 插入消息:MsgModel(id=1, msg=消息1, msg_order_id=1status=0)
  2. 用户注册:[user_id:1,user_name:张三]
  3. 插入消息:MsgModel(id=2, msg=[user_id:1,user_name:张三], msg_order_id=2status=0)
  4. 准备投递消息,{msg_id:2}
  5. 投递消息:MsgModel(id=2, msg=[user_id:1,user_name:张三], msg_order_id=2status=0)
  6. 用户注册:[user_id:2,user_name:李四]
  7. 插入消息:MsgModel(id=3, msg=[user_id:2,user_name:李四], msg_order_id=3status=0)
  8. 准备取消投递消息,{msg_id:1}
  9. 取消投递消息:MsgModel(id=1, msg=消息1, msg_order_id=1status=0)
  10. 准备取消投递消息,{msg_id:3}
  11. 取消投递消息:MsgModel(id=3, msg=[user_id:2,user_name:李四], msg_order_id=3status=0)
  12. java.lang.RuntimeException: 故意失败!
  13.  at com.javacode2018.tx.demo11.UserService.registerFail(UserService.java:44)

大家细看一下结果,和分析的是一致的。

3.4、小结

事务消息分2步走,先落库,此时消息待投递,等到事务执行完毕之后,再确定是否投递,用到的关键技术点是事务扩展接口:TransactionSynchronization,事务执行完毕之后会自动回调接口中的afterCompletion方法。

遗留的一个问题:消息补偿操作

当事务消息刚落地,此时处于待投递状态,系统刚好down机了,此时系统恢复之后,需要有个定时器来处理这种消息,拿着消息中的msg_order_id去业务库查一下订单是否存在,如果存在,则投递消息,否则取消投递,这个留给大家去实现。

4、总结

好了,今天的内容就到此就讲完了,我们一块来总结回顾一下,你需要重点掌握的内容。

1、消息投递的5种方式的推演,要熟练掌握其优缺点

2、方式4中事务消息的代码实现,需要大家掌握

消息服务使用频率挺高的,通常作为系统中的基础服务使用,大家可以尝试一下开发一个独立的消息服务,提供给其他服务使用。

欢迎留言和我分享你的想法,如果有收获,也欢迎你把这篇文章分享给你的朋友,谢谢!

5、案例源码

  1. git地址:
  2. https://gitee.com/javacode2018/spring-series
  3. 本文案例对应源码:
  4.     spring-series\lesson-002-tx\src\main\java\com\javacode2018\tx\demo11

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/766729
推荐阅读
相关标签
  

闽ICP备14008679号