84 redis实现分布式锁的原理

1,Redis使用setnx 实现
2,Redisson 分布式锁;
Redis基于 setnx 实现分布式锁原理:
Redis Setnx 实现分布式锁:
Setnx key value
Redis Setnx(SET if Not eXists) 命令在指定的key 不存在时,为key设置指定的值。
设置成功,返回1, 不成功返回0.
Redis具有先天性,能够保证线程安全问题,多个redis客户端最终只有一个Redis客户端设置成功。

Setnx: key=mayiktRedisLock value=1;该key 如果不存在的时候 执行结果返回1 java层面返回true.

Setnx: key=mayiktRedisLock value=1;该key 如果存在的时候 执行结果返回0 java层面返回false.

set之间的区别:
如果该key 不存在的时候,直接创建,如果存在的时候覆盖。
原理:
获取锁原理:
多个redis客户端执行setnx指令,设置一个相同的Rediskey,谁能创建key成功,谁能获取锁。
如果该key已经存在的情况下,在创建的时候就会返回false。
释放原理:
就是删除key.
Redis实现分布式锁如何避=避免死锁的问题?
如果Redis客户端(获取锁的jvm)宕机的话,如何避免死锁的问题?
zk如何避免该问题?先天性解决了该问题。
可以设置过期时间,过期后该key自动删除。

获取到锁的jvm 业务执行时间>过期key的时间如何处理?
续命:开启一个定时任务实现续命,当我们的业务逻辑没有执行完毕的时候,就会延长过期key的时间。
一直不断续命的情况下,也会发生死锁的问题。
设定续命的次数,续命多次如果还没有执行完业务逻辑的情况下,就应该回滚业务,主动释放锁。
如果当前线程已经获取到锁的情况下,不需要重复获取锁,而是直接复用。

如何考虑避免死锁的问题。
对我们的key 设置 设置锁的过期时间,避免死锁的问题。
如何确保该锁是自己创建,被自己删除。
当我们在执行set的时候value为uuid,如果删除的uuid与该uuid值保持一致,则是自己获取的锁,可以被自己删除。
Redis key 过期了,但是业务还没有执行完毕如何处理;
当redis的过期了,应该采取续命设计,继续延长时间,如果续命多次还是失败的情况下,为了避免死锁的问题,应该主动释放锁和当前的事务操作。
相关核心代码:


import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * @ClassName RedisLockImpl
 * @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com
 * @Version V1.0
 **/
@Component
@Slf4j
public class RedisLockImpl implements RedisLock {
    private String redisLockKey = "mayiktLock";
    private Long timeout = 3000L;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private static Map<Thread, RedisLockInfo> lockCacheMap = new ConcurrentHashMap<>();

    private ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();

    @Override
    public boolean tryLock() {
        Thread cuThread = Thread.currentThread();
        RedisLockInfo redisLockInfo = lockCacheMap.get(cuThread);
        if (redisLockInfo != null && redisLockInfo.isState()) {
            log.info("<<重入锁,直接从新获取锁成功>>");
            return true;
        }
        Long startTime = System.currentTimeMillis();
        for (; ; ) {

            // 1.创建setnx
            String lockId = UUID.randomUUID().toString();
            Long expire = 30L;
            Boolean getLock = stringRedisTemplate.opsForValue().setIfAbsent(redisLockKey, lockId, expire, TimeUnit.SECONDS);
            if (getLock) {
                log.info("<<获取锁成功>>");
                // 将该锁缓存到Map集合中 实现重入锁
                lockCacheMap.put(cuThread, new RedisLockInfo(lockId, cuThread, expire));
                return true;
            }
            // 2.继续循环重试获取 判断是否已经超时重试
            long endTime = System.currentTimeMillis();
            if (endTime - startTime > timeout) {
                return false;
            }
            //3.避免频繁重试 调用阻塞方法等待
            try {
                Thread.sleep(100);
            } catch (Exception e) {

            }
        }
    }

    public RedisLockImpl() {
        //开始定时任务实现续命
//        this.scheduledExecutorService.scheduleAtFixedRate(new LifeExtensionThread(), 0, 5, TimeUnit.SECONDS);
    }

    @Override
    public boolean releaseLock() {
        log.info("<<释放锁成功>>");
        RedisLockInfo redisLockInfo = lockCacheMap.get(Thread.currentThread());
        if (redisLockInfo == null) {
            return false;
        }
        boolean state = redisLockInfo.isState();
        if (!state) {
            return false;
        }
        String redisLockId = stringRedisTemplate.opsForValue().get(redisLockKey);
        if (StringUtils.isEmpty(redisLockId)) {
            return false;
        }
        if (!redisLockId.equals(redisLockInfo.getLockId())) {
            log.info("<<非本线程自己的锁,无法删除>>");
            return false;
        }
        Boolean delete = stringRedisTemplate.delete(redisLockKey);
        if (!delete) {
            return false;
        }
        return lockCacheMap.remove(redisLockKey) != null ? true : false;
    }


    /**
     * 续命次数设计
     */
    class LifeExtensionThread implements Runnable {

