分布式锁常见实现方案总结

一、Redis

1. SETNX

加锁:

127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX
OK
  • lockKey:加锁的锁名;
  • uniqueValue:能够唯一标识锁的随机字符串;
  • NX:只有当 lockKey 对应的 key 值不存在的时候才能 SET 成功;
  • EX:过期时间设置(秒为单位)EX 3 标示这个锁有一个 3 秒的自动过期时间。与 EX 对应的是 PX(毫秒为单位),这两个都是过期时间设置。

解锁:

// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

java 实现方式:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring.redis.host=localhost
public String deductStock() {
    String lockKey = "lock:product_101";
    String clientId = UUID.randomUUID().toString();
    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS); //jedis.setnx(k,v)
    if (!result) {
        return "error_code";
    }
    try {
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
            System.out.println("扣减成功,剩余库存:" + realStock);
        } else {
            System.out.println("扣减失败,库存不足");
        }
    } finally {
        if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
            // 卡在这里,锁过期了,其他线程又可以加锁,此时又把其他线程新加的锁删掉了
            stringRedisTemplate.delete(lockKey);
        }
    }
    return "end";
}

SETNX的坑:

  • 锁失效之后,别人加锁成功,自己把别人的锁删了:
    • 所以这里需要uniqueValue,也就是唯一标识锁的随机字符串,不是自己的锁不要释放。
  • 即使判断了uniqueValue,但是因为程序卡住,判断完uniqueValue以后,锁过期了,其他线程又可以加锁,此时又把其他线程新加的锁删掉了:
    • 使用锁续期(Timer)
  • 过期时间设置不合理,任务未完成但是锁失效了:
    • 使用锁续期(Timer),使用Timer,每隔一段时间就给锁续期,除非线程自己主动删除。这也是Redisson的实现思路。

2. Redisson(推荐)

特点:

  • 自带自动续期机制(看门狗)
  • 内置了多种类型的锁比如可重入锁(Reentrant Lock)、自旋锁(Spin Lock)、公平锁(Fair Lock)、多重锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)
<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.19.1</version>
        </dependency>
@Configuration
public class RedissonConfiguration {

    @Bean
    public RedissonClient getRedissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        return Redisson.create(config);
    }
} 
public void redissonLock() {

        // 1.获取指定的分布式锁对象
        RLock lock = redisson.getLock("lock");
        // 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制
        lock.lock();
        // 手动给锁设置过期时间,不具备 Watch Dog 自动续期机制
        lock.lock(10, TimeUnit.SECONDS);
        // 还有lock.tryLock方法,非阻塞锁,如果拿不到锁,则直接返回,不会阻塞

        // 3.执行业务...

        // 4.释放锁
        lock.unlock();
    }

二、ZooKeeper

1. ZooKeeper

不要重复造轮子,直接用Curator

实现原理:
临时节点具备数据自动删除的功能。当client与ZooKeeper连接和session断掉时,相应的临时节点就会被删除。zk有瞬时和持久节点,瞬时节点不可以有子节点。会话结束之后瞬时节点就会消失,ZooKeeper 分布式锁就是基于 临时顺序节点 和 Watcher(事件监听器) 实现的。

获取锁:

  1. 首先我们要有一个持久节点/locks,客户端获取锁就是在locks下创建临时顺序节点。
  2. 假设客户端 1 创建了/locks/lock1节点,创建成功之后,会判断 lock1是否是 /locks 下最小的子节点。
  3. 如果 lock1是最小的子节点,则获取锁成功。否则,获取锁失败。
  4. 如果获取锁失败,则说明有其他的客户端已经成功获取锁。客户端 1 并不会不停地循环去尝试加锁,而是在前一个节点比如/locks/lock0上注册一个事件监听器。这个监听器的作用是当前一个节点释放锁之后通知客户端 1(避免无效自旋),这样客户端 1 就加锁成功了。

释放锁:

  1. 成功获取锁的客户端在执行完业务流程之后,会将对应的子节点删除。
  2. 成功获取锁的客户端在出现故障之后,对应的子节点由于是临时顺序节点,也会被自动删除,避免了锁无法被释放。
  3. 我们前面说的事件监听器其实监听的就是这个子节点删除事件,子节点删除就意味着锁被释放。

使用 Redis 实现分布式锁的时候,我们是通过过期时间来避免锁无法被释放导致死锁问题的,而 ZooKeeper 直接利用临时节点的特性即可。假设不使用顺序节点的话,所有尝试获取锁的客户端都会对持有锁的子节点加监听器。当该锁被释放之后,势必会造成所有尝试获取锁的客户端来争夺锁,这样对性能不友好。使用顺序节点之后,只需要监听前一个节点就好了,对性能更友好。

2. ZooKeeper Curator

特点:
Curator主要实现了下面四种锁:

  • InterProcessMutex:分布式可重入排它锁
  • InterProcessSemaphoreMutex:分布式不可重入排它锁
  • InterProcessReadWriteLock:分布式读写锁
  • InterProcessMultiLock:将多个锁作为单个实体管理的容器,获取锁的时候获取所有锁,释放锁也会释放所有锁资源(忽略释放失败的锁)。
