手撕redis分布式锁,隔壁张小帅都看懂了!

前言

上一篇老猫和小伙伴们分享了为何要使用分布式锁以及分布式锁的实现思路原理,目前咱们主要采用第三方的组件做为分布式锁的工具。上一篇运用了Mysql中的select ...for update实现了分布式锁,可是咱们说这种实现方式并不经常使用,由于当大并发量的时候,会给数据库带来比较大的压力。固然也有小伙伴给老猫留言说“ 在quartz的集群模式中,就是使用了基于mysql的分布式锁,select for update ”。没错,其实quartz的集群模式中,任务执行的节点个数是可预知的,并且没有那么大的量级,因此是没有问题的。可是若是像千万级别的并发秒杀场景的状况下,那么这种方案实际上是不可行的。由于mysql操做是须要IO的,IO的速度比内存速度慢,所以mysql若是在那种场景下使用的话是会存在系统瓶颈的。因此本篇就和小伙伴们分享基于内存操做的比较经常使用的分布式锁——redis分布式锁。java

手撸Redis分布式锁

实现原理

redis分布式锁实现原理其实也是比较简单的,主要是依赖于redis的 set nx命令,咱们来看一下完整的设置redis的命令:“Set resource_name my_random_value NX PX 30000”。看到这串命令,了解redis的小伙伴应该都看得懂这条命令是在redis中存入一个带有过时时间的值。具体上述设值语句解释以下:mysql

  1. resource_name:资源名称,能够根据不一样的业务区分不一样的锁。(其实就是对应咱们上一篇myql锁中的business_code)。
  2. my_random_value:随机值,每一个线程的随机值都不相同,主要用于释放锁的时候用来校验。
  3. NX:key不存在的时候设置成功,key存在则设置不成功。
  4. PX:自动失效时间,若是出现异常状况,锁能够过时实现,所以达到了自动释放。

那么为何可使用这个思路呢?其实很简单,主要就是利用了set nx的原子性,在多个线程并发执行时,只有一个线程能够设置成功,若是设置成功,那么就表明着得到了锁,就能够执行后续的业务。若是出现了异常,过了锁的有效期,锁会自动释放,释放锁主要采用了redis的delete命令,释放锁以前会校验当前redis存储的随机数,只有当前的随机数和存储的随机数一致的时候才容许释放。具体的redis的删除,咱们能够经过lua脚本进行删除,具体Lua脚本以下:git

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

那么咱们为何要采用这种方式释放锁呢?其实使用这种方式释放锁能够避免删除别的客户端获取成功的锁 。程序员

以下图:github

redis释放锁

客户端A取得资源锁,可是紧接着被一个其余操做阻塞了,当客户端A运行完毕其余操做后要释放锁时,原来的锁早已超时而且被Redis自动释放,而且在这期间资源锁又被客户端B再次获取到。若是仅使用DEL命令将key删除,那么这种状况就会把客户端B的锁给删除掉。使用Lua脚本就不会存在这种状况,由于脚本仅会删除value等于客户端A的value的key(value至关于客户端的一个签名)(说明:其实这些例子在redis的官网都有介绍)。redis

代码实现方式

老猫对redis锁机制进行了相关的抽取,而且封装成了工具类,核心工具类代码以下:sql

/**
 * @author kdaddy@163.com
 * @date 2021/1/7 22:36
 * 公众号“程序员老猫”
 */
@Service
public class RedisLockUtil {

    @Autowired
    private RedisTemplate redisTemplate;

    private String value = UUID.randomUUID().toString();

    public Boolean lock(String key){
        RedisCallback<Boolean> redisCallback = redisConnection -> {
            //表示set nx 存在key的话就不设置,不存在则设置
            RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
            //设置过时时间
            Expiration expiration = Expiration.seconds(30);
            byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
            byte[] redisValue = redisTemplate.getKeySerializer().serialize(value);
            Boolean result = redisConnection.set(redisKey,redisValue,expiration,setOption);
            return result;
        };
        //获取分布式锁
        Boolean lock = (Boolean)redisTemplate.execute(redisCallback);
        return lock;
    }
    //释放分布式锁
    public Boolean releaseLock(String key){
        String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
                "    return redis.call(\"del\",KEYS[1])\n" +
                "else\n" +
                "    return 0\n" +
                "end";
        RedisScript<Boolean> redisScript = RedisScript.of(script,Boolean.class);
        List<String> keys = Arrays.asList(key);

        boolean result = (Boolean) redisTemplate.execute(redisScript,keys,value);
        return result;
    }
}

