先来看看之前分布式锁的实现。
这个基于Redis的分布式锁仍然有着一个问题,那就是误删锁的问题。
简单的来说,就是当第一个线程,也就是线程1,拿到锁后,但由于本身业务复杂,而导致了阻塞,超过了锁设置的超时时间,锁自动释放。这个时候,线程2进来了,也拿到了锁,但是就在线程2执行业务的途中,线程1业务完成,主动释放了锁,又因为我们释放锁的逻辑是直接删除key,这就导致了线程2的锁被误删。
这就导致了线程安全的问题。
解决方法:在每个线程要释放锁的时候,主动判断reids中存入的线程标识,来判断是不是自己的锁,如果不是,就不能删除。
代码实现:
- package com.hmdp.utils;
-
- import cn.hutool.core.collection.CollectionUtil;
- import cn.hutool.core.lang.UUID;
- import org.springframework.core.io.ClassPathResource;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.data.redis.core.script.DefaultRedisScript;
- import org.springframework.util.CollectionUtils;
-
- import java.util.Collections;
- import java.util.concurrent.TimeUnit;
-
- /**
- * @Author 华子
- * @Date 2022/12/3 20:53
- * @Version 1.0
- */
- public class SimpleRedisLock implements ILock {
-
- //Redis
- private StringRedisTemplate stringRedisTemplate;
- //业务名称,也就是锁的名称
- private String name;
- public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
- this.stringRedisTemplate = stringRedisTemplate;
- this.name = name;
- }
-
- //key的前缀
- private static final String KEY_PREFIX = "lock:";
- //线程标识的前缀
- private static final String ID_PREFIX = UUID.randomUUID().toString(true);
-
-
- @Override
- public boolean tryLock(long timeoutSec) {
- //获取线程id,当作set的value
- String threadId = Thread.currentThread().getId()+ID_PREFIX;
-
- Boolean success = stringRedisTemplate.opsForValue()
- .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
- return Boolean.TRUE.equals(success);
- }
-
-
- //释放锁
- @Override
- public void unlock() {
- String threadId = Thread.currentThread().getId()+ID_PREFIX;
- String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
- if (threadId.equals(id)){
- //删除key
- stringRedisTemplate.delete(KEY_PREFIX+name);
- }
- }
- }
-
上述代码仍然会在极端的情况下仍然会出现误删锁的问题。
试想一下这种情况:
在线程判断完线程标识时,发现是自己的id,就在准备释放锁的时候,发生了阻塞。然后就是锁超时时间到了,别的线程有获取到了锁,又出现了误删的问题。
那么,又该如何解决这个问题呢?
我们不妨这样想想:
就是我们把判断id和删除id整合成一个代码,让他一次执行,不用分成两次,这样不就好了?
这里就要使用我们Redis中的脚本:Lua 。
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:Lua 教程 | 菜鸟教程
这里重点介绍Redis提供的调用函数,语法如下:
例如,我们要执行set name jack,则脚本是这样:
- -- 执行reids命令
- redis.call('set','name','jack')
使用redis来运行脚本
例如,我们要使用Redis调用脚本来执行set name jack,则脚本是这样:
- EVAL "return redis.call('set', 'name', 'jack')" 0
-
注:后面的0是参数个数。
如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:
- EVAL "return redis.call('set',KEYS[1],ARGV[1])" 1 name Rose
-
注:我们实际的业务就是需要这种,参数不能写死。
接下来,就是用Lua脚本编写代码逻辑
代码 :
- -- 锁的key
- -- local key = KEYS[1]
-
- -- 线程标识
- -- local threadId = ARGV[1];
-
- --获取锁中的线程标识,get key
- -- local id = redis.call('get',KEYS[1]);
-
- --具体脚本
- if(redis.call('get',KEYS[1]) == ARGV[1]) then
- --释放锁 del key
- return redis.call('del',KEYS[1])
- end
- return 0
然后在IDEA中编写此代码,并使用Redis调用。(注:编写Lua代码需下载EmmyLua插件)
具体实现代码:(主要看释放锁代码)
- package com.hmdp.utils;
-
- import cn.hutool.core.collection.CollectionUtil;
- import cn.hutool.core.lang.UUID;
- import org.springframework.core.io.ClassPathResource;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.data.redis.core.script.DefaultRedisScript;
- import org.springframework.util.CollectionUtils;
-
- import java.util.Collections;
- import java.util.concurrent.TimeUnit;
-
- /**
- * @Author 华子
- * @Date 2022/12/3 20:53
- * @Version 1.0
- */
- public class SimpleRedisLock implements ILock {
-
- //Redis
- private StringRedisTemplate stringRedisTemplate;
- //业务名称,也就是锁的名称
- private String name;
- public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
- this.stringRedisTemplate = stringRedisTemplate;
- this.name = name;
- }
-
- //key的前缀
- private static final String KEY_PREFIX = "lock:";
- //线程标识的前缀
- private static final String ID_PREFIX = UUID.randomUUID().toString(true);
-
- //获取lua脚本
- private static final DefaultRedisScript
UNLOCK_SCRIPT; - static {
- UNLOCK_SCRIPT = new DefaultRedisScript<>();
- UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
- UNLOCK_SCRIPT.setResultType(Long.class);
- }
-
- @Override
- public boolean tryLock(long timeoutSec) {
- //获取线程id,当作set的value
- String threadId = Thread.currentThread().getId()+ID_PREFIX;
-
- Boolean success = stringRedisTemplate.opsForValue()
- .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
- return Boolean.TRUE.equals(success);
- }
-
- @Override
- public void unlock() {
- //调用Lua脚本
- stringRedisTemplate.execute(
- UNLOCK_SCRIPT,
- Collections.singletonList(KEY_PREFIX + name),
- Thread.currentThread().getId()+ID_PREFIX
- );
- }
-
-
- }
主要还是使用了StringRedisTemplate中的excute方法,这里传一个脚本参数,一个key参数,一个value参数
1. 脚本参数:使用DefaultRedisScript 进行获取脚本,需要传入脚本文件的路径,调用new ClassPathResource("unlock.lua") 当做脚本文件路径
2. key参数,就是我们的KEYS[1],是一个集合,我们把我们使用的锁的key转成一个集合当成key参数
3. value,就是我们的线程标识。
这样,我们就使用的Lua脚本将原来两步需要实现的释放锁,合成一行代码实现,就不会出现问题啦~