{T}

分布式锁与幂等设计

分布式系统里最常见的两个"防乱套"手段,就是分布式锁和幂等。它们经常一起出现,但解决的问题完全不同:

  • 分布式锁:控制并发访问共享资源
  • 幂等:允许重复请求或重复消息,但结果不失控

很多系统的问题恰恰来自把这两件事混用。该做幂等的地方上了锁,结果吞吐大降;该做锁保护的地方只做幂等,结果关键资源还是被并发打穿。

分布式锁与幂等的本质区别

对比表格

维度分布式锁幂等设计
解决问题并发访问共享资源重复请求/消息处理
关注点同一时间只有一个执行者即使执行多次,结果也不乱
性能影响串行化,吞吐下降几乎无影响
适用场景定时任务、库存扣减、资源抢占支付回调、消息消费、接口重试
实现复杂度中等(需要考虑锁超时、续期等)低(状态机 + 唯一约束)
失败策略获取锁失败 → 等待或放弃重复请求 → 直接返回成功

典型误区

code
× 错误理解:有了锁就不需要幂等
   → 锁超时释放后,请求可能重复执行

× 错误理解:有了幂等就不需要锁
   → 并发写入可能导致数据不一致

√ 正确理解:锁和幂等各司其职,互相补充

分布式锁解决什么问题

分布式锁适合处理"多个节点同时争抢同一份资源"的场景,例如:

  • 定时任务:只能有一个节点执行
  • 数据处理:同一批数据只允许一个实例处理
  • 临界区保护:某个短临界区需要串行化

它不适合当成通用并发控制手段,更不适合拿去包长事务。

分布式锁的核心要求

一个可靠的分布式锁必须满足:

  1. 互斥性:任意时刻,只有一个客户端持有锁
  2. 防死锁:锁必须有过期时间,避免客户端崩溃后锁无法释放
  3. 防误删:只能删除自己加的锁,不能删除别人的锁
  4. 高可用:锁服务本身要高可用
  5. 可重入:同一个线程可以多次获取同一把锁(可选)

幂等解决什么问题

幂等面对的是重复执行问题:

  • 用户重复提交:连续点击多次按钮
  • 接口超时重试:客户端超时后自动重试
  • 消息重复投递:MQ 至少一次投递语义
  • 第三方回调:支付平台多次通知

幂等的目标是:

同一个业务请求执行多次,最终结果保持一致,不产生额外副作用

幂等的数学定义

code
f(x) = f(f(x))

即:执行一次和执行多次的结果相同

为什么锁和幂等不能互相替代

分布式锁关注的是:

  • "同一时间只有一个执行者"

幂等关注的是:

  • "即使执行了多次,结果也不乱"

所以:

  • 有锁,不代表重复请求就一定安全(锁超时后可能重复执行)
  • 有幂等,也不代表共享资源并发冲突就一定消失(并发写入可能导致数据不一致)

常见分布式锁实现方式对比

实现方式优点缺点适用场景
Redis SET NX性能高、实现简单主从切换可能丢锁、需要续期中等一致性要求
Redisson功能完善、支持续期、可重入依赖 Redis 集群稳定性生产环境推荐
ZooKeeper强一致、可靠性高性能较低、实现复杂强一致性要求
数据库锁简单、无需额外组件性能差、锁表风险低并发场景
etcd强一致、支持租约需要额外部署云原生环境

Redis 分布式锁实现详解

基础实现:SET NX EX

最常见的是基于 Redis 的:

text
SET key value NX EX seconds
  • NX:只在 key 不存在时设置(Not eXists)
  • EX:设置过期时间(EXpire)

完整实现示例(防误删)

java
@Service
public class RedisDistributedLock {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String LOCK_PREFIX = "lock:";
    private static final long DEFAULT_EXPIRE = 30; // 默认 30 秒
    