固然相关的业务代码,老猫仍是使用了以前并发扣减库存的例子,在此相关的代码以及最终运行的结果也不一一进行举例。小伙伴们能够自行去老猫的github获取相关的示例源码信息,而后运行一下便可。github地址:https://github.com/maoba/kd-distribute。代码已经完成了更新。数据库

Redisson分布式锁

介绍和使用

那么Redisson究竟为什么物呢?Redisson 是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。 充分的利用了Redis键值数据库提供的一系列优点,基于Java实用工具包中经常使用接口,为使用者提供了一系列具备分布式特性的经常使用工具类。使得本来做为协调单机多线程并发程序的工具包得到了协调分布式多机多线程并发系统的能力,大大下降了设计和研发大规模分布式系统的难度。同时结合各富特点的分布式服务,更进一步简化了分布式环境中程序相互之间的协做。 (摘自redisson官网:https://redisson.org/)安全

下面咱们来看一下具体用redisson实现分布式锁实战,实际上是至关简单的,redisson已经给咱们进行了相关的封装,咱们开箱即用。多线程

/**
 * @author kdaddy@163.com
 * @date 2021/1/9 14:23
 * @公众号“程序员老猫”
 */
public  Integer createOrder() throws Exception{
    log.info("进入了方法");
    Config config = new Config();
    config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("ktdaddy");
    RedissonClient redissonClient = Redisson.create(config);
    RLock rlock = redissonClient.getLock(ORDER_KEY);
    rlock.lock(30, TimeUnit.SECONDS);

    try {
        log.info("拿到了锁");
        //....具体能够参考老猫的github
        return order.getId();
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        rlock.unlock();
    }
    return null;
}

原理

redisson简单架构

老猫上文中本身实现redis锁的时候用到了lua脚本,redisson实现的时候其实全部的指令都是经过lua脚本去实现的。上述为redisson的简单架构图,画的比较粗糙。老猫稍微做一下解释。上图中有个看门狗(watchdog)概念。其实这就是一个定时任务,在线程获取锁以后,它会每隔10s帮忙将key的超时时间设置为30s,这样就不会出现线程一直持有锁从而影响其余线程获取锁的问题。小伙伴们能够发现该功能其实就是set px,只是换成了定时任务去实现。固然看门狗的存在保证了出现死锁的状况下会自动释放。

以上只是针对redisson作了一个简单的应用介绍,redisson实际上是至关强大的,首先说配置,老猫上述链接redis的方式其实很简单,因为搭建的是单机redis,因此就使用了单机redis的链接方式,固然redisson还支持主从、哨兵、集群等等链接方式;固然锁的种类也至关丰富,以上老猫提供的是可重入锁的流程。其实还包括公平锁、联锁、红锁、读写锁等等,另外的redisson对分布式的容器、队列等等进行了特有的封装,包括分布式的Blocking Queue、分布式Map、分布式Set、分布式List等等。redisson的强大之处老猫在此不一一枚举,有兴趣的小伙伴能够深刻研究一下。

缺陷

redis锁能够比较完美地解决高并发的时候分布式系统的线程安全性的问题,可是这种锁机制也并非完美的。在哨兵模式下,客户端对master节点加了锁,此时会异步复制给slave节点,此时若是master发生宕机,主备切换,slave变成了master。由于以前是异步复制,因此此时正好又有个线程来尝试加锁的时候,就会致使多个客户端对同一个分布式锁完成了加锁操做,这时候业务上会出现脏数据了。关于redis的相关知识,你们能够访问老猫以前的一些文章,包括redis的哨兵模式、持久化等等。

写在最后

本篇主要和小伙伴们分享了redis锁,从老猫本身实现的乞丐版的redis锁到大牛实现的redisson。相信你们也会有必定的收货。其实关于分布式锁,出了redis锁以外还有基于zookeeper的实现。后续老猫会整理而且分享给你们,敬请期待。

固然更多技术干货也欢迎你们搜索关注公众号“程序员老猫”

相关文章
相关标签/搜索