【Redis】分布式锁

2022/7/2 2:20:40

本文主要是介绍【Redis】分布式锁,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

分布式锁的应用场景

在传统单机部署的情况下,可以使用Java并发处理相关的API(如synchronized)进行互斥控制。

但是在分布式系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机并发控制锁策略失效,A 服务器上的 synchronized 并不能限制 B 服务器的程序,所以仅靠关键字无法解决分布式系统的线程并发问题

Redis实现分布式锁

一个简单的实现

Redis中有一个命令SETNX key value,这个命令的作用是set if not exists,这条指令执行的过程中,当key不存在时,设置value,当其存在的时候,则什么也不做。

有了这条命令,再结合Redis的单线程的特性,可以得出以下解决方式

当一个服务器成功的向 Redis 中设置了该命令,那么就认定为该服务器获得了当前的分布式锁,而其他服务器此时就只能一直等待该服务器释放了锁为止。

那么一个商品秒杀下的情景用代码可以实现成

// 为了演示方便,这里简单定义了一个常量作为商品的id
public static final String PRODUCT_ID = "100001";

public String deductStock() throws InterruptedException {

    // 通过 stringRedisTemplate 来调用 Redis 的 SETNX 命令,key 为商品的id,value的值在这不重要
    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(RODUCT_ID, "jojo");
    if (!result) {
        return "error";
    }

    int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
    if (stock > 0) {
        int readStock = stock - 1;
        stringRedisTemplate.opsForValue().set("stock", realStock + ""); 
        System.out.println("扣减成功,剩余库存:" + readStock + "");
    } else {
        System.out.println("扣减失败,库存不足");
    }

    // 业务执行完成,删除PRODUCT_ID key
    stringRedisTemplate.delete(PRODUCT_ID);
    
    return "end";
}

释放锁

上面这种实现方式先然还存在着很大的问题,比如:当程序成功拿到锁,并且执行完业务逻辑,却没有安全的执行到stringRedisTemplate.delete(PRODUCT_ID),也就是没有成功的释放锁,就会进入死锁状态

或许会下意识想到,可以通过try-finally语法块去解决,将释放锁的语句放进finally语法块中。这种解决方式在学习中可能是个好的办法,但是实际生产中不可控因素很多,比如:当线程在成功加锁之后,执行业务代码时,还没来得及删除 Redis 中的锁标志,此时,这台服务器宕机了,程序并没有想我们想象中地去执行 finally 块中的代码。这种情况也会使得其他服务器或者进程在后续过程中无法去获取到锁,从而导致死锁,最终导致业务崩溃的情况。

Redis超时机制

当我们在锁机制中调用Redis存活时间的设置

stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);

依然可能会产生死锁的问题,因为加锁以及设置过期时间是分开来执行的,并不能保证原子性。所以为了解决这个问题,Redis 中也提供了将设置值与设置过期时间合一的操作

stringRedisTemplate.opsForValue().opsForValue().setIfAbsent(lockKey, "jojo", 10, TimeUnit.SECONDS);

这样解决了原子性的问题,但是如果业务代码在10秒内没有执行完,锁却被释放掉,这种问题该如何解决?这块先留个伏笔,我们先将代码进行抽取,方便后续的修改

代码抽取

RedisLock接口

public interface RedisLock {
    /**
     * 尝试加锁
     */
    boolean tryLock(String key, long timeout, TimeUnit unit);
    
    /**
     * 解锁操作
     */
    void releaseLock(String key);
    
}

接下来,基于已有思路来实现这个接口

public class RedisLockImpl implements RedisLock {
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    @Override
    public boolean tryLock(String key, long timeout, TimeUnit unit) {
        return stringRedisTemplate.opsForValue().setIfAbsent(key, "jojo", timeout, unit);
    }
    
    @Override
    public void releaseLock(String key) {
        stringRedisTemplate.delete(key);
    }
    
}

进一步优化,“加锁&解锁”归一化

上面的代码,除了说过的超时问题没有解决,还有一个更大的问题:其他开发人员有可能在编写代码的时候并没有调用 tryLock() 方法,而是直接调用了 releaseLock() 方法,并且可能在调用 releaseLock() 时传入的 Key 值与你调用 tryLock() 时传入的 Key 值是相同的,那么此时就可能出现问题,另一段代码在运行时,硬生生将你代码中加的锁给释放掉了,那么此时的锁就失效了。面对这个问题,我们不得不对加锁和解锁的操作进行归一化处理。所谓的归一化,就是加锁和解锁的线程必须为同一线程,也就是我的锁不能让老王解开

为了进行归一化的操作,需要借助ThreadLocalUUID

ThreadLocal叫本地线程变量,尽管名字中带有“Thread”,但是它是一个变量而非线程,所以称其为“ThreadLocalVariable”可能更易于理解,其支持泛型,并提供了set和get方法,顾名思义就是其中填充的的是当前线程的变量,该变量对其他线程而言是封闭且隔离的,这为解决避免他人释放我们的锁提供了一种很好的思路

结合ThreadLocal和UUID可以得到以下代码

public class RedisLockImpl implements RedisLock {
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    private ThreadLock<string> threadLock = new ThreadLock<>();
    
    @Override
    public boolean tryLock(String key, long timeout, TimeUnit unit) {
        String uuid = UUID.randomUUID().toString();
        threadlocal.set(uuid);
        return stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
    }
    
    @Override
    public void releaseLock(String key) {
        if (threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))) {
            stringRedisTemplate.delete(key);
        }
    }
    
}

再次优化,自旋锁

在上面的代码中,我们保证了锁不可以被两个人同时拥有,当我们一次性获取到锁,那么就会直接返回失败,这对业务来说是十分不友好的,假设用户此时下单,刚好有另外一个用户也在下单,而且获取到了锁资源,那么该用户尝试获取锁之后失败,就只能直接返回“下单失败”的提示信息的。所以我们需要实现以自旋的形式来获取到锁,即不停的重试

public class RedisLockImpl implements RedisLock {
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    private ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
    private ThreadLocal<Integer> threadLocalInteger = new ThreadLocal<>();
    
    @Override
    public boolean tryLock(String key, long timeout, TimeUnit unit) {
        Boolean isLocked = false;
        
        String uuid = UUID.randomUUID().toString();
        threadLocal.set(uuid);
        isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
        // 尝试获取锁失败,则自旋获取锁直至成功
        if (!isLocked) {
          for (;;) {
            isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
              if (isLocked) {
                break;
              }
          }
        }
        
        return isLocked;
    }
    
    @Override
    public void releaseLock(String key) {
        // 判断当前线程所对应的uuid是否与Redis对应的uuid相同,再执行删除锁操作
        if (threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))) {
            stringRedisTemplate.delete(key);
        }
    }
}

超时优化

(挖坑...)





本篇内容参考 https://www.cnblogs.com/jojop/p/14008824.html#55053489



这篇关于【Redis】分布式锁的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程