分布式事务介绍
在分布式系统中实现的事务就是分布式事务, 分布式系统的 CAP 原则是:
一致性
可用性
分区容错性
是分布式事务主要是保证数据的一致性, 主要有三种不同的原则
强一致性
弱一致性
最终一致性
JTA 与 XA
共同点:
- Transaction Manager(事务管理器)
- XA Resource
两阶段提交
Orderservice 监听新订单队列中的消息, 获取之后新增订单, 成功则往新订单缴费队列中写消息, 中间新增订单的过程使用 JTA 事务管理, 当新增失败则事务回滚, 不会往新订单缴费队列中写消息;
再比如 User service 扣费成功后, 往新订单转移票队列写消息, 这时 Ticket service 正在处理中或者处理中发生了失败, 这中间的过程中用户查看自己的余额已经扣费成功, 但票的信息却没有, 此时可以使用事务失败回滚的方式依次回退, 这种叫弱一致性; 又或者可以把处理失败的内容发送至一个错误队列中, 由人工处理等方式解决, 这种叫最终一致性.
Spring JTA 分布式事务实现
可以使用如 JBoss 之类的应用服务器提供的 JTA 事务管理器
可以使用 Atomikos,Bitronix 等库提供的 JTA 事务管理器
不使用 Spring JTA 的分布式事务实现
为什么不使用 JTA?
因为 JTA 采用两阶段提交方式, 第一次是预备阶段, 第二次才是正式提交. 当第一次提交出现错误, 则整个事务出现回滚, 一个事务的时间可能会较长, 因为它要跨越多个数据库多个数据资源的的操作, 所以在性能上可能会造成吞吐量低.
不适用 JTA, 依次提交两事务
- start message transaction
- receive message
- start database transaction
- update database
- commit database transaction
- commit message transaction ## 当这一步出现错误时, 上面的因为已经 commit, 所以不会 rollback
这时候就会出现问题
多个资源的事务同步方法
XA 与最后资源博弈
- start message transaction
- receive message
- start JTA transaction on DB
- update database
- phase-1 commit on DB transaction
- commit message transaction ## 当这一步出现错误时, 上面的因为是 XA 的第一次提交预备状态, 所以可以 rollback
- phase-2 commit on DB transaction ## 当这一步出现错误时, 因为 message 不是 XA 方式, commit 后无法 rollback
但这种相比不使用 JTA, 已经很大程度上避免了事务发生错误的可能性.
共享资源
两个数据源共享同一个底层资源
比如 ActiveMQ 使用 DB 作为底层资源存储
使用数据库的 database transaction Manager 事务管理器来控制事务提交
需要数据源支持指定底层资源存储方式
最大努力一次提交
依次提交事务
可能出错
通过 AOP 或 Listener 实现事务直接的同步
JMS 最大努力一次提交 + 重试
适用于其中一个数据源是 MQ, 并且事务由读 MQ 消息开始
利用 MQ 消息的重试机制
重试的时候需要考虑重复消息
- start message transaction
- receive message
- start database transaction
- update database #数据库操作出错, 消息被放回 MQ 队列, 重试重新触发该方法
- commit database transaction
- commit message transaction
上面这种时候没有问题
- start message transaction
- receive message
- start database transaction
- update database
- commit database transaction
- commit message transaction #提交 MQ 事务出错, 消息放回至 MQ 队列, 重试重新触发该方法
可能存在问题: 会重复数据库操作, 因为 database transaction 不是使用 JTA 事务管理, 所以 database 已经 commit 成功; 如何避免, 需要忽略重发消息, 比如唯一性校验等手段.
链式事务管理
定义一个事务链
多个事务在一个事务管理器里依次提交
可能出错
如何选择 (根据一致性要求)
强一致性事务: JTA(性能最差, 只适用于单个服务内)
弱, 最终一致性事务: 最大努力一次提交, 链式事务 (设计相应的错误处理机制)
如何选择 (根据场景)
MQ-DB: 最大努力一次提交 + 重试
多个 DB: 链式事务管理
多个数据源: 链式事务, 或其他事务同步方式
实例
实例 1-DB-DB
application.properties 中配置了两个数据源
- # 默认的 Datasource 配置
- # spring.datasource.url = jdbc:MySQL://localhost:3307/user
- # spring.datasource.username = root
- # spring.datasource.password = 123456
- # spring.datasource.driverClassName = com.MySQL.jdbc.Driver
- spring.ds_user.url = jdbc:MySQL://localhost:3307/js_user
- spring.ds_user.username = root
- spring.ds_user.password = 123456
- spring.ds_user.driver-class-name = com.MySQL.jdbc.Driver
- spring.ds_order.url = jdbc:MySQL://localhost:3307/js_order
- spring.ds_order.username = root
- spring.ds_order.password = 123456
- spring.ds_order.driver-class-name = com.MySQL.jdbc.Driver
自定义配置类文件
- @Configuration
- public class DBConfiguration{
- @Bean
- @Primary
- @ConfigurationProperties(prefix="spring.ds_user") #设置读取在 properties 文件内容的前缀
- public DataSourceProperties userDataSourceProperties() {
- return new DataSourceProperties();
- }
- @Bean
- @Primary
- public DataSource userDataSource(){
- return userDataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();
- }
- @Bean
- public JdbcTemplate userJdbcTemplate(@Qualifier("userDataSource") DataSource userDataSource){
- return new JdbcTemplate(userDataSource);
- }
- @Bean
- @ConfigurationProperties(prefix="spring.ds_order") #设置读取在 properties 文件内容的前缀
- public DataSourceProperties orderDataSourceProperties() {
- return new DataSourceProperties();
- }
- @Bean
- public DataSource orderDataSource(){
- return userDataSourceProperties().initializeDataSourceBuilder().type(HikariDAtaSource.class).build();
- }
- @Bean
- public JdbcTemplate orderJdbcTemplate(@Qualifier("orderDataSource") DataSource orderDataSource){
- return new JdbcTemplate(orderDataSource);
- }
- }
Spring 注解解释 (@Primary,@Qualifier)
实际调用类
- public class CustomerService{
- @Autowired
- @Qualifier("userJdbcTemplate")
- private jdbcTemplate userJdbcTemplate;
- @Autowired
- @Qualifier("orderJdbcTemplate")
- private jdbcTemplate orderJdbcTemplate;
- private static final String UPDATE_CUSTOMER_SQL;
- private static final String INSERT_ORDER_SQL;
- @Transactional #事务管理注解
- public void createOrder(Order order){
- userJdbcTemplate.update(UPDATE_CUSTOMER_SQL, order)
- if(order.getTitle().contains("error1")){ #模拟异常出现
- throw new RuntimeException("error1")
- }
- orderJdbcTemplate.update(INSERT_ORDER_SQL, order) #没有使用事务, 直接提交
- if(order.getTitle().contains("error2")){ #模拟异常出现
- throw new RuntimeException("error2")
- }
- }
- }
关于上述过程的详细说明:
因为使用了标签 @Transactional 的方式, 使其在一个事务里面执行
也就是同步到 Transaction Manager 上面, 但是这边的同步不是说事务的同步, 只是同步数据库连接的开关
特别说明: @Transactional 如果没有做任何配置的情况下, 则会使用 DBConfiguration 类中 @Primart 注解下的 DataSource, 用它去做 datasource connection
spring DataSourceUtils https://my.oschina.NET/xiaominmin/blog/1597040 源码
spring DataSourceUtils 使用已有的 connection, 只是控制数据库连接的释放, 不是事务.
实例 2-DB-DB. 链式事务管理器
链式事务管理器在 这个库里面
DBConfiguration 类中添加一段
- @Bean
- public PlatformTransactionManager transactionManager(){
- DataSourceTransactionManager userTM = new DataSourceTransactionManager(userDataSource()) #看似方法调用, 实则从 spring 容器中获取
- DataSourceTransactionManager orderTM = new DataSourceTransactionManager(orderDataSource())
- # orderTM.setDataSource(orderDataSource()) 如果使用这种方式则不是从容器中去获取了, 因为 orderTM 不是 spring 容器管理
- ChainedTransactionManager tm = new ChainedTransactionManager(userTM, orderTM) ## order 先执行, user 后执行
- return tm;
- }
链接事务管理器 (Chaining transaction managers)
出现异常是否会有问题呢?
使用 debug 方式模拟运行, 第一个 order 事务提交以后, 第二 user 个事务执行的时候把 MySQL 服务给停掉, 出现如下异常
重启启动 msyql 服务, 程序继续运行, 此时来看数据库 order 表中多了一条记录, 而 user 表没有变化; 第一个 order 事务并没有回滚; 那如果是 rollback 的时候停掉 MySQL 服务, 其实是没有影响的, 因为本身就没有 commit, 执不执行 rollback 本身是没有影响的.
实例 3-JPA-DB. 链式事务管理器
MySQL + MySQL
链式事务: JpaTransactionManager + DataSourceTransactionMananger
不处理重试
基于实例 1 的核心代码继续做修改演示:
实例 4-JMS-DB. 最大努力一次提交
- JMS-DB
- ActiveMQ + MySQL
最大努力一次提交: TransactionAwareConnectionFactoryProxy
分布式系统唯一性
什么是分布式系统 ID?
分布式系统的全局唯一标识
UUID: 生成唯一 ID 的规范
用于唯一标识, 处理重复消息
分布式系统唯一性 ID 生成策略:
数据库自增序列
UUID: 唯一 ID 标准, 128 位, 几种生成方式 (时间 + 版本等方式)
MongDB 的 ObjectID: 时间戳 + 机器 ID + 进程 ID + 序号
Redis 的 INCR 操作, Zookeeper 节点的版本号
使用何种方式?
自增的 ID: 需要考虑安全性, 部署
时间有序: 便于通过 ID 判断创建时间
长度, 是否数字类型: 是否建立索引
分布式系统分布式对象
Redis:Redisson 库: RLock,RMap,RQueue 等对象
Zookeeper:Netflix Curator 库: Lock,Queue 等对象
分布式事务实现模式
消息驱动模式: Message Driven
事件溯源模式: Event Sourcing
TCC 模式: Try-Confirm-Cancel
幂等性
幂等操作: 任意多次执行所产生的影响, 与一次执行的影响相同
方法的幂等性: 使用同样的参数调用一次方法多次, 与调用一次结果相同
接口的幂等性: 接口被重复调用, 结果一致
微服务接口的幂等性
重要性: 经常需要通过重试实现分布式事务的最终一致性
GET 方法不会对系统产生副作用, 具有幂等性
POST,PUT,DELETE 方法的实现需要满足幂等性
Service 方法实现幂等性
- public OrderService{
- Map disMap; # 用于存放已经处理的 id
- @Transactional
- void ticketOrder(BuyTickerDTO dto){
- String uid = createUUID(dto); # 创建并获取数据的唯一 id
- if(!diMap.contains(uuid){ #disMap 还没有处理过这个数据唯一 id, 则进入创建
- Order order = createOrder(dto);
- disMap.append(uid) ## 追加 Map
- }
- }
- userService.charge(dto); #调用 user 微服务
- }
SQL 实现幂等性
- # 通过调节限定, 只有第一次支付的时候才会扣余额, 被重复调用的时候就不会重复扣费用, 通过 paystatus 判断
- UPDATE customer SET deposit = deposit - ${
- value
- }, paystatus = 'PAID' WHERE orderId = ${
- id
- } and paystatus = 'UNPAID'
来源: https://juejin.im/post/5baf66dff265da0ac6695fba