ce分布式锁原理:占用公共访问的地方redis或mysql,先到先得,后到要等。
* 1.双写模式:修改数据库数据同时,也修改redis缓存数据,存在问题,网络交互或者cpu处理速度的差异,并发修改会产生脏数据
* 2.失效模式:修改数据同时删除缓存数据,等待下次主动查询更新缓存
* 思考:如何保证缓存数据与数据库同步,保持一致?要加读写锁;缓存数据本就不应该是实时性,一致性超高的,要求实时性,一致性超高的数据,直接去查数据,简化系统。
package com.hmdp.utils;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
/**
* User: ldj
* Date: 2022/9/3
* Time: 16:55
* Description: Redis分布锁工具类,目前没有做续机,所以过期时间调大一点,确保业务执行完成
* 定时任务进行锁续命
*/
@Slf4j
@Component
@RefreshScope
public class RedisUtil {
private static Integer shortTime;
private static Long expiredTime;
private static StringRedisTemplate stringRedisTemplate;
@Value("${redis.lock.expiredTime:60}")
public void setExpiredTime(Long expiredTime) {
RedisUtil.expiredTime = expiredTime;
}
@Value("${redis.lock.shortTime:10}")
public void setExpiredTime(Integer shortTime) {
RedisUtil.shortTime = shortTime;
}
@Autowired
public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
RedisUtil.stringRedisTemplate = stringRedisTemplate;
}
//避免因线程1执行业务很长,lock1过期了,线程2进来创建lock2,当线程2执行到一半时,线程1执行完业务释放锁是lock2
private static String uuid;
//获取锁+设置过期时间 原子性
public static Boolean getLock() {
uuid = UUID.randomUUID().toString().replaceAll("-", "");
Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, expiredTime, TimeUnit.SECONDS);
if (isLock != null && isLock) {
log.info("[Redis] 添加锁成功 releaseUuid:[{}]", uuid);
return true;
} else {
log.warn("[Redis] 添加锁失败!");
return false;
}
}
//释放锁+获取对比值 原子性
public static void releaseLock() {
String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<Long>(luaScript, Long.class), Collections.singletonList("lock"), uuid);
if (flag != null && flag == 1) {
log.info("[Redis] 释放锁成功 releaseUuid:[{}]", uuid);
} else {
log.warn("[Redis] 释放锁失败!");
}
}
//设置等待重试时间
public static void waite(Long waitTime) {
try {
TimeUnit.MILLISECONDS.sleep(waitTime);
} catch (InterruptedException e) {
log.error("等待异常", e);
}
}
//删除key
public static Boolean removeKey(String key) {
return stringRedisTemplate.delete(key);
}
public static void put(String redisKey, Object object, Integer expireTime) {
if (expireTime != null) {
stringRedisTemplate.opsForValue().set(redisKey, JacksonUtil.writeValueAsString(object), expireTime, TimeUnit.MINUTES);
}
stringRedisTemplate.opsForValue().set(redisKey, JacksonUtil.writeValueAsString(object));
}
public static void putWithLogicalExpire(String redisKey, Object object, Integer time) {
RedisData redisData = new RedisData();
redisData.setData(object);
redisData.setExpireTime(LocalDateTime.now().plusMinutes(time));
stringRedisTemplate.opsForValue().set(redisKey, JacksonUtil.writeValueAsString(redisData));
}
public static <T> T getValue(String key, Class<T> classType) {
String context = stringRedisTemplate.opsForValue().get(key);
if (StringUtils.isBlank(context)) {
return null;
}
return JacksonUtil.readValue(context, classType);
}
public static <T> T getValue(String key, TypeReference<T> valueTypeRef) {
String context = stringRedisTemplate.opsForValue().get(key);
if (StringUtils.isBlank(context)) {
return null;
}
return JacksonUtil.readValue(context, valueTypeRef);
}
public static <K, V, R> R queryAndSet(String key, long expiredTime, Map<K, V> map, TypeReference<R> valueTypeRef, Function<Map<K, V>, R> callback) {
BoundValueOperations<String, String> operations = stringRedisTemplate.boundValueOps(key);
String context = operations.get();
if (StringUtils.isNotBlank(context)) {
return JacksonUtil.readValue(context, valueTypeRef);
}
// ""或" "
if (context != null) {
return null;
}
//如果为null
Boolean lock = RedisUtil.getLock();
if (lock) {
try {
R r = callback.apply(map);
if (r == null) {
operations.set("", shortTime, TimeUnit.SECONDS);
return null;
}
operations.set(JacksonUtil.writeValueAsString(r), expiredTime, TimeUnit.MINUTES);
return r;
} catch (Exception e) {
log.error(e.getMessage());
} finally {
RedisUtil.releaseLock();
}
}
//抢不到锁,重试
return queryAndSet(key, expiredTime, map, valueTypeRef, callback);
}
}
改进 能获取多个分布式锁:
package com.tulin.lock.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* User: ldj
* Date: 2022/10/19
* Time: 14:54
* Description: No Description
*/
@Slf4j
@Component
public class RedisDistributedLockUtil {
private static final String LOCK_PREFIX = "look:";
private static final Integer LEFT_SHIFT_BITS = 32;
private static final Long BEGIN_TIMESTAMP = 1672531200L;
private static Long expiredTime;
private static StringRedisTemplate stringRedisTemplate;
@Value("${redis.lock.expiredTime:60}")
public void setExpiredTime(Long expiredTime) {
RedisDistributedLockUtil.expiredTime = expiredTime;
}
@Autowired
public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
RedisDistributedLockUtil.stringRedisTemplate = stringRedisTemplate;
}
//避免因线程1执行业务很长,lock1过期了,线程2进来创建lock2,当线程2执行到一半时,线程1执行完业务释放锁是lock2
private static String uuid;
//获取锁+设置过期时间 原子性
public static Boolean getLock(String lockName) {
uuid = UUID.randomUUID().toString().replaceAll("-", "");
Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + lockName, uuid, expiredTime, TimeUnit.SECONDS);
if (isLock != null && isLock) {
log.info("[Redis] 添加锁成功 lockName:[{}],releaseUuid:[{}]", lockName, uuid);
return true;
} else {
log.warn("[Redis] 添加锁失败! lockName:[{}]", lockName);
return false;
}
}
//释放锁+获取对比值 原子性
public static void releaseLock(String lockName) {
String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Long flag = stringRedisTemplate.execute(new DefaultRedisScript<Long>(luaScript, Long.class), Collections.singletonList(LOCK_PREFIX + lockName), uuid);
if (flag != null && flag == 1) {
log.info("[Redis] 释放锁成功 lockName:[{}],releaseUuid:[{}]", lockName, uuid);
} else {
log.warn("[Redis] 释放锁失败! lockName:[{}]", lockName);
}
}
//设置等待重试时间
public static void waite(Long waitTime) {
try {
TimeUnit.MILLISECONDS.sleep(waitTime);
} catch (InterruptedException e) {
log.error("等待异常", e);
}
}
public static long getGlobalId(String prefix) {
//生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSeconds = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSeconds - BEGIN_TIMESTAMP;
//生成32位id
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
Long increment = stringRedisTemplate.opsForValue().increment(prefix + ":" + date);
//拼接时间戳 或运算同时为0才是0,有1肯定是1
if (increment == null) {
return 0L;
}
return timestamp << LEFT_SHIFT_BITS | increment;
}
}
测试类
@Test
public void test4() {
Boolean lock = RedisUtil.getLock();
System.out.println(lock);
}
@Test
public void test5() {
RedisUtil.releaseLock();
}
@Test
public void test6() {
Boolean lock = RedisUtil.getLock();
System.out.println(lock);
RedisUtil.releaseLock();
}
/**
* 2.从数据库获取数据并封装,加Redis锁、
* 加锁和设置过期时间是原子操作,避免因业务代码出现异常导致死锁
* 获取值对比和删除锁是原子操作,避免因与redis网络交互完数据,key恰好过期,删掉别的线程的锁 使用lua脚本操作解锁
*/
public Map<String, List<UserInfoVO>> getDataWithRedisLock() {
//1.加锁成功(加过期时间)->执行业务->释放锁
Boolean lock = RedisUtil.getLock();
if (lock != null && lock) {
Map<String, List<UserInfoVO>> dataFromDb;
try {
dataFromDb = this.getDataFromDb();
} finally {
RedisUtil.deleteLock();
}
return dataFromDb;
} else {
//2.加锁失败->休眠3秒后重试加锁
RedisUtil.waite(3);
return this.getDataWithRedisLock();
}
}
@GetMapping("/{id}")
public Result queryAndSetTest(@PathVariable("id") Long id) {
String redisKey = RedisConstant.REDIS_SHOP_DOUBLE_PREFIX + id;
Map<String, Object> map = new HashMap<>();
map.put("id", id);
Shop shop = RedisUtil.queryAndSet(redisKey, 30, map, new TypeReference<Shop>() {
}, invoke -> shopService.lambdaQuery().eq(Shop::getId, invoke.get("id")).one());
if (shop == null) {
return Result.fail("404:店铺不存在!");
}
return Result.ok(shop);
}