分布式锁基础与三种实现方式对比

一:Redisson 分布式锁

1. 为什么需要分布式锁?

单体应用用 synchronizedReentrantLock 就够了,但分布式环境下多个 JVM 实例之间互斥不了。分布式锁解决的是跨进程、跨节点的互斥访问

2. 三种实现方式对比

维度 Redis ZooKeeper MySQL
实现原理 SET NX EX + Lua 释放 临时顺序节点 + Watcher 唯一索引 / 乐观锁
性能 极高(1-3ms) 中等(3-10ms)
一致性 最终一致(主从异步复制有锁丢失风险) 强一致(ZAB 协议) 强一致
可靠性 需 Redisson 看门狗续期 会话过期自动释放 需定时清理死锁
复杂度 低(用 Redisson 一行代码) 中(需理解 ZNode)
适用场景 高并发、性能敏感 强一致、高可靠 简单场景、已有 MySQL

选型建议

  • 绝大多数场景用 Redis + Redisson,性能最好,生态最成熟
  • 金融级强一致场景用 ZooKeeper
  • 简单项目或已有 MySQL 不想引入新组件,用 MySQL 唯一索引

3. 手写 Redis 分布式锁

最基础的版本,用 SET key value NX EX 原子命令:

arduino 复制代码
@Component
public class SimpleRedisLock {
    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String LOCK_PREFIX = "lock:";
    private static final long EXPIRE_SECONDS = 30;

    /**
     * 加锁
     */
    public boolean tryLock(String lockName, String requestId) {
        Boolean success = redisTemplate.opsForValue()
            .setIfAbsent(LOCK_PREFIX + lockName, requestId, EXPIRE_SECONDS, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    /**
     * 释放锁(用 Lua 保证原子性)
     */
    public void unlock(String lockName, 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(LOCK_PREFIX + lockName),
            requestId);
    }
}

为什么释放锁要用 Lua? 因为 GETDEL 是两个操作,中间可能被其他线程插入,导致误删别人的锁。Lua 脚本在 Redis 中是原子执行的。

存在的问题

  • 业务执行时间超过 30 秒,锁过期了,其他线程能加锁,导致并发问题
  • 需要自己处理续期逻辑

这就是 Redisson 看门狗要解决的核心痛点。


二:Redisson 分布式锁与看门狗机制

1. Redisson 快速上手

xml 复制代码
<<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.27.2</version>
</dependency>
yaml 复制代码
spring:
  redis:
    redisson:
      config: |
        singleServerConfig:
          address: "redis://127.0.0.1:6379"
          password: null
          database: 0
          # 看门狗超时时间,默认 30000ms
          lockWatchdogTimeout: 30000

2. 基础使用

csharp 复制代码
@Service
public class OrderService {
    @Autowired
    private RedissonClient redissonClient;

