在分布式系统中,对一块数据的使用,特别是修改操作。微服务环境下,微服务本身是控制不了服务之间的事务的,有时候是需要控制数据的一致性,就需要使用分布式锁。基于redis的分布式锁是一种很好的解决方案,redis是每个服务共享的资源。当某个服务去申请访问资源时,都去获取redis的允许,这其实就达到了一种锁的机制。
Redis的分布式锁主要用于解决在分布式系统中保证互斥性的问题,以防止资源竞争导致的数据不一致或系统错误。以下是一些使用Redis分布式锁的具体场景:
- 对共享资源的互斥访问:例如,对公共数据库的事务操作或者对公共缓存的写操作。在处理这类问题时,分布式锁允许一个时间只允许一个节点访问某个资源,防止多个节点同时进行操作引发数据不一致问题。
- 分布式爬虫:例如,对同一个网站的抓取操作。由于网站数据是共享的,如果多个节点同时进行抓取,可能会导致数据重复或者数据缺失。通过使用分布式锁,可以确保每次只有一个节点进行抓取操作,避免数据的不一致性。
- 分布式任务调度:例如,对同一个任务的并发执行。这种情况下,任务需要在多个节点上并发执行以提高效率,但是同时也要保证任务执行的顺序和一致性。通过使用分布式锁,可以控制任务的执行顺序,避免并发执行导致的问题。
- 分布式缓存更新:例如,对缓存数据的更新操作。当需要对缓存进行更新时,为了保证数据的一致性,需要先获取锁,然后进行更新操作,最后释放锁。这样可以避免多个节点同时更新缓存导致的数据不一致问题。
- 分布式限流:例如,对某一段时间内的访问流量进行限制。通过使用分布式锁,可以限制在一定时间内只允许一定数量的节点访问某个资源,避免因为过多的请求导致系统崩溃或者数据错误。
此外,还有其他的业务场景也可能会使用到Redis的分布式锁,例如在微服务架构中,为了保证服务之间的互斥性和一致性,也可以使用Redis的分布式锁。总的来说,只要是在分布式系统中需要保证互斥性的场景,都可以考虑使用Redis的分布式锁。
springBoot的引入依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
工具类:
import java.util.Arrays;
import java.util.List;
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.script.RedisScript;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.stereotype.Component;
import cn.hutool.core.lang.UUID;
/**
* @author:茅河野人
* @Description:redis分布式锁工具类
* @date:2023年2月6日
*/
@Component
public class RedisLockUtils implements AutoCloseable {
@Autowired
private RedisTemplate redisTemplate;
private String key;
private String value;
private int expireTime;
public RedisLockUtils(RedisTemplate redisTemplate, String key, int expireTime) {
this.redisTemplate = redisTemplate;
this.key = key;
this.expireTime = expireTime;
this.value = UUID.randomUUID().toString();
}
/**
* 获取分布式锁
*/
public boolean getLock() {
RedisCallback<Boolean> redisCallback = connection -> {
// 设置NX
RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
// 设置过期时间
Expiration expiration = Expiration.seconds(expireTime);
// 序列化key
byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
// 序列化value
byte[] redisValue = redisTemplate.getValueSerializer().serialize(value);
// 执行setnx操作
Boolean result = connection.set(redisKey, redisValue, expiration, setOption);
return result;
};
// 获取分布式锁
Boolean lock = (Boolean) redisTemplate.execute(redisCallback);
return lock;
}
/**
* 解锁
*/
public boolean unLock() {
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" + " return redis.call(\"del\",KEYS[1])\n"
+ "else\n" + " return 0\n" + "end";
RedisScript<Boolean> redisScript = RedisScript.of(script, Boolean.class);
List<String> keys = Arrays.asList(key);
Boolean result = (Boolean) redisTemplate.execute(redisScript, keys, value);
return result;
}
/**
* 自动关闭
*/
@Override
public void close() {
unLock();
}
}
调用:
@RequestMapping("redisLock")
public String redisLock(){
try (RedisLockUtils redisLock = new RedisLockUtils(redisTemplate,"redisKey",30)){
if (redisLock.getLock()) {
//延时模拟业务代码执行
Thread.sleep(15000);
}
}catch (Exception e) {
e.printStackTrace();
}
return "方法执行完成";
}
或者使用以下这个工具类
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import com.google.gson.Gson;
import cn.ctg.common.enums.EnumType;
import cn.ctg.common.util.constants.Constant;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
/**
* Redis工具类
*
*/
@Component
public class RedisUtils {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private RedisTemplate redisTemplate;
@Resource(name = "redisTemplate")
private ValueOperations<String, String> valueOperations;
@Autowired
private RedisExtendService redisExtendService;
/** 加分布式锁的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脚本 */
private static final String INCR_LUA =
"local current = redis.call('incr',KEYS[1]);" +
" local t = redis.call('ttl',KEYS[1]); " +
"if t == -1 then " +
"redis.call('expire',KEYS[1],ARGV[1]) " +
"end; " +
"return current";
/** 解锁的LUA脚本 */
private static final String UNLOCK_LUA =
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
private static final Long SUCCESS = 1L;
/** 互斥锁过期时间(分钟) */
private static final long MUTEX_WAIT_MILLISECONDS = 50;
/** 编号规则生成线程等待次数 ((10 * 1000) / 50) + 1 */
public static final long RULE_CODE_THREAD_WAIT_COUNT = 200;
/** 互斥锁等待时间(毫秒) */
private static final long MUTEX_EXPIRE_MINUTES = 3;
/**
* 不设置过期时长
*/
public final static long NOT_EXPIRE = -1;
/**
* 默认过期时长,单位:秒
*/
public final static long DEFAULT_EXPIRE = 7200; // 2小时
/**
* 会员卡缓存失效时间 2小时
*/
public final static long CARD_DEFAULT_EXPIRE = 7200;
/**
* 默认过期时长,1天
*/
public final static long DEFAULT_A_DAY = 86400;
/**
* 默认过期时长,1分钟
*/
public final static long DEFAULT_A_MIN = 60 ;
/**
* 默认过期时长,2分钟
*/
public final static long DEFAULT_TWO_MIN = 120 ;
/**
* 保存数据
*
* @param key
* @param value
* @param expire 过期时间,单位s
*/
public void set(String key, Object value, long expire) {
String valueJson = toJson(value);
valueOperations.set(key, valueJson);
if (expire != NOT_EXPIRE) {
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
redisExtendService.redisDataChange(key, valueJson, EnumType.CRUD_TYPE.CREATE.getValue());
}
/**
* 判断key是否存在
*
* @param key
*/
public Boolean hasKey(String key) {
if (StringUtils.isNotBlank(key)) {
return valueOperations.getOperations().hasKey(key);
}
return Boolean.FALSE;
}
/**
* @param key
* @param value
*/
public void set(String key, Object value) {
set(key, value, NOT_EXPIRE);
}
public <T> T get(String key, Class<T> clazz, long expire) {
String value = Convert.toStr(valueOperations.get(key));
if (expire != NOT_EXPIRE) {
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
return value == null ? null : fromJson(value, clazz);
}
/**
* 批量从Redis中获取数据
*
* @param valueMap 需要存储的数据集合
* @param expire 过期时间,秒
* @return java.util.List<T> 返回值
*/
public void batchSet(Map<String, String> valueMap, long expire) {
valueOperations.multiSet(valueMap);
if (expire != NOT_EXPIRE) {
for (String key : valueMap.keySet()) {
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
}
}
/**
* 批量删除
*
* @param keys 需要删除的KEY集合
* @return void
*/
public void batchDelete(Collection<String> keys) {
redisTemplate.delete(keys);
}
/**
* 批量从Redis中获取数据
*
* @param keyList 需要获取的Key集合
* @param clazz 需要转换的类型
* @return java.util.List<T> 返回值
*/
public <T> Map<String, T> batchGet(List<String> keyList, Class<T> clazz) {
List<String> objectList = valueOperations.multiGet(keyList);
Map<String, T> map = new LinkedHashMap<>(objectList.size());
for (int i = 0; i < keyList.size(); i++) {
String value = Convert.toStr(objectList.get(i));
if (!String.class.equals(clazz)) {
map.put(keyList.get(i), fromJson(value, clazz));
} else {
map.put(keyList.get(i), (T)value);
}
}
return map;
}
public <T> T get(String key, Class<T> clazz) {
return get(key, clazz, NOT_EXPIRE);
}
/**
* 使用 父编码+当前编码获取+集团+语言 获取名称
*
* @param code 父编码
* @param dictCode 当前编码
* @param language 语言 UserUtils.getLanguage()
* @param groupId 集团ID
*/
public String get(String code, String dictCode, String language, String groupId) {
if (StringUtils.isBlank(dictCode)) {
return "";
}
String key = RedisKeys.getSysDictKey(code, dictCode, language, groupId);
return get(key, NOT_EXPIRE);
}
public String get(String key, long expire) {
String value = Convert.toStr(valueOperations.get(key));
if (expire != NOT_EXPIRE) {
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
return value;
}
public String get(String key) {
return get(key, NOT_EXPIRE);
}
public void delete(String key) {
redisTemplate.delete(key);
redisExtendService.redisDataChange(key, "", EnumType.CRUD_TYPE.DELETE.getValue());
}
/**
* Object转成JSON数据
*/
private String toJson(Object object) {
if (object instanceof Integer || object instanceof Long || object instanceof Float || object instanceof Double
|| object instanceof Boolean || object instanceof String) {
return String.valueOf(object);
}
return new Gson().toJson(object);
}
/**
* JSON数据,转成Object
*/
private <T> T fromJson(String json, Class<T> clazz) {
return new Gson().fromJson(json, clazz);
}
/**
* 获取分布式锁,默认过期时间3分钟
*
* @param key 锁的KEY
* @return java.lang.Boolean true为获取到锁,可以下一步业务, false为没有获取到锁
*/
public Boolean setMutexLock(String key) {
return setMutexLockAndExpire(key, getMutexLockExpireMinutes(), TimeUnit.MINUTES);
}
/**
* 获取分布式锁,带Redis事务
*
* @param key 锁的KEY
* @param timeout 锁时效时间,默认单位:秒
* @param unit 锁失效时间单位,为null则默认秒
* @return java.lang.Boolean true为获取到锁,可以下一步业务, false为没有获取到锁
*/
public Boolean setMutexLockAndExpire(String key, long timeout, TimeUnit unit) {
return setMutexLockAndExpire(key, Constant.RESULT_1, timeout, unit);
}
/**
* 获取分布式锁,带Redis事务
* 适用于同一业务,不同的请求用不同的锁,把value当成
* @param key 锁的KEY
* @param value 锁的值,一定要跟解锁的值一样,否则会导致无法解锁
* @param timeout 锁时效时间,默认单位:秒
* @param unit 锁失效时间单位,为null则默认秒
* @return java.lang.Boolean true为获取到锁,可以下一步业务, false为没有获取到锁
*/
public Boolean setMutexLockAndExpire(String key, String value, long timeout, TimeUnit unit) {
value = StrUtil.appendIfMissing(StrUtil.prependIfMissing(value,"\""),"\"");
Long result = executeLua(key, value, LOCK_LUA, timeout, unit, Long.class);
return SUCCESS.equals(result);
}
/**
* 解锁
*
* @param key 锁的Key
* @return boolean
*/
public boolean unlock(String key) {
return unlock(key, Constant.RESULT_1);
}
/**
* 解锁
*
* @param key 锁的Key
* @param value 锁的value,一定要跟加锁的value一致,否则会认为不是同一个锁,不会释放
* @return boolean
*/
public boolean unlock(String key, String value) {
value = StrUtil.appendIfMissing(StrUtil.prependIfMissing(value,"\""),"\"");
Long result = executeLua(key, value, UNLOCK_LUA,null, null, Long.class);
return SUCCESS.equals(result);
}
/**
* 获取等待锁,如果没有获取到锁就一直等待获取,直到超过waitTime的时间
*
* @param key 锁的key
* @param timeout 锁的超时时间
* @param unit 锁的超时时间单位
* @param waitTime 获取锁时的等待时间,一直等不超时则填-1,单位:毫秒
* @return java.lang.Boolean true为获取到锁,可以下一步业务, false为没有获取到锁
*/
public Boolean setMutexWaitLock(String key, long timeout, TimeUnit unit, long waitTime) {
long start = System.currentTimeMillis();
while (true) {
boolean result = setMutexLockAndExpire(key, timeout, unit);
if (result) {
return true;
} else {
long current = System.currentTimeMillis();
// 超过等待时间还没获取到锁则返回false
if (waitTime > 0 && (current - start > waitTime)) {
logger.warn("redis分布式锁获取失败,key[{}],等待时间[{}]", key, waitTime);
return false;
}
// 等待100毫秒后重试
ThreadUtil.sleep(100);
}
}
}
public long getMutexLockExpireMinutes() {
return MUTEX_EXPIRE_MINUTES;
}
/**
* 获取自增序列号
*
* @param key 序列号的KEY
* @param seq 自增值,默认自增1
* @return java.lang.Long 自增后的值
*/
public Long incr(String key, Long seq, long timeout, TimeUnit unit) {
return executeLua(key, null, INCR_LUA, timeout, unit, Long.class);
}
/**
* 执行LUA脚本
*
* @param key redisKey
* @param value 值
* @param lua lua脚本
* @param timeout 超时时间
* @param unit 超时单位
* @param clazz 返回值类型
* @return T 返回值
*/
public <T> T executeLua(String key, Object value, String lua, Long timeout, TimeUnit unit, Class<T> clazz){
// 有时间单位则转成秒,否则默认秒
if (unit != null) {
timeout = unit.toSeconds(timeout);
}
List<String> args = new ArrayList<>(2);
if(value != null){
args.add(Convert.toStr(value));
}
if(timeout != null){
args.add(Convert.toStr(timeout));
}
//spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本异常,此处拿到原redis的connection执行脚本
T result = (T)redisTemplate.execute(new RedisCallback<T>() {
@Override
public T doInRedis(RedisConnection connection) throws DataAccessException {
Object nativeConnection = connection.getNativeConnection();
// 集群模式和单点模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
// 集群
if (nativeConnection instanceof JedisCluster) {
return (T) ((JedisCluster) nativeConnection).eval(lua, Collections.singletonList(key), args);
}
// 单点
else if (nativeConnection instanceof RedisProperties.Jedis) {
return (T) ((Jedis) nativeConnection).eval(lua, Collections.singletonList(key), args);
}
return null;
}
});
return result;
}
public void expire(String key, long timeout, TimeUnit unit) {
try {
redisTemplate.expire(key, timeout, unit);
} catch (Exception e) {
logger.error("设置缓存过期时间失败,key={},timeout={},unit={}", key, timeout, unit, e);
}
}
/**
* 获取互斥线程等待时间
*
* @return
*/
public long getMutexThreadWaitMilliseconds() {
return MUTEX_WAIT_MILLISECONDS;
}
public Set<String> getKeys(String key) {
return redisTemplate.keys(key);
}
/**
* 获取随机秒数 如:getRandomTime(30, 7)返回30天到第37天的随机秒数,即时效时间最小为30天,最大为37天
*
* @param afterDays N天之后
* @param rangeDay 日期范围
* @return java.lang.Long 秒数
*/
public static Long getRandomTime(int afterDays, int rangeDay) {
Calendar calendar = Calendar.getInstance();
long curTime = calendar.getTimeInMillis();
calendar.add(Calendar.DAY_OF_MONTH, afterDays);
long minTime = calendar.getTimeInMillis();
calendar.add(Calendar.DAY_OF_MONTH, rangeDay);
long maxTime = calendar.getTimeInMillis();
long randomTime = RandomUtil.randomLong(minTime, maxTime);
return (randomTime - curTime) / 1000;
}
/**
* 获取30天内的随机秒数
*
* @return long 返回1天后30天内的随机秒数
*/
public static long getRandomTime() {
return getRandomTime(1, 30);
}
public void setnx(String key,String value){
}
}