分布式锁的 5 种实现方式,你用过几种?

3 阅读5分钟

前言

分布式锁是后端开发的核心技能,面试必问,生产必用。

这篇文章总结了 5 种分布式锁实现方式,从简单到复杂,总有一款适合你。


一、分布式锁的核心要求

1.1 基本要求

特性说明
互斥性同一时间只有一个客户端能获取锁
安全性只有加锁的客户端才能解锁
可重入同一线程可以重复获取锁
高可用锁服务不能单点故障

1.2 常见使用场景

场景1:库存扣减
用户下单 → 获取库存锁 → 扣减库存 → 释放锁

场景2:定时任务
每5分钟执行一次 → 获取分布式锁 → 执行成功则释放,失败则放弃

场景3:接口幂等
重复请求 → 获取请求ID锁 → 已处理则返回结果,未处理则执行业务

二、实现方式 1:MySQL 分布式锁

2.1 基于唯一索引

原理:利用数据库唯一索引的排他性。

-- 创建锁表
CREATE TABLE distributed_lock (
    lock_name VARCHAR(64) NOT NULL,
    lock_holder VARCHAR(64) NOT NULL,
    expire_time TIMESTAMP NOT NULL,
    PRIMARY KEY (lock_name)
);
@Service
public class MySQLDistributedLock {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    public boolean tryLock(String lockName, String holder, int expireSeconds) {
        try {
            jdbcTemplate.update(
                "INSERT INTO distributed_lock (lock_name, lock_holder, expire_time) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL ? SECOND))",
                lockName, holder, expireSeconds
            );
            return true;
        } catch (DuplicateKeyException e) {
            return false;
        }
    }
    
    public void unlock(String lockName, String holder) {
        jdbcTemplate.update(
            "DELETE FROM distributed_lock WHERE lock_name = ? AND lock_holder = ?",
            lockName, holder
        );
    }
}

优点:简单、无需额外组件 缺点:性能差、依赖数据库可用性

2.2 基于乐观锁

-- 版本号控制
UPDATE stock SET count = count - 1, version = version + 1 
WHERE product_id = 1 AND version = #{version} AND count > 0;

三、实现方式 2:Redis 单节点锁

3.1 SETNX 命令

@Service
public class RedisLock {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    public boolean tryLock(String lockKey, String requestId, int expireSeconds) {
        Boolean result = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, requestId, expireSeconds, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(result);
    }
    
    public void unlock(String lockKey, String requestId) {
        // 原子性解锁:判断是不是自己加的锁,是则删除
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        
        redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            Collections.singletonList(lockKey),
            requestId
        );
    }
}

3.2 存在的问题

问题1:锁续期
- 业务执行时间 > 锁过期时间
- 解决方案:看门狗自动续期

问题2:主从延迟
- 主节点挂锁,还没同步到从节点,主节点挂了
- 从节点升级为主,锁丢失
- 解决方案:RedLock

四、实现方式 3:Redisson(推荐)

4.1 什么是 Redisson?

Redisson 是 Redis 的 Java 客户端,提供了完善的分布式锁实现。

4.2 使用方法

@Configuration
public class RedissonConfig {
    
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
            .setAddress("redis://127.0.0.1:6379")
            .setDatabase(0);
        return Redisson.create(config);
    }
}

@Service
public class OrderService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    public void createOrder(Long productId) {
        String lockKey = "lock:product:" + productId;
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 尝试获取锁,最多等待10秒,锁30秒后自动释放
            boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
            if (!locked) {
                throw new RuntimeException("获取锁失败");
            }
            
            // 执行业务逻辑
            doCreateOrder(productId);
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            // 释放锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

4.3 Redisson 的优势

特性说明
可重入同一线程可重复获取锁
看门狗自动续期,防止业务执行中锁过期
阻塞等待获取不到锁时,可设置等待时间
公平锁按获取锁的顺序排队
读写锁读读共享、读写互斥、写写互斥

4.4 源码解析

// 看门狗自动续期原理
private void renewExpiration() {
    // 启动定时任务,每 1/3 过期时间检查一次
    Timeout task = commandExecutor.getConnectionManager()
        .newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                // 执行续期命令
                CompletionStage<Boolean> future = renewExpirationAsync();
                future.thenAccept(res -> {
                    if (res) {
                        // 续期成功,继续下一次检查
                        renewExpiration();
                    }
                });
            }
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
}

