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

一: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

相关推荐
MariaH1 小时前
Web服务器开发
后端
程序边界1 小时前
凌晨三点批量掉授权,我花了四小时才搞明白LAC心跳链路是怎么算的
后端
叫我:松哥1 小时前
基于Flask的在线考试刷题系统设计与实现,集智能练习、过程追踪、深度分析与个性化引导
数据库·人工智能·后端·python·flask·boostrap
AI人工智能_电脑小能手1 小时前
【大白话说Java面试题 第106题】【并发篇】第6题:synchronized 锁的锁对象可以是什么?
java·后端·面试
Rain5091 小时前
2.3. 安全配置:环境变量与 API 密钥管理
前端·人工智能·后端·安全·ai·node.js·ai编程
yinchnag1 小时前
Go 语言 map 底层实现
后端·源码阅读
MariaH1 小时前
Express框架使用
后端
MacroZheng1 小时前
横空出世!Claude Code画图神器来了,比Visio快10倍!
java·人工智能·后端
布局呆星1 小时前
Spring Boot + AOP 操作日志实战:自定义注解、切面编程、SecurityContext 全链路贯通,一次讲透
java·spring boot·后端