图解 Redis 分布式锁,写得太好了!
创始人
2024-04-03 08:41:33
0

分布式锁的演进

基本原理

我们可以同时去一个地方“占坑”,如果占到,就执行逻辑。否则就必须等待,直到释放锁。“占坑”可以去redis,可以去数据库,可以去任何大家都能访问的地方。等待可以自旋的方式。

阶段一

public Map> getCatalogJsonDbWithRedisLock() {        //阶段一        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");        //获取到锁,执行业务        if (lock) {            Map> categoriesDb = getCategoryMap();            //删除锁,如果在此之前报错或宕机会造成死锁            stringRedisTemplate.delete("lock");            return categoriesDb;        }else {            //没获取到锁,等待100ms重试            try {                Thread.sleep(100);            } catch (InterruptedException e) {                e.printStackTrace();            }            return getCatalogJsonDbWithRedisLock();        }    } public Map> getCategoryMap() {        ValueOperations ops = stringRedisTemplate.opsForValue();        String catalogJson = ops.get("catalogJson");        if (StringUtils.isEmpty(catalogJson)) {            System.out.println("缓存不命中,准备查询数据库。。。");            Map> categoriesDb= getCategoriesDb();            String toJSONString = JSON.toJSONString(categoriesDb);            ops.set("catalogJson", toJSONString);            return categoriesDb;        }        System.out.println("缓存命中。。。。");        Map> listMap = JSON.parseObject(catalogJson, new TypeReference>>() {});        return listMap;    }

问题: setnx占好了位,业务代码异常或者程序在页面过程中宕机。没有执行删除锁逻辑,这就造成了死锁

解决: 设置锁的自动过期,即使没有删除,会自动删除。

阶段二

 public Map> getCatalogJsonDbWithRedisLock() {        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");        if (lock) {            //设置过期时间            stringRedisTemplate.expire("lock", 30, TimeUnit.SECONDS);            Map> categoriesDb = getCategoryMap();            stringRedisTemplate.delete("lock");            return categoriesDb;        }else {            try {                Thread.sleep(100);            } catch (InterruptedException e) {                e.printStackTrace();            }            return getCatalogJsonDbWithRedisLock();        }    }

问题: setnx设置好,正要去设置过期时间,宕机。又死锁了。

解决: 设置过期时间和占位必须是原子的。redis支持使用setnx ex命令。

阶段三

public Map> getCatalogJsonDbWithRedisLock() {    //加锁的同时设置过期时间,二者是原子性操作    Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "1111",5, TimeUnit.SECONDS);    if (lock) {        Map> categoriesDb = getCategoryMap();        //模拟超长的业务执行时间        try {            Thread.sleep(6000);        } catch (InterruptedException e) {            e.printStackTrace();        }        stringRedisTemplate.delete("lock");        return categoriesDb;    }else {        try {            Thread.sleep(100);        } catch (InterruptedException e) {            e.printStackTrace();        }        return getCatalogJsonDbWithRedisLock();    }}

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

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

阶段四

public Map> getCatalogJsonDbWithRedisLock() {        String uuid = UUID.randomUUID().toString();        ValueOperations ops = stringRedisTemplate.opsForValue();      //为当前锁设置唯一的uuid,只有当uuid相同时才会进行删除锁的操作        Boolean lock = ops.setIfAbsent("lock", uuid,5, TimeUnit.SECONDS);        if (lock) {            Map> categoriesDb = getCategoryMap();            String lockValue = ops.get("lock");            if (lockValue.equals(uuid)) {                try {                    Thread.sleep(6000);                } catch (InterruptedException e) {                    e.printStackTrace();                }                stringRedisTemplate.delete("lock");            }            return categoriesDb;        }else {            try {                Thread.sleep(100);            } catch (InterruptedException e) {                e.printStackTrace();            }            return getCatalogJsonDbWithRedisLock();        }    }

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

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

阶段五-最终形态

 public Map> getCatalogJsonDbWithRedisLock() {        String uuid = UUID.randomUUID().toString();        ValueOperations ops = stringRedisTemplate.opsForValue();        Boolean lock = ops.setIfAbsent("lock", uuid,5, TimeUnit.SECONDS);        if (lock) {            Map> categoriesDb = getCategoryMap();            String lockValue = ops.get("lock");            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";            stringRedisTemplate.execute(new DefaultRedisScript(script, Long.class), Arrays.asList("lock"), lockValue);            return categoriesDb;        }else {            try {                Thread.sleep(100);            } catch (InterruptedException e) {                e.printStackTrace();            }            return getCatalogJsonDbWithRedisLock();        }    }

保证加锁【占位+过期时间】和删除锁【判断+删除】的原子性。更难的事情,锁的自动续期。

Redisson

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。

其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service)

Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

更多请参考官方文档:

https://github.com/redisson/redisson/wiki

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...
有效的括号 一、题目 给定一个只包括 '(',')','{','}'...
【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...