    /**
     * 加锁
     * @param lockKey 锁的 key
     * @param requestId 请求唯一标识(用于防误删)
     * @param expireSeconds 过期时间(秒)
     * @return 是否加锁成功
     */
    public boolean tryLock(String lockKey, String requestId, long expireSeconds) {
        String key = LOCK_PREFIX + lockKey;
        Boolean result = redisTemplate.opsForValue()
            .setIfAbsent(key, requestId, expireSeconds, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(result);
    }
    
    /**
     * 释放锁(Lua 脚本保证原子性)
     * @param lockKey 锁的 key
     * @param requestId 请求唯一标识
     * @return 是否释放成功
     */
    public boolean releaseLock(String lockKey, String requestId) {
        String key = LOCK_PREFIX + lockKey;
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        
        RedisScript<Long> redisScript = RedisScript.of(script, Long.class);
        Long result = redisTemplate.execute(
            redisScript, 
            Collections.singletonList(key), 
            requestId
        );
        return Long.valueOf(1).equals(result);
    }
}

使用示例

java
@Service
public class DailySettlementService {
    
    @Autowired
    private RedisDistributedLock lockService;
    
    @Autowired
    private SettlementService settlementService;
    
    public void executeDailySettlement() {
        String lockKey = "job:daily-settlement";
        String requestId = UUID.randomUUID().toString();
        
        // 尝试加锁
        if (lockService.tryLock(lockKey, requestId, 300)) { // 5 分钟过期
            try {
                // 执行任务
                settlementService.execute();
            } finally {
                // 释放锁(只释放自己加的锁)
                lockService.releaseLock(lockKey, requestId);
            }
        } else {
            log.info("其他节点正在执行,本次跳过");
        }
    }
}

Redis 锁的核心问题与解决

问题 1:锁超时后业务还没执行完怎么办?

问题场景:

  1. 线程 A 获取锁,设置 30 秒过期
  2. 业务执行超过 30 秒
  3. 锁自动释放
  4. 线程 B 获取锁并开始执行
  5. 线程 A 执行完毕,释放了线程 B 的锁

解决方案:锁续期(Watch Dog)

java
@Service
public class RedisLockWithWatchdog {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10);
    private ConcurrentHashMap<String, ScheduledFuture<?>> watchdogs = new ConcurrentHashMap<>();
    
    /**
     * 加锁并启动看门狗
     */
    public boolean tryLockWithWatchdog(String lockKey, String requestId, long expireSeconds) {
        boolean locked = tryLock(lockKey, requestId, expireSeconds);
        if (locked) {
            // 启动看门狗,每隔过期时间的 1/3 续期一次
            long watchInterval = expireSeconds * 1000 / 3;
            ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(() -> {
                try {
                    renewLock(lockKey, requestId, expireSeconds);
                } catch (Exception e) {
                    log.error("锁续期失败", e);
                }
            }, watchInterval, watchInterval, TimeUnit.MILLISECONDS);
            
            watchdogs.put(lockKey, future);
        }
        return locked;
    }
    
    /**
     * 续期
     */
    private boolean renewLock(String lockKey, String requestId, long expireSeconds) {
        String key = "lock:" + lockKey;
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('expire', KEYS[1], ARGV[2]) " +
            "else " +
            "    return 0 " +
            "end";
        
        RedisScript<Long> redisScript = RedisScript.of(script, Long.class);
        Long result = redisTemplate.execute(
            redisScript, 
            Collections.singletonList(key), 
            requestId, 
            String.valueOf(expireSeconds)
        );
        return Long.valueOf(1).equals(result);
    }
    
    /**
     * 释放锁并停止看门狗
     */
    public boolean releaseLockWithWatchdog(String lockKey, String requestId) {
        // 停止看门狗
        ScheduledFuture<?> future = watchdogs.remove(lockKey);
        if (future != null) {
            future.cancel(false);
        }
        
        // 释放锁
        return releaseLock(lockKey, requestId);
    }
}

问题 2:解锁时如何避免误删别人加的锁?

问题场景:

  1. 线程 A 获取锁
  2. 业务执行超时,锁自动过期
  3. 线程 B 获取锁
  4. 线程 A 执行完毕,直接 DEL 删除了线程 B 的锁

解决方案:Lua 脚本保证原子性

lua
-- 释放锁的 Lua 脚本
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end

只有在 value 等于自己的 requestId 时才删除,保证不会误删别人的锁。

问题 3:Redis 主从切换时锁状态是否存在风险?

问题场景:

  1. 客户端 A 在 Master 上加锁成功
  2. Master 还未同步到 Slave 就宕机
  3. Slave 升级为新的 Master
  4. 客户端 B 在新 Master 上加锁成功
  5. 两个客户端同时持有锁

解决方案:Redlock 算法

Redlock 算法核心思想:在多个独立的 Redis 节点上加锁,只有超过半数节点加锁成功才算成功。

