使用SpringBoot+SpringCloud作微服务项目,我最头疼的莫过于事务的控制了,业务逻辑太复杂,不可能把一个接口中的数据库操作都放在一个事务中,服务之间的相互调用,怎么保证事务的一致性?
我看了https://yq.aliyun.com/articles/600584云栖的一篇文章,真的挺好,以至于我想直接接入阿里的FMT模型,不过貌似是收费的,哼!!我辈撸代码,收费呵呵哒(QTM)。
然后就找到了codingapi的官文(http://www.txlcn.org/zh-cn/docs/preface.html)
1.环境
JDK1.8
maven 3.3.9
spring boot 2.2.1.RELEASE
consul
MySQL 5.6
Mybatis
2.TX-LCN原理图,借鉴官网(侵删)
3.TxManager(TM)
tx-manager是事务的管理者,它不需要做任何操作 。
创建spring boot项目,添加以下依赖
com.codingapi.txlcn
txlcn-tm
5.0.2.RELEASE
application.properties中配置如下:
#配置来自codingapi官文
spring.application.name=tx-manager
server.port=7970
# JDBC 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/tx-manager?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root
# 数据库方言
#spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
# 第一次运行可以设置为: create, 为TM创建持久化数据库表
#spring.jpa.hibernate.ddl-auto=validate
# TM监听IP. 默认为 127.0.0.1
tx-lcn.manager.host=127.0.0.1
# TM监听Socket端口. 默认为 ${server.port} - 100
tx-lcn.manager.port=8070
# 心跳检测时间(ms). 默认为 300000
tx-lcn.manager.heart-time=300000
# 分布式事务执行总时间(ms). 默认为36000
tx-lcn.manager.dtx-time=8000
# 参数延迟删除时间单位ms 默认为dtx-time值
tx-lcn.message.netty.attr-delay-time=${tx-lcn.manager.dtx-time}
# 事务处理并发等级. 默认为机器逻辑核心数5倍
tx-lcn.manager.concurrent-level=160
# TM后台登陆密码,默认值为codingapi
tx-lcn.manager.admin-key=codingapi
# 分布式事务锁超时时间 默认为-1,当-1时会用tx-lcn.manager.dtx-time的时间
tx-lcn.manager.dtx-lock-time=${tx-lcn.manager.dtx-time}
# 雪花算法的sequence位长度,默认为12位.
tx-lcn.manager.seq-len=12
# 异常回调开关。开启时请制定ex-url
tx-lcn.manager.ex-url-enabled=false
# 事务异常通知(任何http协议地址。未指定协议时,为TM提供内置功能接口)。默认是邮件通知
#tx-lcn.manager.ex-url=/provider/email-to/***@**.com
#redis
spring.redis.database=0
spring.redis.host=192.168.10.86
spring.redis.password=yunsign
spring.redis.port=6379
# 开启日志,默认为false
tx-lcn.logger.driver-class-name=${spring.datasource.driver-class-name}
tx-lcn.logger.jdbc-url=${spring.datasource.url}
tx-lcn.logger.username=${spring.datasource.username}
tx-lcn.logger.password=${spring.datasource.password}
因为是本地测试环境,tx-manager不需要注册到consul。在启动类添加注解:@EnableTransactionManagerServer
4.事务的参与者
简单做了一个 下单--支付--增加流水 的业务,分别由三个服务组成。
1) service-order(订单服务,事务的发起方)
添加的依赖
com.codingapi.txlcn
txlcn-tc
5.0.2.RELEASE
com.codingapi.txlcn
txlcn-txmsg-netty
5.0.2.RELEASE
application.properties配置,官文上的几个其他配置删掉了,不需要配
spring.application.name=service-order
server.port=5002
server.tomcat.uri-encoding=UTF-8
##日志级别
logging.level.org.springframework.web=debug
logging.level.com.lcn.dao=debug
##数据库
spring.datasource.url=jdbc:mysql://localhost:3306/tx-service-order?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root
##mybatis
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.type-aliases-package=com.lcn.dao.entity
mybatis.configuration.use-generated-keys=true
# 是否启动LCN负载均衡策略(优化选项,开启与否,功能不受影响)
tx-lcn.ribbon.loadbalancer.dtx.enabled=true
# tx-manager 的配置地址,可以指定TM集群中的任何一个或多个地址
# tx-manager 下集群策略,每个TC都会从始至终<断线重连>与TM集群保持集群大小个连接。
# TM方,每有TM进入集群,会找到所有TC并通知其与新TM建立连接。
# TC方,启动时按配置与集群建立连接,成功后,会再与集群协商,查询集群大小并保持与所有TM的连接
tx-lcn.client.manager-address=127.0.0.1:8070
# 调用链长度等级,默认值为3(优化选项。系统中每个请求大致调用链平均长度,估算值。)
tx-lcn.client.chain-level=3
# 开启日志,默认为false
tx-lcn.logger.enabled=true
tx-lcn.logger.driver-class-name=${spring.datasource.driver-class-name}
tx-lcn.logger.jdbc-url=${spring.datasource.url}
tx-lcn.logger.username=${spring.datasource.username}
tx-lcn.logger.password=${spring.datasource.password}
bootstrap.properties配置,基于consul的服务注册与发现
spring.cloud.consul.host=192.168.10.82
spring.cloud.consul.discovery.prefer-ip-address=true
spring.cloud.consul.discovery.instance-id=${spring.application.name}:${spring.cloud.client.ip-address}
spring.cloud.consul.discovery.service-name=${spring.application.name}
spring.cloud.consul.discovery.hostname=${spring.cloud.client.ip-address}
spring.cloud.consul.discovery.port=${server.port}
spring.cloud.consul.discovery.health-check-interval=30s
service-order的项目结构如下:
2)service-account(账户服务),service-pay (支付服务)(事务的参与方)
这两个项目的配置与 service-order 都一样,我这里每个服务都有一个独立的数据库,是为了测试LCN的效果,是不是要分库在这里没区别。
5.CODING
先创建一个待支付的订单,然后拿订单号去支付,服务之间的调用采用 feign 的方式,尝试在支付过程的任何位置抛出异常,看看数据库的结果是否一致。
1)service-order | OrderController.java (LCN模式)
/**
* 支付订单,事务发起方
* @param userId
* @param orderNo
* @param ex 异常标识
* @return
*/
@LcnTransaction
@GetMapping("payorder")
public String payOrder(@RequestParam("userId") Integer userId,
@RequestParam("orderNo") String orderNo,
@RequestParam(value = "ex", required = false) String ex) {
Order order = orderMapper.getByOrderNo(orderNo);
//调用支付
String payResult = payService.pay(orderNo, order.getPrice(), null);
if (Objects.isNull(payResult)) {
throw new IllegalStateException("pay failed");
}
//记录流水
String recordResult = accountFlowService.recordFlow(userId, order.getPrice());
if (Objects.isNull(recordResult)) {
throw new IllegalStateException("record failed");
}
//更新订单
orderMapper.updateOrderPayStatus(order.getId(), 1);
if (Objects.nonNull(ex)) {
throw new IllegalStateException("by exFlag");
}
return payResult + " > " + recordResult + " > " + "ok-service-order";
}
2)service-pay | PayController.java (TXC模式)
// @LcnTransaction(propagation = DTXPropagation.SUPPORTS)
@TxcTransaction(propagation = DTXPropagation.SUPPORTS)
@GetMapping("pay")
public String pay(@RequestParam("orderNo") String orderNo,
@RequestParam("amount") BigDecimal amount,
@RequestParam(value = "ex", required = false) String ex) {
Pay pay = new Pay()
.setPayAmount(amount)
.setOrderNo(orderNo);
payMapper.insert(pay);
return "ok-pay-service";
}
3)service-account | AccountFlowController.java (TCC模式)
TCC模式需要自己写业务执行成功的 commit 和失败的 cancel 方法,这种模式对业务的侵入最大,在业务逻辑非常复杂的时候是很烦的,另外两种不需要。
@Slf4j
@RestController
public class AccountFlowController {
@Autowired
private AccountFlowMapper accountFlowMapper;
private ConcurrentHashMap ids = new ConcurrentHashMap<>();
@GetMapping("recoredflow")
@TccTransaction(propagation = DTXPropagation.SUPPORTS)
@Transactional
public String recordFlow(@RequestParam("userId") Integer userId,
@RequestParam("amount") BigDecimal changeAmount) {
AccountFlow accountFlow = new AccountFlow()
.setFlowDirection(0)
.setChangeAmount(changeAmount)
.setEvent("购买钢笔")
.setUserId(userId)
.setGroupId(TracingContext.tracing().groupId());
accountFlowMapper.insert(accountFlow);
ids.put(TracingContext.tracing().groupId(), accountFlow.getId());
return "ok-service-account";
}
public void confirmRecordFlow(Integer userId, BigDecimal changeAmount) {
log.info("tcc-confirm-{}", TracingContext.tracing().groupId());
ids.remove(TracingContext.tracing().groupId());
}
public void cancelRecordFlow(Integer userId, BigDecimal changeAmount) {
log.info("tcc-cancel-{}", TracingContext.tracing().groupId());
Integer flowId = ids.get(TracingContext.tracing().groupId());
accountFlowMapper.deleteById(flowId);
}
}
6.测试
先启动 TM ,再启动其他三个项目,访问 http://localhost:5002/payorder?orderNo=1578986829457&userId=1&ex=true ,在所有业务都执行完毕的情况下抛出异常,另外两个项目的事务也正常回滚。说明此次的分布式事务LCN模式学习成功!