五、实现方式 4:ZooKeeper

5.1 原理

ZooKeeper 临时顺序节点实现:
1. 客户端创建临时顺序节点 /lock/product_0000000001
2. 判断是不是最小序号节点,是则获取锁成功
3. 不是则监听前一个节点,等待释放
4. 锁释放时删除节点,触发监听,下一个节点获取锁

5.2 代码实现

@Component
public class ZkDistributedLock {
    
    private CuratorFramework client;
    
    @PostConstruct
    public void init() {
        client = CuratorFrameworkFactory.builder()
            .connectString("127.0.0.1:2181")
            .retryPolicy(new ExponentialBackoffRetry(1000, 3))
            .build();
        client.start();
    }
    
    public boolean tryLock(String lockPath) {
        InterProcessMutex mutex = new InterProcessMutex(client, "/locks/" + lockPath);
        try {
            return mutex.acquire(10, TimeUnit.SECONDS);
        } catch (Exception e) {
            return false;
        }
    }
    
    public void unlock(InterProcessMutex mutex) {
        try {
            mutex.release();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

优点:可靠性高、自动释放(会话断开自动删节点) 缺点:性能比 Redis 差、需要维护 ZK 集群


六、实现方式 5:RedLock(不推荐)

6.1 什么是 RedLock?

RedLock 是 Redis 作者提出的多节点锁算法,解决主从延迟问题。

6.2 算法原理

假设有 5 个 Redis 节点:
1. 获取当前时间戳 T1
2. 依次向 5 个节点获取锁,设置超时时间
3. 统计成功获取锁的节点数 N
4. 获取锁总耗时 T2 - T1
5. 如果 N >= 3 且 T2 - T1 < 锁过期时间,则获取锁成功
6. 如果失败,向所有节点释放锁

6.3 为什么不推荐?

争议点:
1. 时钟漂移问题
2. 节点故障恢复问题
3. 性能开销大(需要访问多个节点)

结论:
- 大多数场景用 Redisson 就够了
- 极高可靠性要求用 ZooKeeper

七、方案选型建议

方案优点缺点适用场景
MySQL简单、无需额外组件性能差低并发、简单场景
Redis SETNX简单、性能好有锁续期问题简单分布式锁
Redisson功能完善、自动续期依赖 Redis大多数场景(推荐)
ZooKeeper可靠性高性能一般金融级可靠性要求
RedLock理论完备复杂、有争议极特殊场景

八、生产环境注意事项

8.1 锁续期

// 设置合理的过期时间
// 太短:业务没执行完锁就释放了
// 太长:异常情况下锁很久不释放
// 推荐:使用 Redisson 看门狗自动续期

8.2 幂等性

// 分布式锁 + 幂等性双重保证
public void processOrder(String orderId) {
    // 1. 幂等性检查
    if (redisTemplate.opsForValue().setIfAbsent("order:processed:" + orderId, "1", 1, TimeUnit.DAYS)) {
        // 2. 获取分布式锁
        RLock lock = redissonClient.getLock("lock:order:" + orderId);
        try {
            if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
                // 执行业务
                doProcess(orderId);
            }
        } finally {
            lock.unlock();
        }
    }
}

8.3 监控告警

// 获取锁失败告警
if (!lock.tryLock(5, 30, TimeUnit.SECONDS)) {
    // 发送告警
    alertService.send("获取锁失败: " + lockKey);
    throw new LockAcquireException("获取锁失败");
}

总结

5 种分布式锁实现方式:

MySQL:简单但性能差 ✅ Redis SETNX:简单但需要自己处理续期 ✅ Redisson:功能完善,推荐大多数场景 ✅ ZooKeeper:高可靠,金融场景 ✅ RedLock:理论完备但不推荐

推荐

  • 一般场景 → Redisson
  • 金融级可靠性 → ZooKeeper
  • 简单低并发 → MySQL

💡 互动:你们项目用的哪种分布式锁方案?遇到过什么问题?评论区聊聊!