java
// 使用 Redisson 实现 Redlock
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://node1:6379");

Config config2 = new Config();
config2.useSingleServer().setAddress("redis://node2:6379");

Config config3 = new Config();
config3.useSingleServer().setAddress("redis://node3:6379");

RedissonClient client1 = Redisson.create(config1);
RedissonClient client2 = Redisson.create(config2);
RedissonClient client3 = Redisson.create(config3);

RLock lock1 = client1.getLock("myLock");
RLock lock2 = client2.getLock("myLock");
RLock lock3 = client3.getLock("myLock");

RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);

try {
    // 尝试加锁
    boolean locked = redLock.tryLock(10, 30, TimeUnit.SECONDS);
    if (locked) {
        // 执行业务
    }
} finally {
    redLock.unlock();
}

Redisson 推荐配置

生产环境推荐使用 Redisson,它已经内置了锁续期、可重入等能力:

java
@Configuration
public class RedissonConfig {
    
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        
        // 单节点配置
        config.useSingleServer()
            .setAddress("redis://127.0.0.1:6379")
            .setConnectionPoolSize(64)
            .setConnectionMinimumIdleSize(24)
            .setIdleConnectionTimeout(10000)
            .setConnectTimeout(10000)
            .setTimeout(3000)
            .setRetryAttempts(3)
            .setRetryInterval(1500);
        
        // 集群配置
        // config.useClusterServers()
        //     .addNodeAddress("redis://node1:6379", "redis://node2:6379", "redis://node3:6379");
        
        return Redisson.create(config);
    }
}

@Service
public class DistributedLockService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    public void executeWithLock(String lockKey, Runnable task) {
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 尝试加锁,最多等待 10 秒,锁 30 秒后自动释放
            boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
            if (locked) {
                task.run();
            } else {
                throw new RuntimeException("获取锁失败");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("线程被中断", e);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

所以分布式锁更适合:

  • 可降级

而不是把核心交易链路全部压在它上面。

常见幂等实现方式

幂等实现方式对比

实现方式优点缺点适用场景
业务唯一单号简单、可靠需要业务支持支付、订单等有单号的场景
数据库唯一约束强一致、可靠依赖数据库、可能锁表插入型操作
幂等 Token灵活、可控需要双方配合接口幂等
状态机约束业务语义清晰需要设计状态机状态流转类操作
去重表解耦业务需要额外表通用幂等
Redis 标记性能高需要持久化配合快速幂等判断

实现方式一:业务唯一单号

java
@Service
public class PaymentService {
    
    @Autowired
    private PaymentRecordRepository paymentRepository;
    
    @Transactional(rollbackFor = Exception.class)
    public void handlePayment(PaymentRequest request) {
        // 幂等检查:是否已处理
        if (paymentRepository.existsByPayNo(request.getPayNo())) {
            log.info("支付已处理,幂等返回: {}", request.getPayNo());
            return;
        }
        
        // 执行业务逻辑
        // ...
        
        // 记录支付流水
        PaymentRecord record = PaymentRecord.builder()
            .payNo(request.getPayNo())
            .amount(request.getAmount())
            .build();
        paymentRepository.save(record);
    }
}

实现方式二:数据库唯一约束

sql
-- 创建唯一索引
CREATE UNIQUE INDEX uk_order_no ON orders(order_no);
java
@Service
public class OrderService {
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Transactional(rollbackFor = Exception.class)
    public Order createOrder(CreateOrderCommand command) {
        try {
            Order order = Order.create(command);
            return orderRepository.save(order);
        } catch (DataIntegrityViolationException e) {
            // 唯一索引冲突,说明订单已存在
            log.info("订单已存在,幂等返回: {}", command.getOrderNo());
            return orderRepository.findByOrderNo(command.getOrderNo());
        }
    }
}

实现方式三:幂等 Token

流程:

  1. 客户端先请求获取 Token
  2. 服务端生成 Token 并存储(Redis)
  3. 客户端携带 Token 发起请求
  4. 服务端验证并删除 Token(原子操作)
  5. 验证成功执行业务,失败返回重复请求
java
@Service
public class IdempotentTokenService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String TOKEN_PREFIX = "idempotent:token:";
    
    /**
     * 生成 Token
     */
    public String createToken(String bizType) {
        String token = UUID.randomUUID().toString();
        String key = TOKEN_PREFIX + bizType + ":" + token;
        // Token 有效期 5 分钟
        redisTemplate.opsForValue().set(key, "1", 5, TimeUnit.MINUTES);
        return token;
    }
    
    /**
     * 验证并删除 Token(原子操作)
     */
    public boolean validateAndDeleteToken(String bizType, String token) {
        String key = TOKEN_PREFIX + bizType + ":" + token;
        
        // Lua 脚本:存在则删除,返回 1;不存在返回 0
        String script = 
            "if redis.call('exists', KEYS[1]) == 1 then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        
        RedisScript<Long> redisScript = RedisScript.of(script, Long.class);
        Long result = redisTemplate.execute(redisScript, Collections.singletonList(key));
        return Long.valueOf(1).equals(result);
    }
}

@RestController
public class OrderController {
    
