后端分布式锁:解决并发竞争的终极方案

103 阅读4分钟

在分布式系统中,多个服务实例同时操作共享资源(如库存扣减、订单创建)时,会出现 “超卖”“重复创建” 等并发问题 —— 本地锁(如 Java 的 synchronized)只能控制单实例内的并发,无法解决跨服务的竞争。分布式锁通过分布式协调机制(如 Redis、ZooKeeper),让多个服务实例共享一把 “锁”,确保同一时间只有一个实例能操作资源。

分布式锁的核心要求

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

  • 互斥性:同一时间只有一个服务能获取锁
  • 安全性:锁只能被持有锁的服务释放
  • 可用性:锁服务不能单点故障
  • 重入性:持有锁的服务可再次获取锁(可选,视场景而定)
  • 超时释放:防止服务宕机导致锁永远不释放

主流实现方案

1. Redis 分布式锁:高性能场景的首选

基于 Redis 的SET NX(不存在则设置)命令实现,利用 Redis 的单线程特性保证原子性。

基础实现(Java)

public class RedisDistributedLock {
    private RedisTemplate<String, String> redisTemplate;
    private String lockKey; // 锁的Key(如"stock:lock:1001")
    private String lockValue; // 锁的值(随机UUID,用于标识持有者)
    private long expireTime = 30000; // 锁过期时间(30秒)
    private long waitTime = 10000; // 获取锁的最大等待时间(10秒)

    public RedisDistributedLock(RedisTemplate<String, String> redisTemplate, String lockKey) {
        this.redisTemplate = redisTemplate;
        this.lockKey = lockKey;
        this.lockValue = UUID.randomUUID().toString(); // 生成唯一值,避免误释放
    }

    // 获取锁
    public boolean tryLock() {
        // 使用SET NX + EX命令,确保原子性(不存在则设置,同时设置过期时间)
        Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.MILLISECONDS);
        return Boolean.TRUE.equals(success);
    }

    // 带等待的获取锁
    public boolean tryLockWithWait() throws InterruptedException {
        long startTime = System.currentTimeMillis();
        while (true) {
            if (tryLock()) {
                return true;
            }
            // 等待100ms重试
            Thread.sleep(100);
            // 超过最大等待时间则返回失败
            if (System.currentTimeMillis() - startTime > waitTime) {
                return false;
            }
        }
    }

    // 释放锁(必须用Lua脚本保证原子性)
    public boolean unlock() {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Long result = (Long) redisTemplate.execute(
                new DefaultRedisScript<>(script, Long.class),
                Collections.singletonList(lockKey),
                lockValue
        );
        return result != null && result > 0;
    }
}

使用示例

// 扣减库存(防止超卖)
public boolean deductStock(Long productId, int num) {
    String lockKey = "stock:lock:" + productId;
    RedisDistributedLock lock = new RedisDistributedLock(redisTemplate, lockKey);
    try {
        // 获取锁
        if (!lock.tryLockWithWait()) {
            log.warn("获取锁失败,productId={}", productId);
            return false;
        }
        // 业务逻辑:查询库存、扣减库存
        Product product = productMapper.selectById(productId);
        if (product.getStock() < num) {
            return false;
        }
        product.setStock(product.getStock() - num);
        productMapper.updateById(product);
        return true;
    } catch (InterruptedException e) {
        log.error("获取锁异常", e);
        return false;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

2. 其他实现方案对比

方案优点缺点适用场景
Redis性能高,实现简单可能存在锁超时问题高并发场景(如秒杀)
ZooKeeper可靠性高,支持重入锁性能较低,部署复杂高可靠性场景(如分布式事务)
数据库实现简单(基于唯一索引)性能差,易死锁中小规模系统,快速实现

分布式锁的高级特性

1. 锁自动续期(防超时)

Redis 锁的过期时间若设置过短,可能导致业务未完成锁就释放;设置过长,又可能因服务宕机导致锁长期不释放。解决方案是 “自动续期”:

  • 获取锁后,启动一个后台线程,每隔一段时间(如过期时间的 1/3)延长锁的过期时间

  • 业务完成后,关闭续期线程并释放锁

实现思路:结合 Redis 的EXPIRE命令和 Java 的 ScheduledExecutorService 实现定时续期。

2. 公平锁与非公平锁

  • 非公平锁:多个请求同时抢锁,谁先拿到算谁的(Redis 默认实现),优点是性能高,缺点是可能导致 “饥饿”(某些请求长期抢不到锁)

  • 公平锁:按请求顺序获取锁(ZooKeeper 基于节点顺序实现),优点是避免饥饿,缺点是性能略低

选择建议:秒杀等对性能要求高的场景用非公平锁;订单处理等需公平性的场景用公平锁。

避坑指南

  • 避免锁粒度过大:锁的范围应尽可能小(如锁 “商品 ID=1001” 而非锁整个 “商品表”),否则会降低并发效率

  • 必须处理锁释放失败:即使解锁逻辑正确,也可能因网络问题导致释放失败,需依赖锁的过期时间兜底

  • 集群环境的特殊处理:Redis 集群下,主从切换可能导致 “锁丢失”(主节点锁已写入但未同步到从节点就宕机),需使用 RedLock 算法(多节点加锁)

分布式锁是分布式系统并发控制的 “最后一道防线”,它通过跨服务的协调机制,让原本混乱的并发操作变得有序。但需注意,分布式锁不是银弹 —— 过度使用会降低系统吞吐量,应在 “数据一致性” 和 “性能” 之间找到平衡,这正是后端架构设计 “取舍之道” 的体现。