场景
服务端发起订单落库请求后,已成功写入数据库。此时发生网络波动,数据库传回的信息未传送到服务端。服务端超时后再次发起落库请求,产生第二次落库。这种情况不符合幂等性。
幂等性
幂等性(Idempotence)是计算机科学中的一个概念,用于描述一个操作在执行一次和执行多次的结果是相同的。换句话说,如果它的多次执行不会对系统的状态产生额外的影响,即无论执行多少次,系统的最终状态都是一致的,这种情况下,该操作是幂等的。
解决方案
利用防重ID+编程式事务防止二次落库
在数据库中加入带有唯一性约束的bizID
,在库表中已经存在该bizID时会返回重复键异常(DuplicateKeyException
),捕捉到此异常就利用编程式事务进行事务回滚(恢复到二次落库前状态),从而确保幂等
防重ID的生成
需要确保bizID在业务层生成,并且在请求发起前就已经生成并传递到数据库
大厂一般会基于雪花算法开发自己的全局分布式ID生成器,生产全局唯一的业务ID。
幂等性设计
在业务逻辑中,不仅要在数据库层面实现幂等性,还应确保整个服务层的逻辑也是幂等的。例如,在处理订单时,可以考虑在业务逻辑中检查bizID是否已经处理过,而不仅仅是在数据库层面进行检查。
编程式事务
编程式事务(Programmatic Transaction
)是指在代码中显式地管理事务的生命周期,包括开始、提交和回滚。与声明式事务不同,编程式事务为开发者提供了更大的灵活性和精细的控制权。
Spring 中的编程式事务
在 Spring 框架中,编程式事务通常通过 TransactionTemplate
或 PlatformTransactionManager
实现。这些类提供了用于显式管理事务的 API。
以下实例中,用status.setRollbackOnly()
; 就可以回滚编程式事务块(execute
中的业务代码)
实例【通过订单表中的bizID防重复落库】
// 设置分库分表路由
try {
// 以用户id为切分键,通过 dbRouter 设定路由
dbRouter.doRouter(createQuotaOrderAggregate.getUserId());
// 编程式事务
transactionTemplate.execute(status -> {
try{
// 1. 写入订单记录
raffleActivityOrderDao.insert(raffleActivityOrder);
// 2. 更新账户
int count = raffleActivityAccountDao.updateAccountQuota(raffleActivityAccount);
// 3. 账户不存在则创建新账户(分布式部署下可能调用的表中有这个用户,但是活动账户表中还没有写入)
if (0 == count) {
raffleActivityAccountDao.insert(raffleActivityAccount);
log.info("账户不存在,创建新账户");
}
return 1;
} catch (DuplicateKeyException e) {
// 手动回滚
status.setRollbackOnly();
log.error("写入订单记录,唯一索引冲突orderId:{} OutBusinessNo:{}", activityOrderEntity.getOrderId(), activityOrderEntity.getOutBusinessNo());
throw new AppException(ResponseCode.INDEX_DUP.getCode(), ResponseCode.INDEX_DUP.getInfo());
}
});
} finally {
dbRouter.clear();
}