    @Autowired
    private IdempotentTokenService tokenService;
    
    @Autowired
    private OrderService orderService;
    
    /**
     * 获取 Token
     */
    @GetMapping("/api/order/token")
    public String getToken() {
        return tokenService.createToken("order");
    }
    
    /**
     * 创建订单(幂等)
     */
    @PostMapping("/api/order/create")
    public Order createOrder(
        @RequestHeader("Idempotent-Token") String token,
        @RequestBody CreateOrderCommand command
    ) {
        // 验证 Token
        if (!tokenService.validateAndDeleteToken("order", token)) {
            throw new RuntimeException("重复请求或 Token 已失效");
        }
        
        // 执行业务
        return orderService.createOrder(command);
    }
}

实现方式四:状态机约束

java
@Service
public class OrderStateMachine {
    
    @Autowired
    private OrderRepository orderRepository;
    
    // 定义允许的状态流转
    private static final Map<OrderStatus, Set<OrderStatus>> ALLOWED_TRANSITIONS = Map.of(
        OrderStatus.INIT, Set.of(OrderStatus.PAID, OrderStatus.CANCELLED),
        OrderStatus.PAID, Set.of(OrderStatus.SHIPPED, OrderStatus.REFUNDED),
        OrderStatus.SHIPPED, Set.of(OrderStatus.COMPLETED),
        OrderStatus.COMPLETED, Set.of(),
        OrderStatus.CANCELLED, Set.of(),
        OrderStatus.REFUNDED, Set.of()
    );
    
    @Transactional(rollbackFor = Exception.class)
    public void payOrder(String orderNo) {
        Order order = orderRepository.findByOrderNo(orderNo);
        
        // 状态机检查:只允许 INIT -> PAID
        if (!canTransition(order.getStatus(), OrderStatus.PAID)) {
            throw new IllegalStateException(
                "订单状态不允许支付: " + order.getStatus()
            );
        }
        
        // 更新状态
        order.setStatus(OrderStatus.PAID);
        order.setPayTime(LocalDateTime.now());
        orderRepository.save(order);
    }
    
    private boolean canTransition(OrderStatus from, OrderStatus to) {
        Set<OrderStatus> allowedTargets = ALLOWED_TRANSITIONS.get(from);
        return allowedTargets != null && allowedTargets.contains(to);
    }
}

实现方式五:去重表

sql
CREATE TABLE idempotent_record (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    biz_type VARCHAR(50) NOT NULL COMMENT '业务类型',
    biz_key VARCHAR(200) NOT NULL COMMENT '业务唯一键',
    request_hash VARCHAR(64) COMMENT '请求参数哈希(可选)',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_biz_type_key (biz_type, biz_key)
) COMMENT '幂等记录表';
java
@Service
public class IdempotentRecordService {
    
    @Autowired
    private IdempotentRecordRepository idempotentRepository;
    
    /**
     * 检查并记录(原子操作)
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean checkAndRecord(String bizType, String bizKey) {
        try {
            IdempotentRecord record = IdempotentRecord.builder()
                .bizType(bizType)
                .bizKey(bizKey)
                .build();
            idempotentRepository.save(record);
            return true; // 首次请求
        } catch (DataIntegrityViolationException e) {
            return false; // 重复请求
        }
    }
}

实现方式六:Redis 标记(配合数据库持久化)

java
@Service
public class RedisIdempotentService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    @Autowired
    private IdempotentRecordRepository dbRepository;
    
    private static final String IDEMPOTENT_PREFIX = "idempotent:";
    
    /**
     * 检查幂等(先查 Redis,再查数据库)
     */
    public boolean isProcessed(String bizType, String bizKey) {
        String key = IDEMPOTENT_PREFIX + bizType + ":" + bizKey;
        
        // 1. 先查 Redis
        if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
            return true;
        }
        