        @Override
        public void run() {

            lockCacheMap.forEach((k, lockInfo) -> {
                // 判断线程是否为终止状态,如果是为终止状态 则开始对key实现续命
                Thread lockThread = lockInfo.getLockThread();
                if (!lockInfo.isState() && lockThread.isInterrupted()) {
                    log.info("获取锁失败或者当前获取锁线程已经成功执行完方法");
                    return;
                }
                Integer lifeCount = lockInfo.getLifeCount();
                //开始实现续命 为了避免续命为了避免续命多次还是无法释放锁 则应该回滚业务 主动释放锁
                if (lifeCount > 3) {
                    // 移除不在继续续命
                    lockCacheMap.remove(lockThread);
                    // 回滚当前线程事务
                    // 停止该线程
                    return;
                }
                // 开始延长时间
                stringRedisTemplate.expire(redisLockKey, lockInfo.getExpire(), TimeUnit.SECONDS);
            });
        }
    }
}



Redis过期了,但是业务还没有执行完毕如何处理:
采用续命设计:
看门狗线程--续命线程。
获取锁成功后,应该提前开启一个续命的线程,
检测如果当前业务逻辑还没有执行完毕的情况下,应该不断的延迟过期key的时间。
续命设计: 死锁问题,限制次数。
如果续命多次的情况下,还没有释放锁,则,
1,主动回滚当前线程对应的事务。
2,主动释放锁,
3,主动将该线程通知。
全局续命,
开启一个全局的线程,续命所有的过期key,不合理。
局部续命(增量续命)
只要获取锁成功之后,就开启一个定时任务线程续命。
定时任务每次续命间隔的时间至少小于Redis过期key的时间。
每隔10s续命一次
Redisson设计:
key过期的时候30s
每隔10s续命一次,
当redis 过期了,应该采用续命设计,继续延长时间,如果续命多次还是失败的情况下,为了避免死锁的问题,应该主动释放锁和当前事务。
续命设计增量续命方式。

集群问题:
Redis集群,主节点宕机后如何处理?
Redis集群数据同步,采用异步的方式。
优点: 效率比较高。
缺点: 写的操作效率比较高,有可能存在数据不同步的问题。
zk集群数据同步,采用异步同步的方式。
优点: 保证每个子节点的数据的同步。
缺点: 每次做些的操作的效率比较低。
产生背景:
jvm01 连接到主的redis 做setnx操作的时候,异步将数据同步给redis,意味着jvm01获取锁成功,正好主redis宕机了,redis集群自动开启哨兵机制,就会选举从节点中某个redis为主redis,就会出现2个jvm获取锁成功,违背了分布式锁原子性特征。
思考如何解决:
1,redis集群数据同步改为同步的形式,效率偏低。
2,Redis红锁。
原理;、
1,构建Redis集群没有主从之分,Redis节点都可能为主节点;
2,获取锁的时候,当客户端(JVM)会向多个不同的redis服务端执行setnx操作,只要有一半的redis服务器执行成功,则表示锁成功,和zk数据同步思想一样。

zk数据同步是在zk领导节点实现
Redis是有客户端实现。
考虑问题:
需要设置连接redis超时时间5-50毫秒,时间越短越好,能够减少每个集群redis节点过期延迟。
考虑:
为了防止写入某个redis一直阻塞,需要考虑设置一个超时时间,5-50毫秒
如果无法写入的情况下,直接切换到下一个redis实例,为了防止客户端一直阻塞,影响获取锁的成本。
注意:redis集群个数最好是基数3.

实际上就是zk集群方式。

Redis集群中数据同步,采用异步的形式,当我们连接的主角redis做写的操作的时候,会异步的形式将数据同步给其他从redis,从而可以提高效率,使用ap模式
zk集群数据同步,采用同步模式,当我们连接到主的zk节点,做写的操作的时候,会同步的形式将数据同步给其他的zk从节点。有可能会阻塞,效率比较低,但是可以严格保证数据一致性的问题,使用cp模式

在使用redis实现分布式锁的时候,如果主的redis宕机后,有可能其他从的redis节点会选举主redis节点,有可能会发生多个jvm都会获取到该分布式锁,产生问题。

image.png

如何解决该问题呢?
Redisson 采用红锁解决。
需要考虑的问题:

如何客户端给多个redis服务器设置key,总耗时时间>过期key如何处理?

RedLock(红锁)实现原理》
redis的分布式锁算法采用红锁机制,红锁需要至少三个以上Redis独立节点,这些节点相互之间可以不需要存在主从之分,每个redis保证独立即可。

脑裂:
获取锁:
客户单会在每个redis 实例创建锁,只需要满足一半的redis节点能够获取锁成功,就表示加锁成功。
该方案: 导致获取锁的时间成本可能非常高。
原理:
1.客户端使用相同的key,在从所有的Redis节点获取锁。
2,客户端需要设置超时时间。连接redis设置不成功的情况下立即切换到下一个Redis实例,防止一直阻塞。
3,客户端需要计算获取锁的总耗时,客户端至少需要有N/2+1节点获取锁成功,且总耗时时间小于锁的过期时间才能获取锁成功。
4,如果客户端最终获取锁失败,必须所有节点释放锁。

RedLock(红锁)环境搭建
构建Redis集群环境
不需要设置redis集群的主从关系。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355

推荐阅读更多精彩内容