<dependency>
  <groupId>org.apache.curator</groupId>
  <artifactId>curator-recipes</artifactId>
  <version>4.2.0</version>
  <exclusions>
    <exclusion>
      <artifactId>slf4j-api</artifactId>
      <groupId>org.slf4j</groupId>
    </exclusion>
  </exclusions>
</dependency>
@Configuration
public class CuratorConfiguration {

    @Bean(initMethod = "start", destroyMethod = "close")
    public CuratorFramework getCuratorFramework() {
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        return CuratorFrameworkFactory.
                newClient("localhost:2181", retryPolicy);
    }
}
public void zkLock() {
        InterProcessMutex lock = new InterProcessMutex(curatorFramework, "/order");
        try {
            if (lock.acquire(30, TimeUnit.SECONDS)) {
                try {
                    log.info("我获得了锁!!!");
                } finally {
                    lock.release();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

三、Etcd

不会

四、分布式锁的真相

需要满足的几个特性:

互斥:不同线程、进程互斥。
超时机制:临界区代码耗时导致,网络原因导致。可以使用额外的线程续命保证。
完备的锁接口:阻塞的和非阻塞的接口都要有,lock和tryLock。
可重入性:当前请求的节点+ 线程唯一标识。
公平性:锁唤醒时候,按照顺序唤醒。
正确性:进程内的锁不会因为报错死锁,因为崩溃的时候整个进程都会结束。但是多实例部署时死锁就很容易发生,如果粗暴使用超时机制解决死锁问题,就默认了下面这个假设:

  • 锁的超时时间 >> 获取锁的时延 + 执行临界区代码的时间 + 各种进程的暂停(比如 GC)
  • 但上述假设其实无法保证的。

将分布式锁定位为,可以容忍非常小概率互斥语义失效场景下的锁服务。一般来说,一个分布式锁服务,它的正确性要求越高,性能可能就会越低。

五、锁优化:分段加锁逻辑

针对一个商品,要开启秒杀的时候,会将商品的库存预先加载到Redis缓存中,比如有100个库存,此时可以分为5个key,每一个key有20个库存。可以把分布式锁的性能提升5倍。
例如:
product_10111_stock = 100
product_10111_stock1 = 20
product_10111_stock2 = 20
product_10111_stock3 = 20
product_10111_stock4 = 20
product_10111_stock5 = 20
请求来了可以随机可以轮询,扣减完之后就标记不要下次再分配到这个库存。

参考文档:
https://javaguide.cn/distributed-system/distributed-lock-implementations.html
https://mp.weixin.qq.com/s/JzCHpIOiFVmBoAko58ZuGw
https://mp.weixin.qq.com/s/-N4x6EkxwAYDGdJhwvmZLw

### 分布式锁实现方案概述 分布式锁是一种用于协调多个分布式系统之间资源访问的机制,其主要目的是防止多个客户端同时修改共享资源而导致数据不一致。以下是几种常见分布式锁实现方案及其特点: #### 基于ZooKeeper的分布式锁 ZooKeeper通过创建临时顺序节点来实现分布式锁[^1]。当某个客户端尝试获取锁时,它会在指定路径下创建一个临时顺序节点。如果该节点是最小编号的节点,则认为成功获取锁;否则,监听前驱节点的变化事件并通过Watcher机制通知等待中的线程重新竞争锁。 ```java // ZooKeeper分布式锁伪代码示例 public class ZKLock { private final String lockPath; public void acquire() throws Exception { while (true) { createEphemeralSequentialNode(lockPath); if (isFirstInLine()) break; // 如果当前节点为最小序号则获得锁 waitForPreviousNode(); // 否则阻塞并等待前驱节点释放锁 } } public void release() throws Exception { deleteCurrentNode(); } } ``` #### 基于Redis的分布式锁 Redis因其高性能简单易用的特点,在分布式锁领域也有广泛应用。一种典型的实现方式是使用`SETNX`命令配合过期时间设置唯一键值对作为锁标志[^3]。此外还有更复杂的算法如Redlock,能够进一步提升可靠性。 ```lua -- Lua脚本用于原子化操作以确保安全性 local key = KEYS[1] local value = ARGV[1] if redis.call('setnx', key, value) == 1 then redis.call('expire', key, tonumber(ARGV[2])) return 'OK' else return nil end ``` #### MySQL表锁定方法 虽然MySQL并不是专门设计用来做分布式的工具,但在某些场景下也可以借助数据库事务特性完成简单的同步控制功能[^2]。不过这种方法效率较低且扩展性差,通常只适用于小型项目或者特殊需求场合。 --- ### 总结比较三种技术选型优劣 | 技术 | 特点 | |------|------------------------------------------------------------------------------------------| | ZooKeeper | 提供强一致性保障,适合复杂业务逻辑下的精确控制 | | Redis | 高性能读写能力突出,支持多种高级特性灵活配置选项 | | MySQL | 易于部署维护但性能瓶颈明显,仅推荐少量并发环境 | 每种方案都有各自适用范围以及局限之处,实际应用过程中需综合考虑具体应用场景、性能指标等因素做出合理选择。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值