    public void deductStock(String productId) {
        RLock lock = redissonClient.getLock("stock:" + productId);
        
        try {
            // 方式1:无参 lock(),启用看门狗自动续期
            lock.lock();
            
            // 方式2:带过期时间,禁用看门狗
            // lock.lock(10, TimeUnit.SECONDS);
            
            // 业务逻辑:扣减库存
            int stock = getStock(productId);
            if (stock > 0) {
                updateStock(productId, stock - 1);
            }
        } finally {
            // 只有当前线程持有锁才释放,避免误删
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

3. 看门狗机制

核心机制

参数 默认值 说明
lockWatchdogTimeout 30 秒 锁的过期时间
续期周期 10 秒(30/3) 每隔 1/3 超时时间检查一次
触发条件 lock() 无参调用 leaseTime 参数不启用看门狗

工作流程

  1. 线程 A 调用 lock() 加锁成功,Redisson 设置锁过期时间为 30 秒
  2. 启动看门狗后台任务(Netty HashedWheelTimer),每隔 10 秒检查
  3. 如果线程 A 还持有锁,发送 Lua 脚本将 TTL 重置为 30 秒
  4. 线程 A 调用 unlock(),停止看门狗,释放锁
  5. 如果线程 A 宕机,看门狗停止,锁 30 秒后自动过期

为什么用 Netty Timer 不用 JDK Timer?

  • 异步非阻塞,复用 EventLoop
  • 异常隔离,单个任务失败不影响其他续期
  • 哈希轮算法 O(1) 性能,适合高并发

4. 可重入锁

Redisson 锁是可重入的,同一个线程多次 lock() 不会死锁:

csharp 复制代码
public void methodA() {
    RLock lock = redissonClient.getLock("myLock");
    lock.lock();
    try {
        methodB(); // 同一线程,可以再次获取锁
    } finally {
        lock.unlock();
    }
}

public void methodB() {
    RLock lock = redissonClient.getLock("myLock");
    lock.lock(); // 重入,计数 +1
    try {
        // 业务
    } finally {
        lock.unlock(); // 计数 -1,不会真正释放
    }
}

底层用 Redis Hash 结构存储:KEY 是锁名,FIELD 是线程标识,VALUE 是重入次数。


三:Redisson 进阶:红锁、读写锁、信号量

1. 红锁(RedLock)------多主节点部署

Redis 主从异步复制有锁丢失风险,Redisson 提供 RedLock 算法:在 N 个独立 Redis 主节点上加锁,过半成功才算获取锁。

ini 复制代码
@Configuration
public class RedLockConfig {
    
    @Bean
    public RedissonRedLock redLock() {
        Config config1 = new Config();
        config1.useSingleServer().setAddress("redis://192.168.1.1:6379");
        RedissonClient redisson1 = Redisson.create(config1);
        
        Config config2 = new Config();
        config2.useSingleServer().setAddress("redis://192.168.1.2:6379");
        RedissonClient redisson2 = Redisson.create(config2);
        
        Config config3 = new Config();
        config3.useSingleServer().setAddress("redis://192.168.1.3:6379");
        RedissonClient redisson3 = Redisson.create(config3);
        
        RLock lock1 = redisson1.getLock("redLock");
        RLock lock2 = redisson2.getLock("redLock");
        RLock lock3 = redisson3.getLock("redLock");
        
        return new RedissonRedLock(lock1, lock2, lock3);
    }
}
typescript 复制代码
@Service
public class RedLockService {
    @Autowired
    private RedissonRedLock redLock;

    public void safeOperation() {
        try {
            // 尝试在大多数节点加锁
            boolean locked = redLock.tryLock(10, 30, TimeUnit.SECONDS);
            if (locked) {
                // 执行业务
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            redLock.unlock();
        }
    }
}

实际上 RedLock 争议很大,Redis 作者和分布式系统专家 Martin Kleppmann 有过著名辩论。生产环境更推荐用 Redis Cluster + Redisson 单节点锁 + 看门狗,或者直接用 ZooKeeper。

2. 读写锁(ReadWriteLock)

读读共享,读写互斥,写写互斥:

typescript 复制代码
@Service
public class CacheService {
    @Autowired
    private RedissonClient redissonClient;

    public String readData(String key) {
        RReadWriteLock rwLock = redissonClient.getReadWriteLock("rwLock:" + key);
        RLock readLock = rwLock.readLock();
        
        readLock.lock();
        try {
            return getFromCache(key);
        } finally {
            readLock.unlock();
        }
    }

    public void writeData(String key, String value) {
        RReadWriteLock rwLock = redissonClient.getReadWriteLock("rwLock:" + key);
        RLock writeLock = rwLock.writeLock();
        
        writeLock.lock();
        try {
            updateCache(key, value);
        } finally {
            writeLock.unlock();
        }
    }
}

3. 信号量(Semaphore)------限流

java 复制代码
@Service
public class RateLimiterService {
    @Autowired
    private RedissonClient redissonClient;

    public void limitedOperation() {
        RSemaphore semaphore = redissonClient.getSemaphore("semaphore:api");
        // 设置许可证数量(比如最多 10 个并发)
        semaphore.trySetPermits(10);
        
        boolean acquired = semaphore.tryAcquire(3, 5, TimeUnit.SECONDS); // 等5秒
        if (acquired) {
            try {
                // 执行业务
            } finally {
                semaphore.release(); // 释放许可证
            }
        } else {
            throw new RuntimeException("系统繁忙,请稍后重试");
        }
    }
}

四:ZooKeeper 分布式锁实现

1. 核心原理

利用 ZooKeeper 的临时顺序节点(EPHEMERAL_SEQUENTIAL)

  1. 所有客户端在 /locks 下创建临时顺序节点,如 /locks/lock0000000001
  2. 判断自己创建的节点是不是序号最小的
  3. 如果是,获取锁成功;如果不是,监听前一个节点
  4. 前一个节点删除(释放锁),触发 Watcher 通知,客户端再次判断
  5. 客户端宕机会话过期,临时节点自动删除,避免死锁

2. Curator 实现(生产环境直接用)

Curator 是 Netflix 开源的 ZooKeeper 客户端,封装了分布式锁:

xml 复制代码
<<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>5.6.0</version>
</dependency>
typescript 复制代码
@Configuration
public class CuratorConfig {
    
    @Bean
    public CuratorFramework curatorFramework() {
        CuratorFramework client = CuratorFrameworkFactory.builder()
            .connectString("127.0.0.1:2181")
            .sessionTimeoutMs(5000)
            .connectionTimeoutMs(3000)
            .retryPolicy(new ExponentialBackoffRetry(1000, 3))
            .build();
        client.start();
        return client;
    }
}

@Service
public class ZkLockService {
    @Autowired
    private CuratorFramework client;

    public void deductStock(String productId) {
        InterProcessMutex lock = new InterProcessMutex(client, "/locks/stock/" + productId);
        
        try {
            // 获取锁,最多等待 10 秒
            boolean acquired = lock.acquire(10, TimeUnit.SECONDS);
            if (acquired) {
                try {
                    // 扣减库存业务
                    System.out.println("获取锁成功,执行业务");
                } finally {
                    lock.release();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3. 手写 ZooKeeper 锁

csharp 复制代码
public class ZkDistributedLock implements Watcher {
    private ZooKeeper zk;
    private String lockPath;
    private String currentNode;
    private CountDownLatch connectedLatch = new CountDownLatch(1);
    private CountDownLatch waitLatch;

    public ZkDistributedLock(String connectString, String lockName) throws Exception {
        zk = new ZooKeeper(connectString, 5000, this);
        connectedLatch.await();
        this.lockPath = "/locks/" + lockName;
        
        // 确保父节点存在
        if (zk.exists(lockPath, false) == null) {
            zk.create(lockPath, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
    }

    public void lock() throws Exception {
        // 创建临时顺序节点
        currentNode = zk.create(lockPath + "/lock", new byte[0], 
            ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        
        // 检查是否最小
        List<String> children = zk.getChildren(lockPath, false);
        Collections.sort(children);
        String nodeName = currentNode.substring(lockPath.length() + 1);
        
        if (nodeName.equals(children.get(0))) {
            return; // 获取锁成功
        }
        
        // 不是最小,监听前一个节点
        int index = Collections.binarySearch(children, nodeName);
        String prevNode = children.get(index - 1);
        
        waitLatch = new CountDownLatch(1);
        zk.exists(lockPath + "/" + prevNode, event -> {
            if (event.getType() == Event.EventType.NodeDeleted) {
                waitLatch.countDown();
            }
        });
        waitLatch.await();
    }

    public void unlock() throws Exception {
        zk.delete(currentNode, -1);
        zk.close();
    }

    @Override
    public void process(WatchedEvent event) {
        if (event.getState() == Event.KeeperState.SyncConnected) {
            connectedLatch.countDown();
        }
    }
}

五:MySQL 唯一索引实现分布式锁

1. 核心原理

利用 MySQL 唯一索引的排他性:多个线程同时 INSERT 同一条记录,只有一个能成功,成功的那个获取锁;释放锁时 DELETE 这条记录。

sql 复制代码
CREATE TABLE `distributed_lock` (
    `lock_name` VARCHAR(64) NOT NULL COMMENT '锁名称,如 order:10086',
    `lock_value` VARCHAR(128) NOT NULL COMMENT '锁持有者标识,如 UUID+线程ID',
    `expire_time` DATETIME NOT NULL COMMENT '锁过期时间',
    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`lock_name`),
    UNIQUE KEY `uk_lock_name` (`lock_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分布式锁表';

2. 手写 MySQL 分布式锁

typescript 复制代码
@Component
public class MysqlDistributedLock {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 获取锁
     * @param lockName 锁名称
     * @param requestId 请求标识(UUID + 线程ID)
     * @param expireSeconds 过期时间(秒)
     * @return true=获取成功
     */
    public boolean tryLock(String lockName, String requestId, int expireSeconds) {
        try {
            jdbcTemplate.update(
                "INSERT INTO distributed_lock(lock_name, lock_value, expire_time) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL ? SECOND))",
                lockName, requestId, expireSeconds
            );
            return true;
        } catch (DuplicateKeyException e) {
            // 唯一索引冲突,获取锁失败
            return false;
        }
    }

    /**
     * 释放锁(必须校验归属权,防止误删)
     */
    public boolean unlock(String lockName, String requestId) {
        int affected = jdbcTemplate.update(
            "DELETE FROM distributed_lock WHERE lock_name = ? AND lock_value = ?",
            lockName, requestId
        );
        return affected > 0;
    }

    /**
     * 续期(看门狗思想)
     */
    public boolean renew(String lockName, String requestId, int expireSeconds) {
        int affected = jdbcTemplate.update(
            "UPDATE distributed_lock SET expire_time = DATE_ADD(NOW(), INTERVAL ? SECOND) " +
            "WHERE lock_name = ? AND lock_value = ? AND expire_time > NOW()",
            expireSeconds, lockName, requestId
        );
        return affected > 0;
    }
}
typescript 复制代码
@Service
public class OrderService {
    @Autowired
    private MysqlDistributedLock lock;

    public void deductStock(String productId) {
        String lockName = "stock:" + productId;
        String requestId = UUID.randomUUID() + ":" + Thread.currentThread().getId();
        int expireSeconds = 30;

        boolean acquired = lock.tryLock(lockName, requestId, expireSeconds);
        if (!acquired) {
            throw new RuntimeException("获取锁失败");
        }

        try {
            // 启动看门狗续期(实际用 ScheduledExecutorService)
            // ...
            // 执行业务:扣减库存
        } finally {
            lock.unlock(lockName, requestId);
        }
    }
}

3. 存在的问题与解决方案

问题 原因 解决方案
死锁 客户端宕机,锁记录不删除 expire_time 字段,定时任务清理过期锁
锁过期业务没完 业务执行时间超过 expireSeconds 看门狗续期,或设置足够长的过期时间
误删他人锁 释放时不校验归属 DELETE 必须带 lock_value 条件
性能差 每次加锁都是一次磁盘 INSERT 仅用于低并发场景,高并发用 Redis

4. 基于 SELECT ... FOR UPDATE 的悲观锁方案

另一种 MySQL 锁实现,利用 InnoDB 行锁:

typescript 复制代码
@Component
public class MysqlPessimisticLock {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional  // 必须开启事务
    public boolean tryLock(String lockName, int waitSeconds) {
        try {
            // 先确保锁记录存在
            jdbcTemplate.update(
                "INSERT IGNORE INTO distributed_lock(lock_name, lock_value, expire_time) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 1 DAY))",
                lockName, "placeholder"
            );

            // FOR UPDATE 加行锁,其他线程阻塞等待
            jdbcTemplate.queryForObject(
                "SELECT lock_name FROM distributed_lock WHERE lock_name = ? FOR UPDATE",
                String.class,
                lockName
            );
            return true;
        } catch (CannotAcquireLockException e) {
            return false;
        }
    }
}

缺点

  • 必须开启事务,事务持有锁期间不能干别的
  • 高并发下大量线程阻塞,性能极差(实测 QPS 仅约 10)
  • 容易死锁,需要设置 innodb_lock_wait_timeout

相关推荐
IT_陈寒2 小时前
闭包陷阱让我加了两天班,JavaScript你真行
前端·人工智能·后端
易协同低代码3 小时前
通达OA核心类库TD类深度解析
后端
Gopher_HBo3 小时前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志4 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..4 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects4 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰4 小时前
一些基础和问题解决
后端
陈随易4 小时前
前端项目部署只要30秒
前端·后端·程序员
YIAN4 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp
Imchendiana4 小时前
《狂人日记NO.9》— 前后端一把梭,我的全栈实录
前端·后端