幂等性 VS 分布式锁:分布式系统一致性的两大护法 —— 从原理到实战的深度剖析
在分布式系统的世界里,数据一致性是开发者必须跨越的鸿沟。当多个请求并发访问共享资源时,如何保证操作结果的准确性和一致性?幂等性和分布式锁作为解决这类问题的两大核心技术,常常被开发者提及和使用。但它们之间究竟有何区别?又存在怎样的联系?在实际开发中该如何选择和运用?本文将带你深入探讨这些问题,从底层原理到实战案例,全方位解析幂等性与分布式锁的奥秘。
一、拨开迷雾:理解幂等性
1.1 幂等性的定义
幂等性(Idempotence)是一个从数学领域引入到计算机科学的概念。在数学中,幂等性指的是某些操作或函数,无论应用多少次,其结果都与应用一次相同。例如,数学中的绝对值函数 | x | 就是幂等的,因为 | |x| | = |x|。
在计算机科学中,幂等性指的是同一个操作,无论执行多少次,所产生的影响都是相同的。也就是说,对于同一个请求,无论重复执行多少次,系统的状态都和执行一次时的状态一致。
1.2 为什么需要幂等性?
在分布式系统中,网络延迟、服务超时、节点故障等问题时有发生,为了保证系统的可用性,我们通常会实现重试机制。然而,重试机制可能导致同一个请求被多次执行,如果操作不具备幂等性,就可能产生错误的结果。
例如:
- 用户在支付时因网络波动点击了多次支付按钮
- 服务调用超时后,调用方自动重试
- 消息队列中的消息被重复消费
- 分布式事务中的补偿机制触发多次执行
这些场景下,如果操作不具备幂等性,可能会导致重复支付、库存超额扣减、订单重复创建等严重问题。
1.3 幂等性的分类
根据实现方式和特性,我们可以将幂等性分为以下几类:
天然幂等操作:某些操作本身就具有幂等性,不需要额外处理。例如:
- 查询操作(SELECT):无论执行多少次,都不会改变系统状态
- 删除操作(DELETE):删除一个不存在的资源,多次执行与一次执行效果相同
- 更新操作(UPDATE):如果是基于固定值的更新(如 UPDATE user SET status=1 WHERE id=1),而不是基于当前值的更新(如 UPDATE user SET score=score+10 WHERE id=1),则具有幂等性
人为实现的幂等操作:对于不具备天然幂等性的操作,需要通过人为设计使其具有幂等性。常见的实现方式包括:
- 基于唯一标识的幂等设计
- 基于状态机的幂等设计
- 基于版本号的幂等设计
1.4 幂等性的实现方案
1.4.1 基于唯一标识(Token)的幂等设计
这是最常用的幂等性实现方案之一,其核心思想是:
- 客户端请求前先向服务端申请一个唯一的令牌(Token)
- 客户端携带该令牌发起请求
- 服务端验证令牌的有效性,执行相应操作,并将令牌标记为已使用
- 后续携带相同令牌的请求都会被拒绝或忽略
流程图如下:
1.4.2 基于状态机的幂等设计
很多业务场景中,数据都存在明确的状态流转,例如订单状态会经历 "创建中→已支付→已发货→已完成" 等状态。基于状态机的幂等设计利用了状态流转的不可逆性,确保同一操作在不同状态下的执行结果是可预期的。
例如,对于已支付的订单,再次收到支付请求时,系统可以直接忽略该请求,因为订单状态已经过了可支付的阶段。
1.4.3 基于版本号(乐观锁)的幂等设计
这种方案通过为数据添加版本号字段,实现对并发操作的控制:
- 每次查询数据时,同时获取当前版本号
- 更新数据时,检查版本号是否与查询时一致
- 如果一致,则更新数据并递增版本号
- 如果不一致,则说明数据已被其他请求修改,当前请求失败
这种方式既能保证幂等性,又能有效处理并发问题。
二、分布式锁:分布式系统的并发守护者
2.1 分布式锁的定义
分布式锁是一种在分布式系统中用于控制多个进程或服务对共享资源访问的机制。它能够保证在分布式环境下,同一时间只有一个进程或服务能够执行特定的代码块或操作特定的资源。
与单机环境下的锁(如 Java 中的 synchronized 关键字或 ReentrantLock)不同,分布式锁需要在多个独立的进程或服务之间协调,因此其实现更为复杂。
2.2 为什么需要分布式锁?
在单机系统中,我们可以使用本地锁来解决并发问题。但在分布式系统中,应用部署在多个节点上,本地锁只能控制单个节点上的并发,无法阻止其他节点对共享资源的访问。
例如,在一个分布式电商系统中,多个服务节点都可能处理库存扣减操作。如果没有分布式锁,可能会出现超卖现象:
- 商品 A 的库存为 10
- 两个并发请求同时查询到库存为 10
- 两个请求都扣减 1 个库存,最终库存变为 8
- 但实际上应该只允许扣减 2 个,库存变为 8 是正确的,这个例子不太恰当
更恰当的例子:
- 商品 A 的库存为 1
- 两个并发请求同时查询到库存为 1
- 两个请求都扣减 1 个库存,最终库存变为 - 1,出现超卖
分布式锁可以解决这类问题,确保同一时间只有一个请求能够执行库存扣减操作。
2.3 分布式锁的核心特性
一个可靠的分布式锁应该具备以下特性:
- 互斥性:在任何时刻,只有一个客户端能够持有锁
- 安全性:不会出现死锁,即使持有锁的客户端崩溃或网络中断,锁也能被释放
- 可用性:锁服务应该具有高可用性,不会成为系统瓶颈
- 一致性:无论客户端连接到哪个节点,都能获得一致的锁状态
- 可重入性:同一个客户端可以多次获取同一把锁而不会导致死锁
2.4 分布式锁的实现方案
2.4.1 基于 Redis 的分布式锁
Redis 由于其高性能和单线程特性,成为实现分布式锁的热门选择。基于 Redis 的分布式锁通常使用 SET 命令的扩展参数来实现:
SET key value NX PX expireTime
其中:
- NX:只有当 key 不存在时才设置成功
- PX expireTime:设置 key 的过期时间,单位为毫秒
这种方式可以保证锁的互斥性和自动释放(避免死锁)。
2.4.2 基于 ZooKeeper 的分布式锁
ZooKeeper 是一个分布式协调服务,其节点特性非常适合实现分布式锁:
- 创建临时有序节点
- 判断当前节点是否为最小节点,如果是则获得锁
- 如果不是,则监听前一个节点的变化
- 当前一个节点被删除时,重新判断自己是否为最小节点
ZooKeeper 的分布式锁实现具有天然的可重入性和可靠性,但性能相对 Redis 略低。
2.4.3 基于数据库的分布式锁
利用数据库的唯一约束特性也可以实现分布式锁:
- 创建一张锁表,包含锁名称、持有锁的客户端标识、过期时间等字段
- 获取锁时,向表中插入一条记录,利用唯一约束保证只有一个客户端能插入成功
- 释放锁时,删除对应的记录
- 为了避免死锁,需要定期清理过期的锁记录
数据库分布式锁实现简单,但性能较差,适合并发量不高的场景。
三、幂等性与分布式锁的区别
虽然幂等性和分布式锁都用于解决分布式系统中的并发问题,但它们在本质上有很大的区别:
3.1 解决的核心问题不同
幂等性解决的核心问题是重复执行的问题,即如何保证同一个操作执行多次与执行一次的效果相同。它关注的是操作结果的一致性,而不限制并发执行。
分布式锁解决的核心问题是并发执行的问题,即如何保证同一时间只有一个操作能够执行。它关注的是执行过程的互斥性,通过限制并发来保证结果的正确性。
3.2 实现思路不同
幂等性的实现思路是允许并发执行,但保证重复执行的结果一致。它通常通过标识去重、状态控制等方式实现,不阻止并发操作,而是让重复操作变得 "无害"。
分布式锁的实现思路是阻止并发执行,保证同一时间只有一个操作执行。它通过互斥机制确保操作的串行化执行,从根源上避免了并发冲突。
3.3 适用场景不同
幂等性适用于以下场景:
- 可能发生重复请求的场景(如网络重试、用户重复提交)
- 消息队列中的消息可能被重复消费的场景
- 需要实现最终一致性的分布式事务场景
分布式锁适用于以下场景:
- 对共享资源进行互斥操作的场景(如库存扣减、计数器增减)
- 需要保证操作原子性的场景
- 不允许并发执行的临界区代码
3.4 性能影响不同
幂等性设计通常对系统性能影响较小,因为它允许并发执行,只是在处理重复请求时做一些额外的判断和处理。
分布式锁由于引入了锁竞争和等待机制,可能会对系统性能产生较大影响,特别是在高并发场景下,可能会导致请求排队等待,降低系统吞吐量。
3.5 容错性不同
幂等性设计具有较好的容错性,即使在某些异常情况下(如服务崩溃、网络中断),只要保证重复执行的结果一致,系统就能最终恢复到正确状态。
分布式锁的容错性较差,一旦锁服务出现问题(如 Redis 节点崩溃、ZooKeeper 集群异常),可能会导致整个系统出现死锁或无法获取锁的情况,影响服务可用性。
四、幂等性与分布式锁的联系
尽管幂等性和分布式锁有很多区别,但它们之间也存在密切的联系:
4.1 共同目标:保证系统数据一致性
无论是幂等性还是分布式锁,它们的最终目标都是保证分布式系统的数据一致性。幂等性通过保证重复操作的安全性来实现这一目标,而分布式锁通过控制并发来实现这一目标。
4.2 互补关系:常需结合使用
在很多实际场景中,幂等性和分布式锁需要结合使用才能更好地解决问题。例如:
- 在库存扣减场景中,我们既需要使用分布式锁防止超卖,又需要保证扣减操作的幂等性,以应对锁释放后可能的重试请求
- 在分布式事务场景中,我们需要基于幂等性设计补偿机制,同时可能需要分布式锁来保证补偿操作的安全性
4.3 协同工作:构建可靠系统
幂等性和分布式锁可以协同工作,构建更加可靠的分布式系统:
- 分布式锁保证了操作的互斥性,减少了并发冲突
- 幂等性保证了即使在锁机制失效或操作被重复执行的情况下,系统仍然能够保持数据一致性
五、实战案例分析
5.1 案例一:支付系统中的幂等性设计
在支付系统中,用户可能会因为网络问题重复提交支付请求,或者支付服务在超时后重试,这就要求支付操作必须具备幂等性。
5.1.1 技术选型
- Spring Boot 3.2.0
- MyBatis-Plus 3.5.5
- Redis 7.2.3
- MySQL 8.0.35
- Lombok 1.18.30
- Fastjson2 2.0.32
- SpringDoc OpenAPI 2.1.0(Swagger3)
5.1.2 数据库设计
首先,我们需要设计订单表和支付记录表:
-- 订单表
CREATE TABLE `order_info` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单ID',`order_no` varchar(64) NOT NULL COMMENT '订单编号',`user_id` bigint NOT NULL COMMENT '用户ID',`amount` decimal(10,2) NOT NULL COMMENT '订单金额',`status` tinyint NOT NULL COMMENT '订单状态:0-创建中,1-已支付,2-已取消',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_order_no` (`order_no`),KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单信息表';-- 支付记录表
CREATE TABLE `payment_record` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '记录ID',`payment_no` varchar(64) NOT NULL COMMENT '支付编号',`order_no` varchar(64) NOT NULL COMMENT '订单编号',`user_id` bigint NOT NULL COMMENT '用户ID',`amount` decimal(10,2) NOT NULL COMMENT '支付金额',`payment_status` tinyint NOT NULL COMMENT '支付状态:0-处理中,1-成功,2-失败',`payment_time` datetime DEFAULT NULL COMMENT '支付时间',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_payment_no` (`payment_no`),KEY `idx_order_no` (`order_no`),KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付记录表';-- 幂等性Token表
CREATE TABLE `idempotent_token` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',`token` varchar(64) NOT NULL COMMENT '幂等性Token',`user_id` bigint NOT NULL COMMENT '用户ID',`status` tinyint NOT NULL COMMENT '状态:0-未使用,1-已使用',`expire_time` datetime NOT NULL COMMENT '过期时间',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_token` (`token`),KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='幂等性Token表';
5.1.3 核心代码实现
首先,我们需要创建相关的实体类:
/*** 订单信息实体类* @author ken*/
@Data
@TableName("order_info")
@ApiModel(value = "OrderInfo对象", description = "订单信息表")
public class OrderInfo {@TableId(type = IdType.AUTO)private Long id;@ApiModelProperty(value = "订单编号")@NotBlank(message = "订单编号不能为空")private String orderNo;@ApiModelProperty(value = "用户ID")@NotNull(message = "用户ID不能为空")private Long userId;@ApiModelProperty(value = "订单金额")@NotNull(message = "订单金额不能为空")private BigDecimal amount;@ApiModelProperty(value = "订单状态:0-创建中,1-已支付,2-已取消")private Integer status;@ApiModelProperty(value = "创建时间")@TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;@ApiModelProperty(value = "更新时间")@TableField(fill = FieldFill.INSERT_UPDATE)private LocalDateTime updateTime;
}/*** 支付记录实体类* @author ken*/
@Data
@TableName("payment_record")
@ApiModel(value = "PaymentRecord对象", description = "支付记录表")
public class PaymentRecord {@TableId(type = IdType.AUTO)private Long id;@ApiModelProperty(value = "支付编号")@NotBlank(message = "支付编号不能为空")private String paymentNo;@ApiModelProperty(value = "订单编号")@NotBlank(message = "订单编号不能为空")private String orderNo;@ApiModelProperty(value = "用户ID")@NotNull(message = "用户ID不能为空")private Long userId;@ApiModelProperty(value = "支付金额")@NotNull(message = "支付金额不能为空")private BigDecimal amount;@ApiModelProperty(value = "支付状态:0-处理中,1-成功,2-失败")private Integer paymentStatus;@ApiModelProperty(value = "支付时间")private LocalDateTime paymentTime;@ApiModelProperty(value = "创建时间")@TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;@ApiModelProperty(value = "更新时间")@TableField(fill = FieldFill.INSERT_UPDATE)private LocalDateTime updateTime;
}/*** 幂等性Token实体类* @author ken*/
@Data
@TableName("idempotent_token")
@ApiModel(value = "IdempotentToken对象", description = "幂等性Token表")
public class IdempotentToken {@TableId(type = IdType.AUTO)private Long id;@ApiModelProperty(value = "幂等性Token")@NotBlank(message = "Token不能为空")private String token;@ApiModelProperty(value = "用户ID")@NotNull(message = "用户ID不能为空")private Long userId;@ApiModelProperty(value = "状态:0-未使用,1-已使用")private Integer status;@ApiModelProperty(value = "过期时间")@NotNull(message = "过期时间不能为空")private LocalDateTime expireTime;@ApiModelProperty(value = "创建时间")@TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;@ApiModelProperty(value = "更新时间")@TableField(fill = FieldFill.INSERT_UPDATE)private LocalDateTime updateTime;
}
接下来,创建 Mapper 接口:
/*** 订单信息Mapper* @author ken*/
public interface OrderInfoMapper extends BaseMapper<OrderInfo> {/*** 根据订单编号查询订单* @param orderNo 订单编号* @return 订单信息*/OrderInfo selectByOrderNo(@Param("orderNo") String orderNo);/*** 更新订单状态* @param orderNo 订单编号* @param oldStatus 旧状态* @param newStatus 新状态* @return 影响行数*/int updateOrderStatus(@Param("orderNo") String orderNo, @Param("oldStatus") Integer oldStatus, @Param("newStatus") Integer newStatus);
}/*** 支付记录Mapper* @author ken*/
public interface PaymentRecordMapper extends BaseMapper<PaymentRecord> {/*** 根据支付编号查询支付记录* @param paymentNo 支付编号* @return 支付记录*/PaymentRecord selectByPaymentNo(@Param("paymentNo") String paymentNo);/*** 根据订单编号查询支付记录* @param orderNo 订单编号* @return 支付记录列表*/List<PaymentRecord> selectByOrderNo(@Param("orderNo") String orderNo);
}/*** 幂等性TokenMapper* @author ken*/
public interface IdempotentTokenMapper extends BaseMapper<IdempotentToken> {/*** 根据Token查询记录* @param token Token值* @return Token记录*/IdempotentToken selectByToken(@Param("token") String token);/*** 标记Token为已使用* @param token Token值* @param userId 用户ID* @return 影响行数*/int markTokenUsed(@Param("token") String token, @Param("userId") Long userId);
}
然后,创建 Service 接口和实现类:
/*** 幂等性Token服务* @author ken*/
public interface IdempotentTokenService {/*** 生成幂等性Token* @param userId 用户ID* @return Token值*/String generateToken(Long userId);/*** 验证并使用Token* @param token Token值* @param userId 用户ID* @return 验证结果,true-验证通过且已标记为使用,false-验证失败*/boolean validateAndUseToken(String token, Long userId);
}/*** 幂等性Token服务实现* @author ken*/
@Service
@Slf4j
public class IdempotentTokenServiceImpl implements IdempotentTokenService {@Autowiredprivate IdempotentTokenMapper idempotentTokenMapper;@Overridepublic String generateToken(Long userId) {// 生成UUID作为TokenString token = UUID.randomUUID().toString().replaceAll("-", "");// 设置Token过期时间为30分钟LocalDateTime expireTime = LocalDateTime.now().plusMinutes(30);// 保存Token到数据库IdempotentToken idempotentToken = new IdempotentToken();idempotentToken.setToken(token);idempotentToken.setUserId(userId);idempotentToken.setStatus(0); // 0-未使用idempotentToken.setExpireTime(expireTime);int rows = idempotentTokenMapper.insert(idempotentToken);if (rows <= 0) {log.error("生成幂等性Token失败,userId: {}", userId);throw new BusinessException("生成Token失败,请重试");}log.info("生成幂等性Token成功,userId: {}, token: {}", userId, token);return token;}@Overridepublic boolean validateAndUseToken(String token, Long userId) {// 验证参数if (StringUtils.isEmpty(token)) {log.warn("Token为空,userId: {}", userId);return false;}// 查询TokenIdempotentToken idempotentToken = idempotentTokenMapper.selectByToken(token);if (ObjectUtils.isEmpty(idempotentToken)) {log.warn("Token不存在,userId: {}, token: {}", userId, token);return false;}// 验证用户ID是否匹配if (!idempotentToken.getUserId().equals(userId)) {log.warn("Token与用户不匹配,userId: {}, token: {}, tokenUserId: {}", userId, token, idempotentToken.getUserId());return false;}// 验证Token是否已使用if (idempotentToken.getStatus() == 1) {log.warn("Token已使用,userId: {}, token: {}", userId, token);return false;}// 验证Token是否已过期if (LocalDateTime.now().isAfter(idempotentToken.getExpireTime())) {log.warn("Token已过期,userId: {}, token: {}", userId, token);return false;}// 标记Token为已使用int rows = idempotentTokenMapper.markTokenUsed(token, userId);if (rows <= 0) {log.warn("标记Token为已使用失败,可能已被其他请求使用,userId: {}, token: {}", userId, token);return false;}log.info("Token验证通过并标记为已使用,userId: {}, token: {}", userId, token);return true;}
}/*** 支付服务* @author ken*/
public interface PaymentService {/*** 处理支付* @param paymentRequest 支付请求参数* @return 支付结果*/PaymentResult processPayment(PaymentRequest paymentRequest);
}/*** 支付服务实现* @author ken*/
@Service
@Slf4j
public class PaymentServiceImpl implements PaymentService {@Autowiredprivate PaymentRecordMapper paymentRecordMapper;@Autowiredprivate OrderInfoMapper orderInfoMapper;@Autowiredprivate IdempotentTokenService idempotentTokenService;@Autowiredprivate TransactionTemplate transactionTemplate;@Override@Transactional(rollbackFor = Exception.class)public PaymentResult processPayment(PaymentRequest paymentRequest) {// 参数验证validatePaymentRequest(paymentRequest);// 1. 验证幂等性Tokenboolean tokenValid = idempotentTokenService.validateAndUseToken(paymentRequest.getToken(), paymentRequest.getUserId());if (!tokenValid) {log.warn("支付请求Token验证失败,request: {}", JSON.toJSONString(paymentRequest));return PaymentResult.builder().success(false).message("支付请求已处理或无效").build();}// 2. 查询订单信息OrderInfo orderInfo = orderInfoMapper.selectByOrderNo(paymentRequest.getOrderNo());if (ObjectUtils.isEmpty(orderInfo)) {log.error("订单不存在,orderNo: {}", paymentRequest.getOrderNo());throw new BusinessException("订单不存在");}// 3. 验证订单状态(状态机幂等性控制)if (orderInfo.getStatus() != 0) { // 0-创建中log.warn("订单状态不允许支付,orderNo: {}, status: {}", paymentRequest.getOrderNo(), orderInfo.getStatus());return PaymentResult.builder().success(false).message("订单状态不允许支付").orderNo(paymentRequest.getOrderNo()).build();}// 4. 验证支付金额if (!orderInfo.getAmount().equals(paymentRequest.getAmount())) {log.error("支付金额与订单金额不符,orderNo: {}, orderAmount: {}, payAmount: {}",paymentRequest.getOrderNo(), orderInfo.getAmount(), paymentRequest.getAmount());throw new BusinessException("支付金额与订单金额不符");}// 5. 调用第三方支付接口(此处简化)String paymentNo = callThirdPartyPayment(paymentRequest, orderInfo);// 6. 更新订单状态和创建支付记录(使用事务保证原子性)return transactionTemplate.execute(status -> {try {// 6.1 更新订单状态为已支付int orderRows = orderInfoMapper.updateOrderStatus(paymentRequest.getOrderNo(), 0, 1); // 0-创建中 -> 1-已支付if (orderRows <= 0) {log.error("更新订单状态失败,可能已被其他请求处理,orderNo: {}", paymentRequest.getOrderNo());status.setRollbackOnly();return PaymentResult.builder().success(false).message("支付失败,请重试").orderNo(paymentRequest.getOrderNo()).build();}// 6.2 创建支付记录PaymentRecord paymentRecord = new PaymentRecord();paymentRecord.setPaymentNo(paymentNo);paymentRecord.setOrderNo(paymentRequest.getOrderNo());paymentRecord.setUserId(paymentRequest.getUserId());paymentRecord.setAmount(paymentRequest.getAmount());paymentRecord.setPaymentStatus(1); // 1-成功paymentRecord.setPaymentTime(LocalDateTime.now());int payRows = paymentRecordMapper.insert(paymentRecord);if (payRows <= 0) {log.error("创建支付记录失败,orderNo: {}, paymentNo: {}", paymentRequest.getOrderNo(), paymentNo);status.setRollbackOnly();return PaymentResult.builder().success(false).message("支付失败,请重试").orderNo(paymentRequest.getOrderNo()).build();}log.info("支付成功,orderNo: {}, paymentNo: {}", paymentRequest.getOrderNo(), paymentNo);return PaymentResult.builder().success(true).message("支付成功").orderNo(paymentRequest.getOrderNo()).paymentNo(paymentNo).amount(paymentRequest.getAmount()).paymentTime(LocalDateTime.now()).build();} catch (Exception e) {log.error("支付处理异常,orderNo: {}", paymentRequest.getOrderNo(), e);status.setRollbackOnly();return PaymentResult.builder().success(false).message("支付失败,请重试").orderNo(paymentRequest.getOrderNo()).build();}});}/*** 验证支付请求参数* @param paymentRequest 支付请求*/private void validatePaymentRequest(PaymentRequest paymentRequest) {if (ObjectUtils.isEmpty(paymentRequest)) {throw new BusinessException("支付请求不能为空");}StringUtils.hasText(paymentRequest.getOrderNo(), "订单编号不能为空");if (ObjectUtils.isEmpty(paymentRequest.getUserId())) {throw new BusinessException("用户ID不能为空");}if (ObjectUtils.isEmpty(paymentRequest.getAmount()) || paymentRequest.getAmount().compareTo(BigDecimal.ZERO) <= 0) {throw new BusinessException("支付金额必须大于0");}StringUtils.hasText(paymentRequest.getToken(), "Token不能为空");}/*** 调用第三方支付接口* @param paymentRequest 支付请求* @param orderInfo 订单信息* @return 支付编号*/private String callThirdPartyPayment(PaymentRequest paymentRequest, OrderInfo orderInfo) {// 模拟调用第三方支付接口log.info("调用第三方支付接口,orderNo: {}, amount: {}", paymentRequest.getOrderNo(), paymentRequest.getAmount());// 生成支付编号return "PAY" + System.currentTimeMillis() + RandomUtils.nextInt(1000, 9999);}
}
最后,创建 Controller:
/*** 支付控制器* @author ken*/
@RestController
@RequestMapping("/api/v1/payments")
@ApiModel(value = "PaymentController", description = "支付相关接口")
@Slf4j
public class PaymentController {@Autowiredprivate PaymentService paymentService;@Autowiredprivate IdempotentTokenService idempotentTokenService;/*** 获取支付用的幂等性Token* @param userId 用户ID* @return Token信息*/@GetMapping("/token")@ApiOperation(value = "获取支付用的幂等性Token", notes = "用于保证支付操作的幂等性")@ApiResponses({@ApiResponse(responseCode = "200", description = "成功"),@ApiResponse(responseCode = "500", description = "服务器错误")})public Result<String> getPaymentToken(@ApiParam(value = "用户ID", required = true) @RequestParam Long userId) {try {String token = idempotentTokenService.generateToken(userId);return Result.success(token);} catch (Exception e) {log.error("获取支付Token失败,userId: {}", userId, e);return Result.fail("获取Token失败:" + e.getMessage());}}/*** 处理支付请求* @param paymentRequest 支付请求参数* @return 支付结果*/@PostMapping@ApiOperation(value = "处理支付请求", notes = "提交支付请求并处理")@ApiResponses({@ApiResponse(responseCode = "200", description = "成功"),@ApiResponse(responseCode = "400", description = "参数错误"),@ApiResponse(responseCode = "500", description = "服务器错误")})public Result<PaymentResult> processPayment(@ApiParam(value = "支付请求参数", required = true) @RequestBody @Valid PaymentRequest paymentRequest) {try {PaymentResult result = paymentService.processPayment(paymentRequest);return Result.success(result);} catch (BusinessException e) {log.warn("支付请求处理失败,request: {}, message: {}", JSON.toJSONString(paymentRequest), e.getMessage());return Result.fail(e.getMessage());} catch (Exception e) {log.error("支付请求处理异常,request: {}", JSON.toJSONString(paymentRequest), e);return Result.fail("支付处理异常,请稍后重试");}}
}
5.1.4 实现说明
在这个支付系统的案例中,我们通过以下方式保证了支付操作的幂等性:
基于 Token 的幂等设计:
- 客户端在发起支付前,先获取一个唯一的 Token
- 支付请求必须携带该 Token
- 服务端验证 Token 的有效性,并在处理成功后标记为已使用
- 重复的支付请求会因为 Token 已被使用而被拒绝
基于状态机的幂等设计:
- 订单状态从 "创建中" 到 "已支付" 的流转是单向的
- 对于已支付的订单,再次收到支付请求会被直接拒绝
基于数据库约束的幂等设计:
- 订单编号和支付编号都设置了唯一约束
- 防止重复创建订单或支付记录
通过这些机制的组合,我们确保了即使支付请求被重复提交,也不会导致重复支付的问题。
5.2 案例二:库存系统中的分布式锁应用
在电商系统中,库存扣减是一个典型的并发场景,如果处理不当,很容易出现超卖问题。分布式锁是解决这类问题的有效手段。
5.2.1 技术选型
- 与案例一相同,额外增加 Redisson 作为 Redis 分布式锁的实现
<!-- Redisson -->
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.25.0</version>
</dependency>
5.2.2 数据库设计
-- 商品表
CREATE TABLE `product` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品ID',`product_code` varchar(64) NOT NULL COMMENT '商品编码',`product_name` varchar(255) NOT NULL COMMENT '商品名称',`price` decimal(10,2) NOT NULL COMMENT '商品价格',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_product_code` (`product_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';-- 库存表
CREATE TABLE `inventory` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '库存ID',`product_id` bigint NOT NULL COMMENT '商品ID',`product_code` varchar(64) NOT NULL COMMENT '商品编码',`stock_quantity` int NOT NULL DEFAULT 0 COMMENT '库存数量',`locked_quantity` int NOT NULL DEFAULT 0 COMMENT '锁定数量',`version` int NOT NULL DEFAULT 0 COMMENT '版本号,用于乐观锁',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_product_id` (`product_id`),KEY `idx_product_code` (`product_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表';-- 库存操作记录表
CREATE TABLE `inventory_operation_log` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志ID',`product_id` bigint NOT NULL COMMENT '商品ID',`product_code` varchar(64) NOT NULL COMMENT '商品编码',`operation_type` tinyint NOT NULL COMMENT '操作类型:1-扣减,2-增加,3-锁定,4-解锁',`quantity` int NOT NULL COMMENT '操作数量',`before_quantity` int NOT NULL COMMENT '操作前数量',`after_quantity` int NOT NULL COMMENT '操作后数量',`operator` varchar(64) NOT NULL COMMENT '操作人',`operation_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',`remark` varchar(512) DEFAULT NULL COMMENT '备注',PRIMARY KEY (`id`),KEY `idx_product_id` (`product_id`),KEY `idx_product_code` (`product_code`),KEY `idx_operation_time` (`operation_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存操作记录表';
5.2.3 核心代码实现
首先,创建相关的实体类:
/*** 商品实体类* @author ken*/
@Data
@TableName("product")
@ApiModel(value = "Product对象", description = "商品表")
public class Product {@TableId(type = IdType.AUTO)private Long id;@ApiModelProperty(value = "商品编码")@NotBlank(message = "商品编码不能为空")private String productCode;@ApiModelProperty(value = "商品名称")@NotBlank(message = "商品名称不能为空")private String productName;@ApiModelProperty(value = "商品价格")@NotNull(message = "商品价格不能为空")private BigDecimal price;@ApiModelProperty(value = "创建时间")@TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;@ApiModelProperty(value = "更新时间")@TableField(fill = FieldFill.INSERT_UPDATE)private LocalDateTime updateTime;
}/*** 库存实体类* @author ken*/
@Data
@TableName("inventory")
@ApiModel(value = "Inventory对象", description = "库存表")
public class Inventory {@TableId(type = IdType.AUTO)private Long id;@ApiModelProperty(value = "商品ID")@NotNull(message = "商品ID不能为空")private Long productId;@ApiModelProperty(value = "商品编码")@NotBlank(message = "商品编码不能为空")private String productCode;@ApiModelProperty(value = "库存数量")private Integer stockQuantity;@ApiModelProperty(value = "锁定数量")private Integer lockedQuantity;@ApiModelProperty(value = "版本号,用于乐观锁")@Versionprivate Integer version;@ApiModelProperty(value = "创建时间")@TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;@ApiModelProperty(value = "更新时间")@TableField(fill = FieldFill.INSERT_UPDATE)private LocalDateTime updateTime;
}/*** 库存操作日志实体类* @author ken*/
@Data
@TableName("inventory_operation_log")
@ApiModel(value = "InventoryOperationLog对象", description = "库存操作记录表")
public class InventoryOperationLog {@TableId(type = IdType.AUTO)private Long id;@ApiModelProperty(value = "商品ID")@NotNull(message = "商品ID不能为空")private Long productId;@ApiModelProperty(value = "商品编码")@NotBlank(message = "商品编码不能为空")private String productCode;@ApiModelProperty(value = "操作类型:1-扣减,2-增加,3-锁定,4-解锁")@NotNull(message = "操作类型不能为空")private Integer operationType;@ApiModelProperty(value = "操作数量")@NotNull(message = "操作数量不能为空")private Integer quantity;@ApiModelProperty(value = "操作前数量")@NotNull(message = "操作前数量不能为空")private Integer beforeQuantity;@ApiModelProperty(value = "操作后数量")@NotNull(message = "操作后数量不能为空")private Integer afterQuantity;@ApiModelProperty(value = "操作人")@NotBlank(message = "操作人不能为空")private String operator;@ApiModelProperty(value = "操作时间")@TableField(fill = FieldFill.INSERT)private LocalDateTime operationTime;@ApiModelProperty(value = "备注")private String remark;
}
接下来,创建分布式锁工具类:
/*** 分布式锁工具类* @author ken*/
@Component
@Slf4j
public class DistributedLockUtil {@Autowiredprivate RedissonClient redissonClient;/*** 获取分布式锁* @param lockKey 锁的键* @param waitTime 等待时间(秒)* @param leaseTime 锁的持有时间(秒)* @return 锁对象,如果获取失败则为null*/public RLock getLock(String lockKey, long waitTime, long leaseTime) {if (StringUtils.isEmpty(lockKey)) {log.warn("锁的键不能为空");return null;}RLock lock = redissonClient.getLock(lockKey);try {// 尝试获取锁boolean locked = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);if (locked) {log.debug("获取分布式锁成功,lockKey: {}", lockKey);return lock;} else {log.warn("获取分布式锁失败,lockKey: {}", lockKey);return null;}} catch (InterruptedException e) {log.error("获取分布式锁被中断,lockKey: {}", lockKey, e);Thread.currentThread().interrupt();return null;}}/*** 释放分布式锁* @param lock 锁对象* @param lockKey 锁的键(用于日志)*/public void releaseLock(RLock lock, String lockKey) {if (ObjectUtils.isEmpty(lock)) {return;}try {// 只有持有锁的线程才能释放锁if (lock.isHeldByCurrentThread()) {lock.unlock();log.debug("释放分布式锁成功,lockKey: {}", lockKey);}} catch (Exception e) {log.error("释放分布式锁失败,lockKey: {}", lockKey, e);}}
}
然后,创建 Service 接口和实现类:
/*** 库存服务* @author ken*/
public interface InventoryService {/*** 扣减库存* @param deductRequest 库存扣减请求* @return 扣减结果*/InventoryResult deductInventory(InventoryDeductRequest deductRequest);
}/*** 库存服务实现* @author ken*/
@Service
@Slf4j
public class InventoryServiceImpl implements InventoryService {@Autowiredprivate InventoryMapper inventoryMapper;@Autowiredprivate InventoryOperationLogMapper operationLogMapper;@Autowiredprivate DistributedLockUtil distributedLockUtil;@Autowiredprivate TransactionTemplate transactionTemplate;/*** 库存扣减的分布式锁前缀*/private static final String INVENTORY_DEDUCT_LOCK_PREFIX = "inventory:deduct:";@Overridepublic InventoryResult deductInventory(InventoryDeductRequest deductRequest) {// 参数验证validateDeductRequest(deductRequest);String productCode = deductRequest.getProductCode();int quantity = deductRequest.getQuantity();String operator = deductRequest.getOperator();// 构建锁的键String lockKey = INVENTORY_DEDUCT_LOCK_PREFIX + productCode;// 获取分布式锁,最多等待3秒,持有锁10秒RLock lock = distributedLockUtil.getLock(lockKey, 3, 10);if (ObjectUtils.isEmpty(lock)) {log.error("获取分布式锁失败,无法进行库存扣减,productCode: {}", productCode);return InventoryResult.builder().success(false).message("系统繁忙,请稍后重试").productCode(productCode).build();}try {// 执行库存扣减操作(使用事务保证原子性)return transactionTemplate.execute(status -> {try {// 1. 查询库存信息Inventory inventory = inventoryMapper.selectByProductCode(productCode);if (ObjectUtils.isEmpty(inventory)) {log.error("商品库存不存在,productCode: {}", productCode);status.setRollbackOnly();return InventoryResult.builder().success(false).message("商品不存在").productCode(productCode).build();}// 2. 检查库存是否充足if (inventory.getStockQuantity() < quantity) {log.error("库存不足,productCode: {}, 可用库存: {}, 请求扣减: {}",productCode, inventory.getStockQuantity(), quantity);status.setRollbackOnly();return InventoryResult.builder().success(false).message("库存不足").productCode(productCode).currentStock(inventory.getStockQuantity()).build();}// 3. 记录操作前的库存数量int beforeQuantity = inventory.getStockQuantity();// 4. 扣减库存(使用乐观锁防止并发问题)int rows = inventoryMapper.deductInventory(productCode, quantity, inventory.getVersion());if (rows <= 0) {log.error("库存扣减失败,可能已被其他请求处理,productCode: {}", productCode);status.setRollbackOnly();return InventoryResult.builder().success(false).message("库存扣减失败,请重试").productCode(productCode).build();}// 5. 查询更新后的库存信息Inventory updatedInventory = inventoryMapper.selectByProductCode(productCode);int afterQuantity = updatedInventory.getStockQuantity();// 6. 记录库存操作日志InventoryOperationLog operationLog = new InventoryOperationLog();operationLog.setProductId(updatedInventory.getProductId());operationLog.setProductCode(productCode);operationLog.setOperationType(1); // 1-扣减operationLog.setQuantity(quantity);operationLog.setBeforeQuantity(beforeQuantity);operationLog.setAfterQuantity(afterQuantity);operationLog.setOperator(operator);operationLog.setRemark("订单号:" + deductRequest.getOrderNo());operationLogMapper.insert(operationLog);log.info("库存扣减成功,productCode: {}, 扣减数量: {}, 扣减前: {}, 扣减后: {}",productCode, quantity, beforeQuantity, afterQuantity);return InventoryResult.builder().success(true).message("库存扣减成功").productCode(productCode).deductQuantity(quantity).currentStock(afterQuantity).build();} catch (Exception e) {log.error("库存扣减异常,productCode: {}", productCode, e);status.setRollbackOnly();return InventoryResult.builder().success(false).message("库存扣减异常,请稍后重试").productCode(productCode).build();}});} finally {// 释放分布式锁distributedLockUtil.releaseLock(lock, lockKey);}}/*** 验证库存扣减请求参数* @param deductRequest 库存扣减请求*/private void validateDeductRequest(InventoryDeductRequest deductRequest) {if (ObjectUtils.isEmpty(deductRequest)) {throw new BusinessException("库存扣减请求不能为空");}StringUtils.hasText(deductRequest.getProductCode(), "商品编码不能为空");StringUtils.hasText(deductRequest.getOrderNo(), "订单编号不能为空");if (ObjectUtils.isEmpty(deductRequest.getQuantity()) || deductRequest.getQuantity() <= 0) {throw new BusinessException("扣减数量必须大于0");}StringUtils.hasText(deductRequest.getOperator(), "操作人不能为空");}
}
最后,创建 Controller:
/*** 库存控制器* @author ken*/
@RestController
@RequestMapping("/api/v1/inventories")
@ApiModel(value = "InventoryController", description = "库存相关接口")
@Slf4j
public class InventoryController {@Autowiredprivate InventoryService inventoryService;/*** 扣减库存* @param deductRequest 库存扣减请求参数* @return 扣减结果*/@PostMapping("/deduct")@ApiOperation(value = "扣减库存", notes = "根据商品编码扣减相应数量的库存")@ApiResponses({@ApiResponse(responseCode = "200", description = "成功"),@ApiResponse(responseCode = "400", description = "参数错误"),@ApiResponse(responseCode = "500", description = "服务器错误")})public Result<InventoryResult> deductInventory(@ApiParam(value = "库存扣减请求参数", required = true) @RequestBody @Valid InventoryDeductRequest deductRequest) {try {InventoryResult result = inventoryService.deductInventory(deductRequest);return Result.success(result);} catch (BusinessException e) {log.warn("库存扣减失败,request: {}, message: {}", JSON.toJSONString(deductRequest), e.getMessage());return Result.fail(e.getMessage());} catch (Exception e) {log.error("库存扣减异常,request: {}", JSON.toJSONString(deductRequest), e);return Result.fail("库存扣减异常,请稍后重试");}}
}
5.2.4 实现说明
在这个库存系统的案例中,我们通过以下方式保证了库存扣减的安全性:
基于 Redis 的分布式锁:
- 使用 Redisson 实现分布式锁,确保同一商品的库存扣减操作同一时间只有一个请求能够执行
- 设置了合理的锁等待时间和持有时间,避免长时间阻塞和死锁
乐观锁机制:
- 在库存表中添加了 version 字段,作为乐观锁的版本控制
- 库存扣减时会检查版本号,确保扣减操作基于最新的库存状态
事务保证:
- 使用 Spring 的事务管理,确保库存扣减和日志记录的原子性
- 任何一步操作失败都会导致整个事务回滚
通过分布式锁和乐观锁的结合,我们有效地防止了库存超卖问题,同时在保证数据一致性的前提下,尽可能地提高了系统的并发处理能力。
5.3 案例三:订单系统中的幂等性与分布式锁结合使用
在订单系统中,我们既需要保证订单创建的幂等性(防止重复创建订单),又需要使用分布式锁来处理某些临界资源的访问(如库存检查和扣减)。
5.3.1 技术选型
- 与案例一和案例二相同
5.3.2 核心代码实现
这里我们重点展示订单服务的实现,其他相关实体类和 Mapper 可以参考前面的案例。
/*** 订单服务* @author ken*/
public interface OrderService {/*** 创建订单* @param createRequest 订单创建请求* @return 订单创建结果*/OrderResult createOrder(OrderCreateRequest createRequest);
}/*** 订单服务实现* @author ken*/
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {@Autowiredprivate OrderInfoMapper orderInfoMapper;@Autowiredprivate IdempotentTokenService idempotentTokenService;@Autowiredprivate InventoryService inventoryService;@Autowiredprivate DistributedLockUtil distributedLockUtil;@Autowiredprivate TransactionTemplate transactionTemplate;/*** 订单创建的分布式锁前缀*/private static final String ORDER_CREATE_LOCK_PREFIX = "order:create:";@Overridepublic OrderResult createOrder(OrderCreateRequest createRequest) {// 参数验证validateCreateRequest(createRequest);Long userId = createRequest.getUserId();String productCode = createRequest.getProductCode();int quantity = createRequest.getQuantity();String token = createRequest.getToken();// 1. 验证幂等性Tokenboolean tokenValid = idempotentTokenService.validateAndUseToken(token, userId);if (!tokenValid) {log.warn("订单创建请求Token验证失败,request: {}", JSON.toJSONString(createRequest));return OrderResult.builder().success(false).message("订单请求已处理或无效").build();}// 生成订单编号String orderNo = generateOrderNo(userId);// 构建锁的键(使用用户ID+商品编码,确保同一用户对同一商品的订单创建操作互斥)String lockKey = ORDER_CREATE_LOCK_PREFIX + userId + ":" + productCode;// 获取分布式锁,最多等待5秒,持有锁15秒RLock lock = distributedLockUtil.getLock(lockKey, 5, 15);if (ObjectUtils.isEmpty(lock)) {log.error("获取分布式锁失败,无法创建订单,userId: {}, productCode: {}", userId, productCode);// 注意:这里需要考虑Token的处理,由于Token已经被标记为使用,// 可能需要提供一个补偿机制让用户可以重新获取Tokenreturn OrderResult.builder().success(false).message("系统繁忙,请稍后重试").build();}try {// 执行订单创建操作(使用事务保证原子性)return transactionTemplate.execute(status -> {try {// 2. 检查并扣减库存InventoryDeductRequest deductRequest = InventoryDeductRequest.builder().productCode(productCode).quantity(quantity).orderNo(orderNo).operator("USER_" + userId).build();InventoryResult inventoryResult = inventoryService.deductInventory(deductRequest);if (!inventoryResult.isSuccess()) {log.error("库存扣减失败,无法创建订单,orderNo: {}, message: {}",orderNo, inventoryResult.getMessage());status.setRollbackOnly();return OrderResult.builder().success(false).message(inventoryResult.getMessage()).build();}// 3. 查询商品信息(此处简化,实际应调用商品服务)Product product = getProductByCode(productCode);if (ObjectUtils.isEmpty(product)) {log.error("商品不存在,无法创建订单,orderNo: {}, productCode: {}",orderNo, productCode);status.setRollbackOnly();return OrderResult.builder().success(false).message("商品不存在").build();}// 4. 计算订单金额BigDecimal amount = product.getPrice().multiply(new BigDecimal(quantity));// 5. 创建订单记录OrderInfo orderInfo = new OrderInfo();orderInfo.setOrderNo(orderNo);orderInfo.setUserId(userId);orderInfo.setAmount(amount);orderInfo.setStatus(0); // 0-创建中int rows = orderInfoMapper.insert(orderInfo);if (rows <= 0) {log.error("创建订单记录失败,orderNo: {}", orderNo);status.setRollbackOnly();return OrderResult.builder().success(false).message("订单创建失败,请重试").build();}log.info("订单创建成功,orderNo: {}, userId: {}, productCode: {}, quantity: {}",orderNo, userId, productCode, quantity);return OrderResult.builder().success(true).message("订单创建成功").orderNo(orderNo).amount(amount).productCode(productCode).quantity(quantity).status(0).createTime(LocalDateTime.now()).build();} catch (Exception e) {log.error("订单创建异常,orderNo: {}", orderNo, e);status.setRollbackOnly();return OrderResult.builder().success(false).message("订单创建异常,请稍后重试").build();}});} finally {// 释放分布式锁distributedLockUtil.releaseLock(lock, lockKey);}}/*** 验证订单创建请求参数* @param createRequest 订单创建请求*/private void validateCreateRequest(OrderCreateRequest createRequest) {if (ObjectUtils.isEmpty(createRequest)) {throw new BusinessException("订单创建请求不能为空");}if (ObjectUtils.isEmpty(createRequest.getUserId())) {throw new BusinessException("用户ID不能为空");}StringUtils.hasText(createRequest.getProductCode(), "商品编码不能为空");if (ObjectUtils.isEmpty(createRequest.getQuantity()) || createRequest.getQuantity() <= 0) {throw new BusinessException("购买数量必须大于0");}StringUtils.hasText(createRequest.getToken(), "Token不能为空");}/*** 生成订单编号* @param userId 用户ID* @return 订单编号*/private String generateOrderNo(Long userId) {// 订单编号规则:ORDER + 年月日时分秒 + 用户ID后4位 + 随机4位数return "ORDER" + DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now())+ String.format("%04d", userId % 10000)+ RandomUtils.nextInt(1000, 9999);}/*** 根据商品编码查询商品信息* @param productCode 商品编码* @return 商品信息*/private Product getProductByCode(String productCode) {// 实际应调用商品服务,此处简化Product product = new Product();product.setProductCode(productCode);product.setProductName("测试商品");product.setPrice(new BigDecimal("99.99"));return product;}
}
5.3.3 实现说明
在这个订单系统的案例中,我们结合使用了幂等性设计和分布式锁:
幂等性保证:
- 使用 Token 机制确保同一订单请求不会被重复处理
- 订单编号的唯一性约束防止重复创建订单
分布式锁应用:
- 在订单创建过程中使用分布式锁,确保同一用户对同一商品的订单创建操作互斥
- 结合库存服务中的分布式锁,形成了完整的并发控制体系
多层次的并发控制:
- 外层订单创建使用分布式锁,控制整体流程的并发
- 内层库存扣减也使用分布式锁,控制临界资源的访问
- 同时使用乐观锁作为最后一道防线,确保数据一致性
通过这种多层次、多机制的设计,我们既保证了订单创建的幂等性,防止了重复下单,又通过分布式锁控制了并发访问,防止了库存超卖等问题,构建了一个健壮可靠的订单系统。
六、最佳实践与注意事项
6.1 幂等性设计的最佳实践
优先使用天然幂等的操作:在设计 API 时,尽量使用天然具有幂等性的操作,如基于唯一标识的查询、删除等。
选择合适的幂等标识:
- 对于用户操作,可使用前端生成的 UUID 作为幂等标识
- 对于内部服务调用,可使用请求 ID 作为幂等标识
- 对于消息消费,可使用消息 ID 作为幂等标识
幂等标识的存储:
- 关键业务的幂等标识应存储在数据库中,确保可靠性
- 非关键业务可存储在 Redis 中,提高性能
- 无论存储在哪里,都要设置合理的过期时间
状态机设计:对于有明确状态流转的业务(如订单、支付),应设计清晰的状态机,利用状态的不可逆性实现幂等性。
避免过度设计:并非所有接口都需要实现幂等性,只需要对可能发生重复请求的接口进行幂等性设计。
6.2 分布式锁实现的注意事项
选择合适的锁实现:
- 高并发场景优先选择 Redis 实现的分布式锁
- 对可靠性要求极高的场景可选择 ZooKeeper 实现的分布式锁
- 低并发场景可考虑使用数据库实现的分布式锁
设置合理的锁参数:
- 锁的等待时间不宜过长,避免请求长时间阻塞
- 锁的持有时间应大于业务处理时间,避免锁提前释放
- 可考虑实现锁的自动续期机制,应对长耗时操作
防止死锁:
- 确保锁能够被正确释放,即使在业务处理异常的情况下
- 避免在持有锁的情况下调用外部服务,防止因外部服务超时导致锁无法释放
- 实现锁的超时自动释放机制
锁的粒度:
- 锁的粒度应尽可能小,只锁定必要的资源
- 避免使用全局锁,以免影响系统并发性能
容错处理:
- 实现锁服务的降级策略,在锁服务不可用时能够优雅降级
- 对于获取锁失败的情况,应提供友好的错误提示和重试机制
6.3 幂等性与分布式锁的选择策略
仅需幂等性的场景:
- 用户重复提交表单
- 服务调用超时重试
- 消息队列消息重复消费
- 这些场景下,操作可以并发执行,只需保证重复执行的结果一致
仅需分布式锁的场景:
- 对共享计数器进行增减操作
- 临界区代码的串行化执行
- 这些场景下,操作不能并发执行,必须保证同一时间只有一个操作执行
需要结合使用的场景:
- 订单创建与库存扣减
- 支付处理
- 这些场景下,既需要防止重复操作,又需要控制并发访问
选择原则:
- 优先考虑幂等性设计,因为它对性能影响较小
- 在必须保证操作互斥的场景下使用分布式锁
- 复杂业务场景下,结合使用两种机制,发挥各自优势
七、总结与展望
幂等性和分布式锁是分布式系统中保证数据一致性的两大核心技术,它们各有侧重又相互补充:
- 幂等性保证了重复操作的安全性,允许并发但确保结果一致
- 分布式锁保证了操作的互斥性,通过控制并发来确保结果正确
在实际应用中,我们需要根据具体的业务场景选择合适的技术方案:简单的重复请求问题可以通过幂等性设计解决;复杂的并发控制问题可能需要分布式锁;而大多数关键业务场景则需要两者结合使用,才能构建出既可靠又高效的分布式系统。

