分布式锁

通过本文档,你将会了解到

  • 本地锁不香么?你搞什么分布式锁?
  • 分布式锁的产生背景?
  • 分布式锁怎么玩?以及玩起来的注意事项

1、 回顾

上次我们讲到了,本地缓存的缺陷,所以喽,直接用一个缓存中间件redis喽。关于怎么使用,就不扯了,大家都会。接下来主要讲的是我们项目中使用到的分布式锁。

2、锁

2.1、本地锁

本地锁

说到加锁 我们有很多方式,比如synchronized,JUC包下的各种各样的锁,但是这种锁只能锁住当前进程,我们部署10台机器,本地锁就玩不转了。所以分布式锁说那我来吧,这也是我们项目中使用到的。我们所有的机器都先去占坑,如果占坑成功,那么你想怎么样就怎么样。

所以喽要使用同一个锁,我们使用redis分布式锁,当然也有ZK锁。我们讲我们项目中使用到的redis分布式锁。我们可以同时去一个地方“占坑”,如果占到,就执行逻辑。否则就必须等待,直到释放锁。 “占坑”可以去redis,可以去数据库,可以去任何大家都能访问的地方。 等待可以自旋的方式。

上次我们讲到缓存击穿的时候说到过:加锁。大量并发只让一个人去查,其他人等待,查到之后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去查数据库

2.2、分布式锁

2.2.1、介绍

分布式锁的原理就是占坑呗,所有的人都想上厕所,那谁占到坑,那谁上喽,分布式锁大概就是这个原理搞的。

分布式锁

废话不多少,直接上菜呗,Redis里面那个是占坑的?说到学习一门新的知识,那么权威肯定是官网喽。
redis官网

http://www.redis.cn/
http://www.redis.cn/commands/set.html

有个命令比较有特色就是set命令 这个就是占坑的命令。这个坑占起来还有讲究,hahah
SET 里面放个key 一个value 这个后面可是有可选参数哦,有的是过期时间,有一个是NX,所以综上所述
占坑命令就是
set lock haha NX

SET key value [EX seconds] [PX milliseconds] [NX|XX]

EX seconds – Set the specified expire time, in seconds.
PX milliseconds – Set the specified expire time, in milliseconds.
NX – Only set the key if it does not already exist.
XX – Only set the key if it already exist.
EX seconds – 设置键key的过期时间,单位时秒
PX milliseconds – 设置键key的过期时间,单位时毫秒
NX – 只有键key不存在的时候才会设置key的值
XX – 只有键key存在的时候才会设置key的值


注意: 由于SET命令加上选项已经可以完全取代SETNX, SETEX, PSETEX的功能,所以在将来的版本中,redis可能会不推荐使用并且最终抛弃这几个命令。

2.2.2、分布式锁的演进过程

巧了,我懂了,直接用set lock haha NX 分布式锁搞的,分享结束。你觉得会是这么简单么?不要太天真,到时候发现小丑竟是我自己hahaha。

2.2.2.1、青铜玩家

来上菜。我们先看青铜玩家怎么玩?

Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "11111");
if (lock) {
 
    try {
        //加锁成功...执行业务
        XXXXXXX
        //删除
        stringRedisTemplate.delete(XXXXXX);
    }else{
        //重试 自旋
        1.自己调取自己
        2.可以休眠1S
    }

你看看青铜玩家,一看就是新手。一眼就看出来的错误就是异常了没人解锁了。
解决:

  • 设置锁的自动过期,即使没有删除,会自动删除。
  • try 包裹 finally解锁

2.2.2.2、白银玩家

 Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);

if (lock) {
 
    try {
        //加锁成功...执行业务
    }else{
        //重试 自旋
        1.自己调取自己
        2.可以休眠1S
    } finally{
     //删除
        stringRedisTemplate.delete(XXXXXX);
}

你看看白银玩家,有点那么个意思了是吧?那白银玩家有没有问题呢?阶段才是白银了,那肯定是有问题的,关键是问题在哪里?

1、删除锁直接删除???
如果由于业务时间很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了。

2、解决:

  • 占锁的时候,值指定为uuid,每个人匹配是自己的锁才删除。。

2.2.2.2、黄金玩家

黄金玩家

关键是问题在哪里?

问题:
1、如果正好判断是当前值,正要删除锁的时候,锁已经过期, 别人已经设置到了新的值。那么我们删除的是别人的锁。

2、解决:

  • 删除锁必须保证原子性。使用redis+Lua脚本完成

2.2.2.2、钻石玩家

//1、占分布式锁。去redis占坑      设置过期时间必须和加锁是同步的,保证原子性(避免死锁)
String uuid = UUID.randomUUID().toString();
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if (lock) {
    System.out.println("获取分布式锁成功...");
    Map> dataFromDb = null;
    try {
        //加锁成功...执行业务
        dataFromDb = getDataFromDb();
    } finally {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

        //删除锁
        stringRedisTemplate.execute(new DefaultRedisScript(script, Long.class), Arrays.asList("lock"), uuid);

    }

当然线上你可以这么写,但是毕竟是钻石玩家,我们还有更好的玩法。

2.3、Redisson 作为分布式锁

这个也是我们项目中使用到的。废话不多说,第一步打开官网,开启寻宝之路。

https://www.redis.io/topics/distlock

分布式锁的介绍页面


分布式锁

2.3.1 Redisson 是什么

一种 可重入、持续阻塞、独占式的 分布式锁协调框架。
Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

特点:

  • 可重入
    拿到锁的线程后续拿锁可跳过获取锁的步骤,只进行value+1的步骤。
  • 持续阻塞
    获取不到锁的线程,会在一定时间内等待锁。
  • 独占式
    同一环境下理论上只能有一个线程可以获取到锁

比较爽的一点是什么?
基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。

这句话什么意思?如果你已经了解了JUC包下的各种锁,什么ReentrantLock,信号量什么的。恭喜你无缝切换。就像你换手机一样,iPhone12换iPhone13 像流水切换一样自然。以前怎么用,现在还怎么用。

关于并发编程JUC的知识不在本次分享范围内。

      //1、获取一把锁,只要锁的名字一样,就是同一把锁
        RLock myLock = redisson.getLock("my-lock");

        //2、加锁
        myLock.lock();      //阻塞式等待。默认加的锁都是30s
        //1)、锁的自动续期,如果业务超长,运行期间自动锁上新的30s。不用担心业务时间长,锁自动过期被删掉
        //2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题
        // myLock.lock(10,TimeUnit.SECONDS);   //10秒钟自动解锁,自动解锁时间一定要大于业务执行时间
        //问题:在锁时间到了以后,不会自动续期
        //1、如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是 我们制定的时间
        //2、如果我们指定锁的超时时间,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】
        //只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒都会自动的再次续期,续成30秒
        // internalLockLeaseTime 【看门狗时间】 / 3, 10s
        try {
            System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
            try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            //3、解锁  假设解锁代码没有运行,Redisson会不会出现死锁
            System.out.println("释放锁..." + Thread.currentThread().getId());
            myLock.unlock();
        }


关于看门狗,就是业务执行需要10秒,还没执行完就解锁了?看门狗自动帮你续命时间(这个是一个定时任务,没10秒执行一次看看要不要续命)当前线程活着就续命,线程(而key的组成应该是:{uuid}:{threadid})只有没有指定过去时间才会启动。

最佳实践:我们指定过期时间,性能会有一点增加,指定3秒过期时间,3秒都没执行完,那早就超时了什么的。

你可能感兴趣的:(分布式锁)