20220502_Lua脚本操作redis锁学习笔记.md
1概述
释放锁要用 lua 脚本,把检查锁是不是本线程持有的逻辑和删除锁逻辑这连个操作放到一个 lua 脚本中,保证原子性,利用redis的单线程特性, 防止高并发时误删其他线程写入的锁。
考虑这样的时序:
- 如持有锁的线程A,过期了,但业务逻辑还在执行,
- 此时别的线程B获取了锁
- 原先持有锁的线程A业务逻辑执行完成,随手删除了锁(线程B岂不是一脸懵逼)。
所有我们要确保锁的删除合法有效(如删除判断持有者ID、获取基于redisLua脚本保持原子性)。
1.1Lua脚本的独占性
当lua脚本在执行的时候,不会有其他脚本和命令同时执行,这种语义类似于 MULTI/EXEC。
从别的客户端的视角来看,一个lua脚本要么不可见,要么已经执行完。
2代码示例
2.1pom依赖
<!--1.jedis for redis.clients-->
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${jedis.version}</version>
</dependency>
2.2MyJedisLuaDeleteLockUtil
package com.kikop.util;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import redis.clients.jedis.Jedis;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Collections;
/**
* @author kikop
* @version 1.0
* @project myluascriptdemo
* @file MyJedisLuaDeleteLockUtil
* @desc Redislua脚本
* @date 2022/5/2
* @time 9:00
* @by IDE IntelliJ IDEA
*/
public class MyJedisLuaDeleteLockUtil {
// 初始化一次
private static final Jedis jedis;
static {
try {
jedis = new Jedis("localhost", 6379);
} catch (Exception ex) {
throw new RuntimeException("constructor jedis instance failed!");
}
}
// Lua脚本
// 获取key
// 如果 key 不存在那么返回特殊值 nil--> not nil==true
// 返回值:bool
private static final String GET_LOCK_LUA_SCRIPT =
"local lockClientId = redis.call('GET', KEYS[1])\n" +
"if lockClientId == ARGV[1] then\n" + // Keys的值是自己,则重置延迟过期时间
" redis.call('PEXPIRE', KEYS[1], ARGV[2])\n" +
" return true\n" +
"elseif not lockClientId then\n" + // 客户ID不为空,则设置key
" redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])\n" +
" return true\n" +
"end\n" +
"return false"; // 锁已经被别的线程占用
// 删除key
// 脚本的删除,返回被删除key的数量
// 返回值:int
private static final String DELETE_LOCK_LUA_SCRIPT =
"local lockClientId = redis.call('GET', KEYS[1])\n" +
"if lockClientId == ARGV[1] then\n" +
" return redis.call('del',KEYS[1]) \n" +
"elseif not lockClientId then\n" +
" return 0\n" +
"end\n" +
"return 0";
/**
* 通过Lua脚本获取锁
*
* @param key
* @param clientId
* @return
*/
public static boolean acquireLockByLua(String key, String clientId, long expireMs) {
// keys:
// key[1]:key
// args:
// argv[1]:clientId
// argv[2]:expireMS
Object eval = jedis.eval(GET_LOCK_LUA_SCRIPT, // script
Lists.newArrayList(key), // keys
Lists.newArrayList(clientId, String.valueOf(expireMs))); // args
if (null == eval) {
return false;
}
long acquiereResult = (long) eval;
if (acquiereResult == 1) {
return true;
}
return false;
}
/**
* 通过Lua脚本删除锁
*
* @param key
* @param clientId
* @return
*/
public static boolean deleteLockByLua(String key, String clientId) {
// 面试重点,
// redis 分布式锁的面试题的时候,都会注意说
// “释放锁要用 lua 脚本,把检查锁是不是本线程持有和删除锁放到一个 lua 脚本中,
// 防止高并发时误删其他线程写入的锁”。
// keys:
// key[1]:key
// args:
// argv[1]:clientId
long deleteResult = (long) jedis.eval(DELETE_LOCK_LUA_SCRIPT,
Collections.singletonList(key),
Collections.singletonList(clientId));
if (deleteResult >= 1) {
return true;
}
return false;
}
}
2.3测试
package com.kikop;
import com.kikop.util.MyJedisLuaDeleteLockUtil;
import com.kikop.util.MyJedisLuaTimeWindowLimiterUtil;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
/**
* @author kikop
* @version 1.0
* @project myluascriptdemo
* @file MyNormalLuaScriptApplication
* @desc
* @date 2022/5/2
* @time 9:00
* @by IDE IntelliJ IDEA
*/
public class MyNormalLuaScriptApplication {
private static void testClassPath() {
String classPathFile = MyNormalLuaScriptApplication.class.getResource("/").getFile();
System.out.println(classPathFile);
String classPathFile2 = MyNormalLuaScriptApplication.class.getResource("/mytimewindowlimit.lua").getFile();
File file = new File(classPathFile2);
System.out.println(file.getAbsolutePath());
}
public static void main(String[] args) throws IOException {
String key = "my:lock:goods";
String clientId = UUID.randomUUID().toString();
long expireMs = 2 * 60000L; // 2分钟
boolean acquireLockByLuaResult = testGetKey(key, clientId, expireMs);
if (acquireLockByLuaResult) {
System.out.println("获取锁成功");
} else {
System.out.println("获取锁失败");
}
}
/**
* 获取锁
*
* @param key
* @param clientId
* @param expireMs
* @return
*/
private static boolean testGetKey(String key, String clientId, long expireMs) {
boolean acquireLockByLuaResult = MyJedisLuaDeleteLockUtil.acquireLockByLua(key, clientId, expireMs);
return acquireLockByLuaResult;
}
/**
* 删除锁
*
* @param key
* @param clientId
* @return
*/
private static boolean testRemoveKey(String key, String clientId) {
boolean deleteLockByLuaResult = MyJedisLuaDeleteLockUtil.deleteLockByLua(key, clientId);
return deleteLockByLuaResult;
}
/**
* 由持有锁的线程删除锁
*/
private static void testLockOperOk() {
String key = "my:lock:goods";
String clientId = UUID.randomUUID().toString();
long expireMs = 2 * 60000L; // 2分钟
boolean acquireLockByLuaResult = testGetKey(key, clientId, expireMs);
if (acquireLockByLuaResult) {
boolean deleteLockByLuaResult = testRemoveKey(key, clientId);
System.out.println("获取锁成功");
if (deleteLockByLuaResult) {
System.out.println("删除锁成功");
} else {
System.out.println("删除锁失败");
}
} else {
System.out.println("获取锁失败");
}
}
/**
* 由非持有锁的线程删除锁
*/
private static void testLockOperFail() {
String key = "my:lock:goods";
String clientId = UUID.randomUUID().toString();
long expireMs = 2 * 60000L; // 2分钟
boolean acquireLockByLuaResult = testGetKey(key, clientId, expireMs);
if (acquireLockByLuaResult) {
String newkey = String.format("%s001", key);
boolean deleteLockByLuaResult = testRemoveKey(newkey, clientId);
if (deleteLockByLuaResult) {
System.out.println("删除锁成功");
} else {
System.out.println("删除锁失败");
}
} else {
System.out.println("获取锁失败");
}
}
}
[图片上传失败...(image-7043be-1651481526512)]
[图片上传失败...(image-862d06-1651481526513)]
网友评论