我们在Springboot项目中分别整合了redis和redission框架。
下边我记录一下再框架中分别使用redis和redission实现分布式锁的代码。
一:redis-lua脚本实现分布式锁
lua 本身是不具备原子性的, 但由于Redis的命令是单线程执行的,它会把整个Iua脚本作为一个命令执行,会阻塞其间接受到的其他命令,这就保证了lua脚本的原子性。
Lua 脚本好处:
1) 原子性:Lua脚本的所有命令在执行过程中是原子的,避免了并发修改带来的问题。
2) 减少网络往返次数:通过在服务器端执行脚本,减少了客户端和服务器之间的网络往返次数,提高了性能。
3) 复杂操作:可以在Lua脚本中执行复杂的逻辑,超过了单个Redis命令的能力。
lua 脚本使用注意点
1) 由于Redis执行lua脚本其间,无法处理其他命令,因此如果lua脚本的业务过于复杂,则会产生长时间的阻塞,因此编写Lua脚本时应尽量保持简短和高效。
2) Redis默认限制lua执行脚本时间为5s,如果超过这个时间则会终止且抛错,可以通过lua-time-limit调整时长。
但是,redis-lua分布式锁有一个小小的问题,他不具备可重入性。针对这个问题,我对redis-lua分布式锁实现了封装。代码如下所示:
RedisLuaUtils.java
package com.modules.redis.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* @author camellia
* redis 加锁工具类
*/
@Slf4j
public class RedisLuaUtils
{
/**
* 超时时间(毫秒)
*/
private static final long TIMEOUT_MILLIS = 15000;
/**
* 重试次数
*/
private static final int RETRY_TIMES = 10;
/***
* 睡眠时间(重试间隔)
*/
private static final long SLEEP_MILLIS = 100;
/**
* 用来加锁的lua脚本
* 因为新版的redis加锁操作已经为原子性操作
* 所以放弃使用lua脚本
*/
private static final String LOCK_LUA =
"if redis.call("setnx",KEYS[1],ARGV[1]) == 1 " +
"then " +
" return redis.call('expire',KEYS[1],ARGV[2]) " +
"else " +
" return 0 " +
"end";
/**
* 用来释放分布式锁的lua脚本
* 如果redis.get(KEYS[1]) == ARGV[1],则redis delete KEYS[1]
* 否则返回0
* KEYS[1] , ARGV[1] 是参数,我们只调用的时候 传递这两个参数就可以了
* KEYS[1] 主要用來传递在redis 中用作key值的参数
* ARGV[1] 主要用来传递在redis中用做 value值的参数
*/
private static final String UNLOCK_LUA =
"if redis.call("get",KEYS[1]) == ARGV[1] "
+ "then "
+ " local number = redis.call("del", KEYS[1]) "
+ " return tostring(number) "
+ "else "
+ " return tostring(0) "
+ "end ";
/**
* 检查 redisKey 是否上锁(没加锁返回加锁)
*
* @param redisKey redisKey
* @param template template
* @return Boolean
*/
public static Boolean isLock(String redisKey, String value, RedisTemplate<Object, Object> template)
{
return lock(redisKey, value, template, RETRY_TIMES);
}
private static Boolean lock(String redisKey, String value, RedisTemplate<Object, Object> template, int retryTimes)
{
boolean result = lockKey(redisKey, value, template);
// 循环等待上一个用户锁释放,或者锁超时释放
while (!(result) && retryTimes-- > 0)
{
try
{
log.debug("lock failed, retrying...{}", retryTimes);
Thread.sleep(RedisLuaUtils.SLEEP_MILLIS);
}
catch (InterruptedException e)
{
return false;
}
result = lockKey(redisKey, value, template);
}
return result;
}
/**
* 加锁
* @param key
* @param value
* @param template
* @return
*/
private static Boolean lockKey(final String key, final String value, RedisTemplate<Object, Object> template)
{
try
{
/*RedisCallback<Boolean> callback = (connection) -> connection.set(
key.getBytes(StandardCharsets.UTF_8),
value.getBytes(StandardCharsets.UTF_8),
Expiration.milliseconds(RedisLuaUtils.TIMEOUT_MILLIS),
RedisStringCommands.SetOption.SET_IF_ABSENT
);
//System.out.println("callback:"+callback);
boolean result = template.execute(callback);
//System.out.println("result:"+result);
return result;//*/
Boolean nativeLock = template.opsForValue().setIfAbsent(key,value, Duration.ofSeconds(RedisLuaUtils.TIMEOUT_MILLIS));
System.out.println("加锁成功:"+nativeLock);
return nativeLock;//*/
}
catch (Exception e)
{
log.info("lock key fail because of ", e);
//throw new Exception("redis 连接失败!");
return false;
}
}
/**
* 释放分布式锁资源(解锁)
*
* @param redisKey key
* @param value value
* @param template redis
* @return Boolean
*/
public static Integer releaseLock(String redisKey, String value, RedisTemplate<Object, Object> template)
{
try
{
/*RedisCallback<Boolean> callback = (connection) -> connection.eval(
UNLOCK_LUA.getBytes(),
ReturnType.BOOLEAN,
1,
redisKey.getBytes(StandardCharsets.UTF_8),
value.getBytes(StandardCharsets.UTF_8)
);
return template.execute(callback);
//*/
// Integer result = template.execute(new DefaultRedisScript<>(UNLOCK_LUA,Integer.class), Collections.singletonList(redisKey),value);
List<Object> list = new CopyOnWriteArrayList<>();
list.add(redisKey);
Integer result = template.execute(new DefaultRedisScript<>(UNLOCK_LUA,Integer.class), list, value);
return result;//*/
}
catch (Exception e)
{
log.info("release lock fail because of ", e);
return 0;
}
}
}
上边的工具类中,我封装了过期时间和重试次数。你可以根据你的需求调整这两个参数。增加锁的性能。
测试一下:
@GetMapping("java/testRedis")
public void testRedis()
{
// 使用多线程来模拟多用户并发操作
for (int i = 0; i < 20; i++)
{
final int temp = i;
new Thread(() -> {
System.out.println("开始创建订单:"+temp);
// 生成唯一uuid
String uuid = UUID.randomUUID().toString();
// 上锁
Boolean isLock = redisLuaUtilsStater.isLock("Locks", uuid, redisTemplate);
if (!isLock)
{
// System.out.println("锁已经被占用:"+temp);
}
else
{ // 获取到锁,做对应的处理。
System.out.println("获取到锁!"+temp);
Boolean res = redisUtil.set("tests", "test:"+temp);
String username = "redis";
// 将记录写入数据库,这一步可以换成其他操作
String ip = "0.0.0.0";
Browse browse = new Browse();
browse.setUsername(username.toString());
browse.setArticleTitle("test:"+temp);
browse.setIp(ip);
browse.setIsWeixin((byte) '0');
articleDao.addBrowse(browse);
System.out.println("redis写入状态:"+res+" - "+temp);
}
/*try
{
Thread.sleep((long) 1);
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}//*/
//一定要记得释放锁,否则会出现问题,解锁会校验uuid,校验失败,解锁也就失败
Integer res = redisLuaUtilsStater.releaseLock("Locks", uuid, redisTemplate);
System.out.println("redis-lock解锁状态:"+res+" - "+temp);
}).start();
}
}
具体操作代码中都有注释。参照即可。
数据库写入操作那部分可以整体换成其他操作。都可以。
二:redission实现分布式锁
Redission分布式锁的底层是setnx和lua脚本(保证原子性)
1.是可重入锁。
2.Redisson 锁支持自动续期功能,这可以帮助我们合理控制分布式锁的有效时长,当业务逻辑执行时间超出了锁的过期时间,锁会自动续期,避免了因为业务逻辑执行时间过长而导致锁提前释放。Redission锁提供的看门狗,一个线程成功索取锁后,看门狗会给持有锁的线程续期。
3.Redission锁支持等待锁,一个while循环不断等待,能提升性能。
4.Redission锁的红锁解决分布式锁的主从一致性问题,红锁:在多个redis实例上(n/2 + 1)创建锁,获取锁时要求在多个实例上都能获取锁成功。但这样性能太低了。
测试代码如下:
// =============================================================
// redission 框架测试
@Autowired
private RedissonClient redissonClient;
@Resource
private ArticleDao articleDao;
/**
* 分布式锁案详解
* http://127.0.0.1:8001/java/redissonLockLock?lock=redissionlocklock
*/
@GetMapping("redissonLockLock")
public void redissonLockLock(String lock)
{
RLock rLock = redissonClient.getLock(lock);
// 使用多线程来模拟多用户并发操作
for (int i = 0; i < 20; i++)
{
final int temp = i;
new Thread(() -> {
System.out.println("开始创建订单:"+temp);
// 生成唯一uuid
String uuid = UUID.randomUUID().toString();
// 获取锁,并设置锁的自动释放时间。
try
{
// 上锁
//waitTime:等待获取锁的最大时间量; leaseTime:锁的自动释放时间(每个线程占用锁的最大时间); unit:时间单位。
// tryLock(10, 30, TimeUnit.SECONDS):此行代码尝试在10秒内获得锁,如果成功,锁将在30秒后自动释放。
boolean tryLock = rLock.tryLock(30000, 10, TimeUnit.MILLISECONDS);
if (tryLock)
{
// 开启看门狗模式,锁到期自动续期
rLock.lock(15, TimeUnit.MILLISECONDS);
// 模拟执行业务逻辑
log.error("开始执行业务逻辑......");
// 获取到锁,做对应的处理。
System.out.println("获取到锁!"+temp);
String username = "redis";
// 将记录写入数据库,这一步可以换成其他操作
String ip = "0.0.0.0";
Browse browse = new Browse();
browse.setUsername(username.toString());
browse.setArticleTitle("test:"+temp);
browse.setIp(ip);
browse.setIsWeixin("0");
int res = articleDao.addBrowse(browse);
System.out.println("redis写入状态:"+res+" - "+temp);
log.error("业务逻辑执行完成......");
}
else
{
log.error("锁已存在......");
}
}
catch (InterruptedException e)
{
//throw new RuntimeException(e);
System.out.println("出错了!");
}
finally
{
// 解锁报错:attempt to unlock lock, not locked by current thread by node id: 544b01e7-babb-4223-b855-00baab04f050 thread-id: 666
// 解决:https://segmentfault.com/a/1190000042576825?sort=newest
if (rLock.isLocked() && rLock.isHeldByCurrentThread())
{
rLock.unlock();
log.error("释放锁完成……");
}
else
{
log.error("锁已过期......");
}
}
}).start();
}
}
三:小结
理论上来说,对性能要求比较高的地方,建议使用redis+lua脚本方式(我自己封装的工具类)来实现加锁。
Redission实现分布式锁功能相对复杂,具体流程如下:
**1) ** 获取锁对象
**2) ** 尝试加锁
**3) ** 处理加锁结果
**4) ** 自旋等待与订阅通知
**5) ** 续命机制
所以相对来说,redission实现分布式锁的性能相对我们自己封装的redis+lua脚本分布式锁较低~
以上大概就是redis实现分布式锁的两种方式。
有好的建议,请在下方输入你的评论。