分布式
业务系统如何保证一致性
回答思路:分场景分析
强一致性
案例:
1、酒店管理系统中创建订单需要先修改房态,才能创建订单成功执行
2、订单积分抵扣、优惠券核销
强一致性往往存在于只有前置行为执行成功后才能继续执行的场景,如果乱序则会使数据不一致和业务流程异常,造成超卖或使商家造成损失等。
解决方法:
1、加分布式锁,短时间内只允许一个订单进行
2、分布式事务管理,二阶段提交或补偿事务模式
2PC
二阶段提交
Atomikos
Atomikos 支持XA分布式事务处理,可以帮助开发者解决跨多数据库或资源的事务管理问题,从而实现如2PC这样的分布式事务机制。
1.Maven引入jar
<dependency>
<groupId>com.atomikos</groupId>
<artifactId>atomikosTransactionsEssentials</artifactId>
<version>5.0.6</version>
</dependency>
2.配置事务管理器,在Spring框架中,可以定义一个Manger bean来初始化Atomikos事务管理器
@Bean
public AtomikosTransactionManager atomikosTransactionManager() {
AtomikosTransactionManager atomikosTransactionManager = new AtomikosTransactionManager();
// 设置相关属性,例如最大事务超时时间等
return atomikosTransactionManager;
}
3.配置数据源:确保数据源是Atomikos提供XADataSource,这样才可以参与到分布式事务中
4.在服务类或DAO层启动事务传播行为,通过@Transactional注解开启事务管理,并根据业务需求设置事务的隔离级别、传播行为等属性。
@Service
public class MyService {
@Autowired
private JdbcTemplate jdbcTemplate1;
@Autowired
private JdbcTemplate jdbcTemplate2;
@Transactional
public void doTransaction() {
// 在这里对两个不同的数据库进行操作
jdbcTemplate1.update(...);
jdbcTemplate2.update(...);
}
}
Atomikos只适用于单服务管理多数据源,需要与其他服务治理策略相结合,处理微服务间事务问题。
Bitronix Transaction Manager
<dependency>
<groupId>org.bitronix</groupId>
<artifactId>bitronix-transaction-manager</artifactId>
<version>3.0.1</version>
</dependency>
BTM 与 Atomikos 差异不大,其中Atomikos 社区活跃更高,但在不同的场景需要具体分析决定。
附:XA分布式事务处理指的是XA规范,分布式环境下实现跨多个数据库或资源的原子性事务操作。XA协议的关键组件包括,全局事务管理器,资源管理器。
Seata-AT模式
- 分支事务注册
- SQL解析与拦截
- 一阶段提交
- 全局提交/回滚
- 二阶段提交/回滚
Seata降低了传统2PC协议在准备阶段长时间锁定资源的问题,提高了系统并发性能,对业务代码侵入性较低,只需要简单的配置即可。
Seata-AT 模式,在事务资源器执行数据操作(SQL)后,其它线程或服务可以对这条数据进行再次修改,但资源管理器任然可以发起Undo 操作,这就造成了“幻读”风险。
TCC
Try-Confirm-Cancel 是分布式事务的一种补偿性模式。
TCC模式的基本原理
- Try 阶段
- 尝试执行业务操作,这个阶段不仅实际业务改动,仅保证后续确认操作可以成功执行
- Confirm阶段
- 如果全局事务提交,则确认Try阶段的业务操作,正在执行业务逻辑并修改业务状态。
- Cancel阶段
- 如果全局事务需要会馆,那么取消Try阶段的操作,释放预留的资源,确保嵟恢复到初始状态
Seata
Seata针对不同的框架有对应的jar包,比如Fegin和DUBBO都有对应的实现jar包,使用相对简单。
- 集成
引入Maven配置,在根据项目情况配置nacos或者Zookeeper
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-tcc</artifactId>
<version>1.7.0</version>
</dependency>
- 接口定义与实现
在业务模块中定义Try/Confirm/Cancel三个方法的接口和实现类。
如:订单核销优惠券
@LocalTCC
public interface CouponTccService {
// Try阶段:尝试核销优惠券,预留资源(如检查优惠券有效性、锁定优惠券等)
boolean tryConsumeCoupon(String orderId, String couponId);
// Confirm阶段:确认核销优惠券,执行实际业务操作(如更新优惠券状态为已使用)
boolean confirmConsumeCoupon(String orderId, String couponId);
// Cancel阶段:取消核销优惠券,回滚预留资源(如恢复优惠券到未使用状态)
boolean cancelConsumeCoupon(String orderId, String couponId);
}
- 事务协调
扫描TCC接口,如果使用的是Spring框架,自动扫描识别并注册TCC接口到Seata容器中。Seata TCC 是通过接口方法的命名来区分Try/Confirm/Cancel,如:try/prepare 都是指try
开启全局事务
@GlobalTransactional(timeoutMills = 30000, name = "order-create-transaction")
public void createOrderAndConsumeCoupon(Order order) throws Exception {
// 创建订单...
// ...
// 尝试核销优惠券
boolean tryResult = couponTccService.tryConsumeCoupon(order.getId(), order.getCouponId());
if (tryResult) {
// 其他业务逻辑...
// 确认创建订单和核销优惠券的其他相关操作
// 如果没有异常,全局事务将在方法结束时自动提交
} else {
// 如果尝试阶段失败,由于全局事务的存在,整个操作将会被回滚
throw new RuntimeException("尝试核销优惠券失败");
}
}
@GlobalTransactional 注解来开启全局事务,这个注解会让Seata管理方法内的数据库操作,并根据业务流程来决定最终是提交还是回滚。
- 异常处理与重试
- 捕获异常
Seata通过全局事务管理和资源管理器之间的通信,以及对本地数据库的操作的代理,可以捕获到分布式事务执行过程中发生的各种异常。 - 分支事务状态管理
当一个分支事务在Try/Confirm/Cancel阶段发生异常时,Seata会将该分支事务的状态更新为需要重试的状态,并存储到TC中,等待后续重试。 - 重试策略
Seata允许用户配置重试次数和重试时间。当分支事务因网络问题、短暂的服务不可用等原因失败后,Seata会根据这些配置进行自动重试。 - 回滚策略
在Confirm或Cancel阶段如果遇到异常导致无法提交或回滚,Seata同样会重试。 - 超时处理
对于整个全局事务或分支事务,Seata支持设置超时时间,一旦超时,Seata会主动触发事务回滚流程,避免长时间锁定资源影响性能。 - 事务上下文管理
Seata会再每个服务调用链路中传播全局事务ID,XID。确保即使在一部调用或跨进程调用的情况下,也能确保正确识别和管理事务。 - 异常通知
Seata通常会提供异常日志和报警功能,以便开发者及时发现和处理异常。
2PC和TCC的差异
- 设计思路
- 2PC
- 将多数据库事务分为两个阶段,准备和提交。准备阶段会在各个数据库开启事务,并向协调者反馈是否准备好提交。提交阶段,协调者会根据参与者反馈决定提交或回滚事务。
- TCC
- 一种业务补偿型的分布式事务方案,强调应用层控制事务边界。每个服务都需要提供三个接口Try/Confirm/Cancel,事务管理器会根据需要调用这些方法。
- 2PC
- 优点
- 2PC
- 简单易理解
- 保证了数据强一致性
- TCC
- 可以灵活地根据业务场景定制资源管理策略,有利于提高系统性能和可扩展性。
- 资源锁定时间少,适合与高并发环境,通过业务逻辑的设计避免长时间锁表。
- 提供了一种更细粒度的事务控制机制。
- 2PC
弱一致性
案例:向第三方异步发放优惠券
第三方系统无法做到尽可能通知。
解决方法:重试。超时中心,
生成失败日志,记录失败内容、原因、时间等要素,在失败后通过重试策略进行重新发放,双方要确认好幂等。
最终一致性
无法理解更新所有副本的数据以达到一致状态,但在一段时间后,系统会通过数据复制、补偿等方式使得所有副本数据达成一致。
案例:支付后,因为网络问题未同步支付结果回订单系统。
Seata Saga模式
核心思想:
- 将一个长事务拆分成一系列短的本地事务,每个子事务都是一个原子操作
- 这些原子操作按照一定的顺序执行,并且每个操作都对应一个补偿操作。补偿操作的作用是撤销或者修正前面已经成功执行的操作,保证数据一致性。
- Saga模式通常不支持事务的ACID特性中的隔离性,但可以提供最终一致性。
事件驱动
状态机
使用方式
1.Maven引入Seata
<!-- 引入 Spring Cloud Alibaba Seata 相关依赖,使用 Seata 实现分布式事务,并实现对其的自动配置 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>${spring.cloud.alibaba.version}</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>${seata.version}</version>
</dependency>
2.各微服务实现业务代码
简单的只写一个订单案例
@RestController
@RequestMapping("order")
public class OrderController {
@Resource
private OrderService orderService;
@RequestMapping("/createOrder")
public Boolean createOrder(@RequestParam("orderId") Long orderId,
@RequestParam("userId") Long userId,
@RequestParam("productId") Long productId,
@RequestParam("amount") Integer amount,
@RequestParam("count") Integer count) throws Exception {
return orderService.createOrder(orderId, userId, productId, amount, count);
}
@RequestMapping("/revokeOrder")
public Boolean revokeOrder(@RequestParam("orderId") Long orderId) throws Exception {
return orderService.revokeOrder(orderId);
}
}
@Service
public class OrderServiceImpl implements OrderService {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
OrderDao orderDao;
@Transactional(rollbackFor = Exception.class)
@Override
public Boolean createOrder(Long orderId, Long userId, Long productId, Integer amount, Integer count) throws Exception {
OrderDTO orderDTO = new OrderDTO(orderId, userId, productId, count, amount);
logger.info("[createOrder] 开始创建订单: {}", orderDTO.toString());
logger.info("[createOrder] XID: {}", RootContext.getXID());
int result = orderDao.createOrder(orderDTO);
if(result == 0){
logger.warn("[createOrder] 创建订单 {} 失败", orderDTO.toString());
return false;
}
logger.info("[createOrder] 保存订单成功: {}", orderDTO.getId());
return true;
}
@Transactional(rollbackFor = Exception.class)
@Override
public Boolean revokeOrder(Long orderId) throws Exception {
logger.info("[revokeOrder] 开始撤销订单, orderId: {}", orderId);
logger.info("[revokeOrder] XID: {}", RootContext.getXID());
int result = orderDao.revokeOrder(orderId);
if(result == 0){
logger.warn("[revokeOrder] 撤销订单 {} 失败",orderId);
return false;
}
logger.info("[revokeOrder] 撤销订单成功: {}", orderId);
return true;
}
}
3.配置Config
- ThreadPoolExecutor
- 线程池,如果是串行化执行,则可以不需要
- 通过线程池可以有效的管理并发执行任务,并根据需要进行调度
- DbStateMachineConfig
- 配置数据库资源、Seata 应用编号、Seata 事务组编号
- 配置存放 Saga事务状态图(Json文件)的目录
- StateMachineEngine
- 分布式事务状态机的核心引擎,负责解析和执行事务状态机的状态机模型。
- StateMachineEngineHolder
- 单例持有类,确保整个应用上下文中只存在一个全局唯一的StateMachineEngine实例
4.初始化
- io.seata.saga.engine.impl.DefaultStateMachineConfig#init
- 初始化方法
- io.seata.saga.engine.repo.impl.StateMachineRepositoryImpl#registryByResources
- 读取在Config配置的json文件目录
- 解析JSON文件
- io.seata.saga.engine.repo.impl.StateMachineRepositoryImpl#registryStateMachine
- 比对新旧数据
- 重新插入
5.服务调度
开启订单创建事件
StateMachineInstance instance = stateMachineEngine.start("BusinessOrder", null, businessParam);
(1)初始化并验证Saga事务状态
(2)加载和准备执行的业务流程模型
通过Feign 从nacos中获取对应的服务信息,需要实现实际服务API代理对象
@FeignClient(value = "saga-order-service", configuration = {FeignErrorDecoder.class})
@RequestMapping("/order")
public interface OrderService {
@RequestMapping("/createOrder")
Boolean createOrder(@RequestParam("orderId") Long orderId,
@RequestParam("userId") Long userId,
@RequestParam("productId") Long productId,
@RequestParam("amount") Integer amount,
@RequestParam("count") Integer count) throws Exception;
@RequestMapping("/revokeOrder")
Boolean revokeOrder(@RequestParam("orderId") Long orderId) throws Exception;
}
(3)异步地执行这些任务,并根据执行结果更新Saga事务状态。
AsyncEventBus
@Override
public boolean offer(ProcessContext context) throws FrameworkException {
List<EventConsumer> eventConsumers = getEventConsumers(context.getClass());
if (CollectionUtils.isEmpty(eventConsumers)) {
if (LOGGER.isWarnEnabled()) {
LOGGER.warn("cannot find event handler by class: " + context.getClass());
}
return false;
}
for (EventConsumer eventConsumer : eventConsumers) {
threadPoolExecutor.execute(() -> eventConsumer.process(context));
}
return true;
}
(4)等待结果返回
5.补偿
DirectEventBus
按序将已经执行的任务执行补偿方法。