        // 2. 再查数据库(防止 Redis 数据丢失)
        return dbRepository.existsByBizTypeAndBizKey(bizType, bizKey);
    }
    
    /**
     * 标记已处理
     */
    public void markProcessed(String bizType, String bizKey) {
        String key = IDEMPOTENT_PREFIX + bizType + ":" + bizKey;
        
        // 1. 写入 Redis(过期时间 1 天)
        redisTemplate.opsForValue().set(key, "1", 1, TimeUnit.DAYS);
        
        // 2. 写入数据库(持久化)
        IdempotentRecord record = IdempotentRecord.builder()
            .bizType(bizType)
            .bizKey(bizKey)
            .build();
        dbRepository.save(record);
    }
}

其中最稳的一类通常是"业务结果层约束",比如数据库唯一键和状态机,而不是只靠入口防抖。

示例场景

场景一:支付回调幂等

第三方支付平台可能重复通知多次。

正确做法通常是:

  • 按支付单号做幂等
  • 订单状态只允许 INIT -> PAID
  • 已经处理过的通知直接返回成功

这个场景通常不需要分布式锁,核心是状态机和幂等约束。

完整实现:

java
@Service
public class PaymentCallbackService {
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private PaymentRecordRepository paymentRepository;
    
    @Autowired
    private EventPublisher eventPublisher;
    
    @Transactional(rollbackFor = Exception.class)
    public void handlePaymentCallback(PaymentCallback callback) {
        // 1. 幂等检查:是否已处理
        if (paymentRepository.existsByPayNo(callback.getPayNo())) {
            log.info("支付回调已处理,幂等返回: {}", callback.getPayNo());
            return;
        }
        
        // 2. 查询订单
        Order order = orderRepository.findByOrderNo(callback.getOrderNo());
        if (order == null) {
            throw new RuntimeException("订单不存在: " + callback.getOrderNo());
        }
        
        // 3. 状态机检查:只允许 INIT -> PAID
        if (order.getStatus() != OrderStatus.INIT) {
            log.warn("订单状态不允许支付: orderNo={}, status={}", 
                order.getOrderNo(), order.getStatus());
            return; // 直接返回,不抛异常
        }
        
        // 4. 更新订单状态
        order.setStatus(OrderStatus.PAID);
        order.setPayTime(callback.getPayTime());
        orderRepository.save(order);
        
        // 5. 记录支付流水
        PaymentRecord record = PaymentRecord.builder()
            .payNo(callback.getPayNo())
            .orderNo(order.getOrderNo())
            .amount(callback.getAmount())
            .payTime(callback.getPayTime())
            .build();
        paymentRepository.save(record);
        
        // 6. 发送下游事件(异步)
        eventPublisher.publish("order-paid", order);
    }
}

场景二:定时任务单实例执行

报表汇总、库存对账、补偿扫描这类任务,经常要求同一时刻只能一个实例跑。

这时分布式锁就比较合适:

  • 加锁成功的节点执行任务
  • 其他节点快速退出
  • 任务结束后正常释放锁

实现要点:

java
@Service
public class DailySettlementJob {
    
    @Autowired
    private RedissonClient redissonClient;
    
    @Autowired
    private SettlementService settlementService;
    
