The main contents of this article are as follows:
synchronzied or
lock ) to lock its own thread resources to prevent
cache breakdown.
local locking method, which will cause data inconsistency in
distributed situations: for example, after service A obtains data, it updates the cache key =100, service B is not subject to the lock restriction of service A, and concurrently updates the cache key = 99. The final result may be 99 or 100, but this is an unknown state,
is inconsistent with the expected result . The flow chart is as follows:
Based on the above local lock problem, we need a support distributed cluster environment Lock under : When querying the DB, only one thread can access it, and other threads need to wait for the first thread to release the lock resource before they can continue execution.
Cases in life: The lock can be regarded as a lock
outside the door, and all concurrent threads are compared to people
, they Everyone wants to enter the room, but only one person can enter the room. When someone enters, lock the door and others must wait until the person who entered comes out.
Let’s take a look at the basic principles of distributed locks, as shown in the figure below:
Let’s analyze it Distributed lock in the picture above:
Explanation in vernacular: All requested threads go to the same place"Occupy the pit"
. If there is a pit, the business logic will be executed. If there is no pit, You need other threads to release the "pit". This pit is visible to all threads. You can put this pit in the Redis cache or database. This article talks about how to use Redis to make "distributed pits"
.
As a publicly accessible place, Redis can be used as a place to "take advantage of".
Several solutions for implementing distributed locks using Redis, we all use the SETNX command (setting key equal to a certain value). Only the number of parameters passed in the high-end scheme is different, and abnormal situations are taken into account.
Let’s take a look at this command. SETNX
is the abbreviation of set If not exist
. This means that when the key does not exist, set the value of the key, and when it exists, do nothing.
This is how it is executed in the Redis command line:
set <key> <value> NX
We can enter the redis container to try the SETNX
command.
Enter the container first:
docker exec -it <容器 id> redis-cli
然后执行 SETNX 命令:将 wukong
这个 key 对应的 value 设置成 1111
。
set wukong 1111 NX
返回 OK
,表示设置成功。重复执行该命令,返回 nil
表示设置失败。
我们先用 Redis 的 SETNX 命令来实现最简单的分布式锁。
我们来看下流程图:
代码示例如下,Java 中 setnx 命令对应的代码为 setIfAbsent
。
setIfAbsent 方法的第一个参数代表 key,第二个参数代表值。
// 1.先抢占锁 Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "123"); if(lock) { // 2.抢占成功,执行业务 List<TypeEntity> typeEntityListFromDb = getDataFromDB(); // 3.解锁 redisTemplate.delete("lock"); return typeEntityListFromDb; } else { // 4.休眠一段时间 sleep(100); // 5.抢占失败,等待锁释放 return getTypeEntityListByRedisDistributedLock(); }
一个小问题:那为什么需要休眠一段时间?
因为该程序存在递归调用,可能会导致栈空间溢出。
The reason why bronze is called bronze is because it is the most elementary and will definitely cause many problems.
Imagine a family scene: At night, Xiao Kong unlocks the door alone and enters the room, turns on the light?, and then suddenly the power is cut off, Xiao Kong wants to open the door and go out. But if the door lock position cannot be found, Xiao Ming cannot get in, and neither can anyone outside.
deadlock. .
So how to avoid this risk?
Set the lock'sautomatic expiration time. After a period of time, the lock will be automatically deleted so that other threads can acquire the lock.
hourglass countdown⏳ for the smart lock. After the hourglass was completed, the door The lock opens automatically. Even if there is a sudden power outage in the room, the lock will automatically open after a while and others can come in.
清理 redis key 的代码如下
// 在 10s 以后,自动清理 lock redisTemplate.expire("lock", 10, TimeUnit.SECONDS);
完整代码如下:
// 1.先抢占锁 Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "123"); if(lock) { // 2.在 10s 以后,自动清理 lock redisTemplate.expire("lock", 10, TimeUnit.SECONDS); // 3.抢占成功,执行业务 List<TypeEntity> typeEntityListFromDb = getDataFromDB(); // 4.解锁 redisTemplate.delete("lock"); return typeEntityListFromDb; }
白银方案看似解决了线程异常或服务器宕机造成的锁未释放的问题,但还是存在其他问题:
因为占锁和设置过期时间是分两步执行的,所以如果在这两步之间发生了异常,则锁的过期时间根本就没有设置成功。
所以和青铜方案有一样的问题:锁永远不能过期。
上面的白银方案中,占锁和设置锁过期时间是分步两步执行的,这个时候,我们可以联想到什么:事务的原子性(Atom)。
原子性:多条命令要么都成功执行,要么都不执行。
将两步放在一步中执行:占锁+设置锁过期时间。
Redis 正好支持这种操作:
# 设置某个 key 的值并设置多少毫秒或秒 过期。 set <key> <value> PX <多少毫秒> NX 或 set <key> <value> EX <多少秒> NX
然后可以通过如下命令查看 key 的变化
ttl <key>
下面演示下如何设置 key 并设置过期时间。注意:执行命令之前需要先删除 key,可以通过客户端或命令删除。
# 设置 key=wukong,value=1111,过期时间=5000ms set wukong 1111 PX 5000 NX # 查看 key 的状态 ttl wukong
执行结果如下图所示:每运行一次 ttl 命令,就可以看到 wukong 的过期时间就会减少。最后会变为 -2(已过期)。
黄金方案和白银方案的不同之处:获取锁的时候,也需要设置锁的过期时间,这是一个原子操作,要么都成功执行,要么都不执行。如下图所示:
设置 lock
的值等于 123
,过期时间为 10 秒。如果 10
秒 以后,lock 还存在,则清理 lock。
setIfAbsent("lock", "123", 10, TimeUnit.SECONDS);
我们还是举生活中的例子来看下黄金方案的缺陷。
123
. 123
, and the expiration time was set to 10 seconds
. resulting in a conflict
. 15 s
, while user B was still executing the task. 123
. We can know from the above case that because the time required for user A to process the task is greater than the time for automatic lock cleaning (unlocking), so before the automatic unlocking Later, another user seized the lock. When user A completes the task, he will actively open the locks seized by other users.
Why are other people’s locks opened here? Because the lock numbers are all called "123"
, user A only recognizes the lock number and opens the lock when he sees the lock numbered "123"
. As a result, user B's lock is opened. , user B has not completed the task at this time, and of course he is angry.
The defects of the above gold plan can also be easily solved. Wouldn't it be nice to set a different number for each lock~
As shown in the figure below, the lock preempted by B is blue, which is different from the green lock preempted by A. This way it won't be opened by A. Made an animated picture for easy understanding: The static picture is more high-definition, you can take a look:// 1.生成唯一 id String uuid = UUID.randomUUID().toString(); // 2. 抢占锁 Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 10, TimeUnit.SECONDS); if(lock) { System.out.println("抢占成功:" + uuid); // 3.抢占成功,执行业务 List<TypeEntity> typeEntityListFromDb = getDataFromDB(); // 4.获取当前锁的值 String lockValue = redisTemplate.opsForValue().get("lock"); // 5.如果锁的值和设置的值相等,则清理自己的锁 if(uuid.equals(lockValue)) { System.out.println("清理锁:" + lockValue); redisTemplate.delete("lock"); } return typeEntityListFromDb; } else { System.out.println("抢占失败,等待锁释放"); // 4.休眠一段时间 sleep(100); // 5.抢占失败,等待锁释放 return getTypeEntityListByRedisDistributedLock(); }
上面的方案看似很完美,但还是存在问题:第 4 步和第 5 步并不是原子性的。
时刻:0s。线程 A 抢占到了锁。
时刻:9.5s。线程 A 向 Redis 查询当前 key 的值。
时刻:10s。锁自动过期。
时刻:11s。线程 B 抢占到锁。
时刻:12s。线程 A 在查询途中耗时长,终于拿多锁的值。
时刻:13s。线程 A 还是拿自己设置的锁的值和返回的值进行比较,值是相等的,清理锁,但是这个锁其实是线程 B 抢占的锁。
那如何规避这个风险呢?钻石方案登场。
上面的线程 A 查询锁和删除锁的逻辑不是原子性
的,所以将查询锁和删除锁这两步作为原子指令操作就可以了。
如下图所示,红色圈出来的部分是钻石方案的不同之处。用脚本进行删除,达到原子操作。
那如何用脚本进行删除呢?
我们先来看一下这段 Redis 专属脚本:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
这段脚本和铂金方案的获取key,删除key的方式很像。先获取 KEYS[1] 的 value,判断 KEYS[1] 的 value 是否和 ARGV[1] 的值相等,如果相等,则删除 KEYS[1]。
那么这段脚本怎么在 Java 项目中执行呢?
分两步:先定义脚本;用 redisTemplate.execute 方法执行脚本。
// 脚本解锁 String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"; redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
上面的代码中,KEYS[1] 对应“lock”
,ARGV[1] 对应 “uuid”
,含义就是如果 lock 的 value 等于 uuid 则删除 lock。
而这段 Redis 脚本是由 Redis 内嵌的 Lua 环境执行的,所以又称作 Lua 脚本。
那钻石方案是不是就完美了呢?有没有更好的方案呢?
下篇,我们再来介绍另外一种分布式锁的王者方案:Redisson。
This article introduces the problem of distributed lock through the problem of local lock. Then it introduces five distributed lock solutions, and explains the improvements of different solutions from shallow to deep.
From the continuous evolution of the above solutions, we know where abnormal situations may exist in the system and how to handle them better.
Draw inferences from one example, and this evolving thinking model can also be applied to other technologies.
The following summarizes the shortcomings and improvements of the above five solutions.
Bronze Solution:
Silver Solution:
Golden Solution:
Platinum Solution:
Diamond Plan:
The King’s Plan, see you in the next article~
The above is the detailed content of Redis distributed lock|Five evolution plans from bronze to diamond. For more information, please follow other related articles on the PHP Chinese website!