问题背景
Redisson做分布式锁是目前比较流行的方式,但是在使用的过程中遇到一些坑:
- Redisson的分布式锁只能通过创建锁的线程进行解锁,正所谓解铃还须系铃人,不是同一个线程解锁会报异常
- 因为Redisson是为锁而生,所以一开始设计的时候,为了防止死锁,默认锁的过期时间为30S
- 当时我居然傻到用单元测试来测试Redisson的分布式锁,我太傻了,单元测试之后马上就会结束项目运行,那么就没有线程持有锁了,更别说还需要同线程解锁了
但我的项目中做了分布式任务调度,定时去扫描任务完成没有,完成了再进行解锁,但定时的扫描线程不是之前的加锁线程了,所以使用redisson做分布式锁不太合适,本篇介绍redis的分布式锁的模板使用
注意事项:
- 代码是从我的这篇文章进行添加修改的
- 可以自己创建工程,也可以下载源码进行参考
- 默认已安装redis,可以使用安装包安装看这篇文章,使用docker安装看这篇文章
项目搭建
1 在RedisUtils工具类中添加setNX方法,value值可以设置为globalId,这样具有唯一性,每次解锁还有增加一层value的判断保证更安全
package com.yg.redisson.utils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* @Author suolong
* @Date 2022/4/26 13:33
* @Version 2.0
*/
@Service("redisUtils")
@Slf4j
public class RedisUtils {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
try {
return key == null null : redisTemplate.opsForValue().get(key);
}catch (Exception ex){
log.error("获取缓存异常", ex);
return null;
}
}
public boolean del(String key){
try {
return Boolean.TRUE.equals(redisTemplate.delete(key));
}catch (Exception e){
log.error("删除缓存键异常",e);
return false;
}
}
public Long increment(String key, Long delta){
try {
return redisTemplate.opsForValue().increment(key, delta);
}catch (Exception ex){
log.error("操作缓存异常",ex);
return null;
}
}
public void saveByPipeLine(Map<String, Map<String, String>> dataMap){
try {
final RedisSerializer keySerializer = redisTemplate.getKeySerializer();
final RedisSerializer valueSerializer = redisTemplate.getValueSerializer();
redisTemplate.executePipelined((RedisCallback<Object>) redisConnection -> {
dataMap.forEach((key,value) -> redisConnection.set(Objects.requireNonNull(keySerializer.serialize(key)), Objects.requireNonNull(valueSerializer.serialize(value)), Expiration.seconds(6 * 3600), RedisStringCommands.SetOption.UPSERT));
return null;
});
}catch (Exception ex){
log.error("pipeline存储异常",ex);
}
}
public void saveByPipeLineMap(Map<String, String> dataMap, String key){
try {
final RedisSerializer keySerializer = redisTemplate.getKeySerializer();
final RedisSerializer valueSerializer = redisTemplate.getValueSerializer();
redisTemplate.executePipelined((RedisCallback<Object>) redisConnection -> {
redisConnection.set(Objects.requireNonNull(keySerializer.serialize(key)), Objects.requireNonNull(valueSerializer.serialize(dataMap)), Expiration.seconds(6 * 3600), RedisStringCommands.SetOption.UPSERT);
return null;
});
}catch (Exception ex){
log.error("pipeline存储异常",ex);
}
}
public List<Object> getByPipeLine(List<String> keys){
try {
return redisTemplate.executePipelined((RedisCallback<Object>) redisConnection -> {
for (String key : keys) {
redisConnection.get(key.getBytes(StandardCharsets.UTF_8));
}
return null;
});
}catch (Exception ex){
log.error("pipeline读取异常",ex);
}
return Collections.emptyList();
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
log.error("设置缓存异常:{}",e.getLocalizedMessage());
return false;
}
}
public Long del(Set<String> key) {
try {
if (CollectionUtils.isNotEmpty(key)) {
return redisTemplate.delete(key);
}
return null;
}catch (Exception ex){
log.error("删除缓存键异常:{}", ex.getLocalizedMessage());
return null;
}
}
//设置锁名,value值,过期时间
public boolean setNX(String lockKey, String value, long expiryTime) {
if (redisTemplate == null) {
log.info("redisTemplate is null");
return false;
}
try {
Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockKey, value, expiryTime, TimeUnit.SECONDS);
assert flag != null;
if (!flag) {
log.info("加锁失败");
return false;
}
log.info("Thread [{}] redisTemplate lock [{}] success", Thread.currentThread().getName(), lockKey);
// 加锁成功
return true;
} catch (Exception e) {
log.info("加锁失败", e);
log.error("redisTemplate lock [{}] Exception:", lockKey, e);
return false;
}
}
}
2 setNX测试,先设置setNX,然后进行del,此时两个方法并不是同一个线程
@Test
void redisDel() {
boolean res = redisUtils.del("yuan");
log.info("res: {}", res);
}
@Test
void redisSetNX() {
boolean res = redisUtils.setNX("yuan", "123",12*60*60L);
log.info("res: {}", res);
}
总结
- 需要不同线程删除锁,使用这种方式比较合适
作为程序员第 125 篇文章,每次写一句歌词记录一下,看看人生有几首歌的时间,wahahaha ...