0
点赞
收藏
分享

微信扫一扫

【Redis从入门到进阶】第 7 讲:基于 Redis 实现分布式锁


本文已收录于专栏 🍅《Redis从入门到进阶》🍅

专栏前言

   本专栏开启,目的在于帮助大家更好的掌握学习Redis,同时也是为了记录我自己学习Redis的过程,将会从基础的数据类型开始记录,直到一些更多的应用,如缓存击穿还有分布式锁等。希望大家有问题也可以一起沟通,欢迎一起学习,对于专栏内容有错还望您可以及时指点,非常感谢大家 🌹。

目录

  • 专栏前言
  • 1.什么是分布式锁?
  • 2. 分布式锁的条件
  • 3.常见的分布式锁
  • 4.Redis 实现分布式锁
  • 5. 分布式锁误删问题
  • 6. 分布式锁原子性问题

1.什么是分布式锁?

  锁这个东西,大家都知道,在我们 jvm 内部多个线程竞争同一个资源时,我们利用jvm提供的synchronized 或者一些其他的锁可以帮助我们让线程对资源的串行使用。但这种方法并不适合现在企业广泛使用的分布式架构。因为在这种集群模式下,jvm内部的锁无法被其他jvm内部感知到,那这样肯定无法满足我们的要求,因为锁肯定是要被大家都能感知到的,所以分布式锁应用而生。以前的锁竞争对象是线程之间,而分布式系统中竞争共享资源的单位从线程升级为了进程。

2. 分布式锁的条件

  那么作为一个分布式锁,它应该具备哪些条件呢?

  1. 可见性:多个进程之间均可以看见该锁,且可以尝试获取该锁
  2. 互斥性:锁最基本的特性,同一时间只能保证锁被一个进程持有
  3. 高可用性:也可以理解为容错性,当提供锁的服务结点产生故障时,程序不会因为守到强烈影响
  4. 高性能:锁的释放和添加本身十分消耗性能,我们应选择性能较好的锁

3.常见的分布式锁

  我们一般常见的分布式锁,有以下三种:

  1. MySQLMySQL自带锁机制,但由于其性能一般,所以作为分布式锁比较少见
  2. RedisRedis是分布式锁一种非常常见的实现方式,我们可以利用setnx这个方法,如果插入成功表示锁获取成功,否则获取失败。利用这个机制完成互斥,从而实现分布式锁,而且Redis存储在内存中本身就符合高性能特点。
  3. ZookeeperZookeeper也是企业开发中较好的一种实现分布式锁的方案,以后有机会讲解。

4.Redis 实现分布式锁

基于Redis实现分布式锁,我们使用两个方法:

  • 1.获取锁
  • 【Redis从入门到进阶】第 7 讲:基于 Redis 实现分布式锁_java

  • 该指令会插入一个结构为lock:thread01的锁,且超时时间为100秒,返回值为OK说明获取锁成功,失败则返回false,该方法不会进行阻塞。
  • 2.释放锁
  • 【Redis从入门到进阶】第 7 讲:基于 Redis 实现分布式锁_java_02

  • 通过手动删除该锁来进行释放,或者可以等待TTL让该锁自动过期

核心思路:
  利用RedisSETNX方法,当多个进程同时竞争该锁时,都会调用该方法获取锁,只有一个进程成功能成功获取成功,此时Redis将会生成该锁,其他进程获取失败。那么获取锁成功的进程将会去执行业务,最后删除该锁,这样就将锁释放了。如果想让获取锁失败的进程重新获取,可以手动休眠一段时间后重新获取。

下面是一个简单的使用StringRedisTemplate实现分布式锁的代码:

public class DistributedLock {
    
    private StringRedisTemplate redisTemplate;
    private String lockKey;
    private String lockValue;
    private long lockTimeout;
   	//构造方法
    public DistributedLock(StringRedisTemplate redisTemplate, String lockKey, String lockValue, long lockTimeout) {
        this.redisTemplate = redisTemplate;
        //key
        this.lockKey = lockKey;
        //value
        this.lockValue = lockValue;
        //过期时间
        this.lockTimeout = lockTimeout;
    }
  
    public boolean tryLock() {
        // 尝试获取锁
        Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, lockTimeout, TimeUnit.MILLISECONDS);
        return result != null && result;
    }
    
    public void unlock() {
        // 释放锁
        redisTemplate.delete(lockKey);
    }
}

5. 分布式锁误删问题

  上面的逻辑看上去完美无缺,但还是存在很严重的问题,考虑下面一个场景:
逻辑说明
  线程A持有锁执行业务的时候发生了堵塞,导致他的锁TTL到期自动释放了,此时线程B成功获取到锁了,因为线程A已经释放该锁了。这时候线程A阻塞完毕后继续执行完业务,然后删除该锁,线程B执行完业务时突然发现——woc 我锁呢? 它的锁被线程A误删了,这就是分布式锁误删问题。
解决方案
  上诉问题的产生主要原因,还是因为每个线程并不知道该锁是不是自己的,那我们可以在删除锁的时候去加以判断,如果该锁不属于自己,则不删除该锁。如果该锁是自己的且还未到期,再进行删除锁。我们这里标识一把锁的时候同时存入线程的ID,一般在同一个jvm中线程的标识一般不相同,但我们这是在集群模式下,所以也有可能出现ThreadID重复的情况,所以我们可以考虑在前面拼接上一个UUID



【Redis从入门到进阶】第 7 讲:基于 Redis 实现分布式锁_分布式_03


private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {
    // 获取线程标识
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    // 获取锁
    Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
    return Boolean.TRUE.equals(success);
}

@Override
public void unlock() {
    // 获取当前线程的标识
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    // 获取锁中的标识
    String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    // 判断标识是否一致
    if (threadId.equals(id)) {
        // 释放锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

6. 分布式锁原子性问题

  在进行区分锁的处理后,那么是不是一定不会产生问题呢?

  考虑一种更加极端的情况,当线程 A判断完标识发现一致后,准备释放锁的时候又突然出现了阻塞情况(比如JVM垃圾挥手),锁又到期了,线程B进来拿了一把锁,因为线程A已经判断完标识,所以它一删锁又把B的锁给删掉了,这就又产生了误删的问题。

  解决的方案需要我们使用Lua脚本,来保证拿锁、判断标识、删锁三个操作是一个原子性操作,而Lua脚本可以同时执行多条Redis指令并且保证原子性,Lua脚本是一门脚本语言,有兴趣可以自行了解一下。


举报

相关推荐

0 条评论