    @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨 2 点执行
    public void execute() {
        RLock lock = redissonClient.getLock("job:daily-settlement");
        
        try {
            // 尝试加锁,最多等待 0 秒,锁 1 小时后自动释放
            boolean locked = lock.tryLock(0, 3600, TimeUnit.SECONDS);
            if (locked) {
                log.info("获取锁成功,开始执行对账任务");
                settlementService.execute();
            } else {
                log.info("其他节点正在执行,本次跳过");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("获取锁被中断", e);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

场景三:秒杀下单

秒杀场景如果对每个请求都上全局锁,吞吐会立刻崩掉。

更合理的思路通常是:

  • 入口限流:令牌桶、漏桶、滑动窗口
  • 库存预扣减:Redis 原子操作
  • 业务单号幂等:防止重复下单
  • 细粒度锁:必要时只对热点资源做细粒度锁控制

这里幂等和限流通常比"大锁包全场"更重要。

实现方案:

java
@Service
public class SeckillService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    @Autowired
    private OrderService orderService;
    
    /**
     * 秒杀下单
     */
    public OrderResult seckill(SeckillRequest request) {
        String productId = request.getProductId();
        String userId = request.getUserId();
        
        // 1. 幂等检查:是否已下单
        String orderKey = "seckill:order:" + productId + ":" + userId;
        if (Boolean.TRUE.equals(redisTemplate.hasKey(orderKey))) {
            throw new RuntimeException("您已参与过秒杀");
        }
        
        // 2. 库存扣减(Lua 脚本保证原子性)
        String stockKey = "seckill:stock:" + productId;
        String stockScript = 
            "if tonumber(redis.call('get', KEYS[1])) > 0 then " +
            "    return redis.call('decr', KEYS[1]) " +
            "else " +
            "    return -1 " +
            "end";
        
        RedisScript<Long> redisScript = RedisScript.of(stockScript, Long.class);
        Long stock = redisTemplate.execute(redisScript, Collections.singletonList(stockKey));
        
        if (stock == null || stock < 0) {
            throw new RuntimeException("商品已售罄");
        }
        
        try {
            // 3. 创建订单(数据库事务)
            Order order = orderService.createOrder(request);
            
            // 4. 标记已下单
            redisTemplate.opsForValue().set(orderKey, order.getOrderNo(), 1, TimeUnit.DAYS);
            
            return OrderResult.success(order);
        } catch (Exception e) {
            // 5. 创建订单失败,回滚库存
            redisTemplate.opsForValue().increment(stockKey);
            throw new RuntimeException("下单失败", e);
        }
    }
}

分布式锁性能优化

锁粒度优化

java
// × 错误:锁粒度太粗,吞吐低
public void updateInventory(Long productId, Integer quantity) {
    RLock lock = redissonClient.getLock("inventory");
    lock.lock();
    try {
        // 更新库存
    } finally {
        lock.unlock();
    }
}

// √ 正确:锁粒度细,只锁定特定商品
public void updateInventory(Long productId, Integer quantity) {
    RLock lock = redissonClient.getLock("inventory:" + productId);
    lock.lock();
    try {
        // 更新库存
    } finally {
        lock.unlock();
    }
}

锁等待优化

java
// × 错误:无限等待,可能阻塞线程池
RLock lock = redissonClient.getLock("myLock");
lock.lock();

// √ 正确:设置合理的等待时间
RLock lock = redissonClient.getLock("myLock");
boolean locked = lock.tryLock(3, 30, TimeUnit.SECONDS);
if (!locked) {
    // 快速失败或降级处理
    return;
}

联锁(MultiLock)处理多个资源

java
// 需要同时锁定多个资源
public void transfer(Account from, Account to, BigDecimal amount) {
    RLock lock1 = redissonClient.getLock("account:" + from.getId());
    RLock lock2 = redissonClient.getLock("account:" + to.getId());
    
    // 按固定顺序加锁,避免死锁
    RLock multiLock = redissonClient.getMultiLock(lock1, lock2);
    
    try {
        multiLock.lock();
        // 执行转账
    } finally {
        multiLock.unlock();
    }
}

什么时候优先考虑幂等

出现这些情况时,优先考虑幂等而不是锁:

  • 请求可能重试
  • 消息可能重复
  • 回调可能重复到达
  • 最终副作用可通过状态约束去重

什么时候才上分布式锁

只有当确实存在"共享资源并发冲突"且无法用状态机、唯一键、CAS 等方式解决时,再考虑分布式锁。

而且通常要满足:

  • 临界区够短
  • 失败后可接受或可补偿
  • 锁失效风险可控

治理重点

治理项分布式锁幂等设计
基本要求锁要有过期时间幂等键要有唯一性
防误操作解锁要校验 value,避免误删幂等判断要落在结果层
监控告警锁等待时间、持有时间监控重复请求次数监控
降级策略获取锁失败后的降级方案幂等失败后的处理方案

常见误区

  • 把分布式锁当成万能并发药:滥用锁导致性能问题
  • 明明可以用唯一键或状态机解决,却先上锁:简单问题复杂化
  • 锁持有时间过长:导致吞吐急剧下降
  • 只做入口防重,不做结果层幂等:并发场景仍然有问题
  • 以为加了锁就不需要补偿和监控:锁失效后无感知

最佳实践总结

选择决策树

code
问题类型是什么?
├─ 并发访问共享资源 → 分布式锁
│   └─ 是否可以用数据库锁/CAS 解决?
│       ├─ 是 → 使用数据库锁/CAS
│       └─ 否 → 使用分布式锁(Redis/ZooKeeper)
│
└─ 重复请求/消息处理 → 幂等设计
    └─ 是否有业务单号?
        ├─ 是 → 业务单号 + 唯一约束
        └─ 否 → 幂等 Token / 去重表

实施清单

分布式锁:

  • 设置合理的过期时间
  • 使用 Lua 脚本防误删
  • 实现锁续期机制(Watch Dog)
  • 按固定顺序加锁,避免死锁
  • 设置合理的等待时间
  • 监控锁等待时间和持有时间

幂等设计:

  • 幂等键具有唯一性
  • 幂等判断落在结果层(数据库/状态机)
  • 使用唯一索引防止重复插入
  • 状态机约束非法流转
  • 监控重复请求次数
  • 保留幂等记录便于排查

面试补充

高频面试题

Q1:分布式锁和幂等分别解决什么问题?

A:

  • 分布式锁:解决并发冲突问题,保证同一时间只有一个执行者访问共享资源
  • 幂等:解决重复执行问题,保证同一个请求执行多次结果一致

Q2:为什么支付回调更适合幂等而不是大锁?

A:因为支付回调的重点是重复通知的结果收敛,而不是并发临界区串行。支付平台可能多次通知同一笔支付,只需要保证不重复处理即可,不需要加锁串行化。

Q3:Redis 分布式锁至少要注意什么?

A:

  1. 过期时间:避免客户端崩溃后锁无法释放
  2. 防误删:使用 Lua 脚本保证"判断 + 删除"的原子性
  3. 续期:业务执行时间超过锁过期时间时需要续期
  4. 主从切换风险:考虑使用 Redlock 算法或 ZooKeeper

Q4:幂等最稳的落点在哪里?

A:业务结果层,例如:

  • 数据库唯一索引(防止重复插入)
  • 状态机约束(限制非法状态流转)
  • 业务单号 + 唯一约束(业务层面的幂等)

入口层的幂等(如 Redis 标记)不够可靠,可能因为缓存失效导致重复处理。

Q5:为什么秒杀场景不能靠一个全局锁硬扛?

A:因为吞吐会被锁串行化彻底拖垮。秒杀场景的核心矛盾是高并发,应该:

  1. 入口限流(令牌桶、漏桶)
  2. 库存预扣减(Redis 原子操作)
  3. 业务单号幂等(防重复下单)
  4. 细粒度锁(必要时只锁定热点商品)

Q6:TCC 和分布式锁有什么关系?

A:TCC 的 Try 阶段通常需要预留资源(如库存预占),这个阶段需要加锁保护,避免并发冲突。但 TCC 的核心是事务补偿,锁只是其中的一个手段。

Q7:如何实现一个可靠的分布式锁?

A:

  1. 使用 Redis 的 SET key value NX EX seconds 命令
  2. value 使用唯一标识(如 UUID),用于防误删
  3. 释放锁时使用 Lua 脚本保证原子性
  4. 实现锁续期机制(Watch Dog)
  5. 考虑主从切换风险(Redlock 算法)
  6. 设置合理的等待时间和过期时间

Q8:分布式锁的锁续期机制是什么?

A:当业务执行时间超过锁的过期时间时,通过定时任务(Watch Dog)定期延长锁的过期时间,避免锁提前释放导致并发问题。Redisson 已经内置了这个机制。

版本差异(技术原理说明)

维度说明
技术原理分布式一致性/事务/锁/ID 生成等原理与具体版本无关,长期有效
落地选型新项目建议优先使用 Nacos/Redis/Seata 等成熟组件(JDK 17+ 兼容)
Java 版本示例代码基于 JDK 8 编写,JDK 17/21 下语法兼容

本文讲解的分布式系统核心问题与解决方案原理稳定,不随框架版本变化;落地时选用支持 JDK 17/21 的组件版本即可。