关于分布式锁的Aop(这里没写延时双删)
1.首先关于分布式锁
分布式锁主流的实现机制(都是为了跨jvm的互斥机制来控制共享资源)
基本思路
1. 获取锁
步骤:
- 构建锁的键:根据业务需求生成一个唯一的锁键,例如
lock:sku:123
。- 设置锁:使用Redis的
SETNX
命令(Set if Not Exists)尝试设置锁键的值。如果键不存在,则设置成功,表示获取锁。- 设置过期时间:为了防止锁持有者崩溃导致锁无法释放,设置一个合理的过期时间(TTL),例如30秒。
2. 检查锁状态
步骤:
- 判断是否获取锁:如果
SETNX
返回true
,表示成功获取锁。- 未获取锁时重试:如果
SETNX
返回false
,表示锁已被其他线程持有,可以选择自旋等待或重试。3. 释放锁
步骤:
- 删除锁键:在完成业务逻辑后,删除锁键以释放锁。
- 避免误删:为了防止误删其他线程的锁,可以使用Lua脚本确保只有锁的持有者才能删除锁。
1.1redis实现
try {
// 缓存存储数据:key-value
// 定义key sku:skuId:info
String skuKey = RedisConst.SKUKEY_PREFIX+skuId+RedisConst.SKUKEY_SUFFIX;
// 获取里面的数据? redis 有五种数据类型 那么我们存储商品详情 使用哪种数据类型?
// 获取缓存数据
skuInfo = (SkuInfo) redisTemplate.opsForValue().get(skuKey);
// 如果从缓存中获取的数据是空
if (skuInfo==null){
// 直接获取数据库中的数据,可能会造成缓存击穿。所以在这个位置,应该添加锁。
// 第一种:redis ,第二种:redisson
// 定义锁的key sku:skuId:lock set k1 v1 px 10000 nx
String lockKey = RedisConst.SKUKEY_PREFIX+skuId+RedisConst.SKULOCK_SUFFIX;
// 定义锁的值
String uuid = UUID.randomUUID().toString().replace("-","");
// 上锁
Boolean isExist = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, RedisConst.SKULOCK_EXPIRE_PX2, TimeUnit.SECONDS);
if (isExist){
// 执行成功的话,则上锁。
System.out.println("获取到分布式锁!");
// 真正获取数据库中的数据 {数据库中到底有没有这个数据 = 防止缓存穿透}
skuInfo = getSkuInfoDB(skuId);
// 从数据库中获取的数据就是空
if (skuInfo==null){
// 为了避免缓存穿透 应该给空的对象放入缓存
SkuInfo skuInfo1 = new SkuInfo(); //对象的地址
redisTemplate.opsForValue().set(skuKey,skuInfo1,RedisConst.SKUKEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
return skuInfo1;
}
// 查询数据库的时候,有值
redisTemplate.opsForValue().set(skuKey,skuInfo,RedisConst.SKUKEY_TIMEOUT,TimeUnit.SECONDS);
// 解锁:使用lua 脚本解锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 设置lua脚本返回的数据类型
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
// 设置lua脚本返回类型为Long
redisScript.setResultType(Long.class);
redisScript.setScriptText(script);
// 删除key 所对应的 value
redisTemplate.execute(redisScript, Arrays.asList(lockKey),uuid);
return skuInfo;
}else {
// 其他线程等待
Thread.sleep(1000);
return getSkuInfo(skuId);
}
}else {
return skuInfo;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
// 为了防止缓存宕机:从数据库中获取数据
return getSkuInfoDB(skuId);
}
1.2通过redission
try {
//定义获取sku信息的key
String skuKey = RedisConst.SKUKEY_PREFIX+skuId+RedisConst.SKUKEY_SUFFIX;
//判断里面是否有
skuInfo = (SkuInfo) redisTemplate.opsForValue().get(skuKey);
//判断skuInfo是否有
if(skuInfo==null){
//设置skuLock
String skuLock = RedisConst.SKUKEY_PREFIX+skuId+RedisConst.SKULOCK_SUFFIX;
//先注入redisson
RLock rlLock= redissonClient.getLock(skuLock);
//设置锁
boolean res = rlLock.tryLock(RedisConst.SKULOCK_EXPIRE_PX1, RedisConst.SKULOCK_EXPIRE_PX2, TimeUnit.SECONDS);
//判断是否拥有锁
if(res){
//拥有锁,查询数据
skuInfo = getSkuInfoDB(skuId);
try {
//判断是否为空
if(skuInfo==null){
//new一个
skuInfo = new SkuInfo();
redisTemplate.opsForValue().set(skuKey,skuInfo,RedisConst.SKUKEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
}else{
//不为空,存储到redis中
redisTemplate.opsForValue().set(skuKey,skuInfo,RedisConst.SKUKEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
//释放锁
}
} finally {
//释放锁
rlLock.unlock();
}
}else{
//没有获得锁则自旋
Thread.sleep(1000);
getSkuInfoByRedisson(skuId);
}
}else{
//有就直接返回
return skuInfo;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return getSkuInfoDB(skuId);
2.关于使用AOp来实现
为什么要使用aop的思想:因为我们不可能只缓存一个,有多个类似的,
依旧是基本的思想
可以看看spring官网,上面有
https://docs.spring.io/spring-framework/reference/core/aop/ataspectj/at-aspectj.html
package com.atguigu.gmall.common.cache;
import com.alibaba.fastjson.JSON;
import com.atguigu.gmall.common.constant.RedisConst;
import lombok.SneakyThrows;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
/**
* @author ChenZhou
* @date 2025-04-01 18:35
*
* 高并发
* 定义Key
* 根据key获取尝试从redis获取数据
* 判断是否有缓存 有直接返回
* 没有的话定义锁的key--->尝试加锁
* ------>睡眠重试
* --->成功的话 又查询
* --->失败的话,赋值成null,
* 无论有没有值都需要释放锁
*
*
* 释放锁lua脚本
*/
/*需要能顾被扫描到*/
@Component
//这是一个切面
@Aspect
public class GmallCacheAspect {
@Autowired
private RedissonClient redissonClient;
@Autowired
private RedisTemplate redisTemplate;
//定义一个环绕通知
@Around("@annotation(com.atguigu.gmall.common.cache.GmallCache)")
@SneakyThrows
//需不需要添加什么参数 需要加入连接点
public Object mallCacheAspectMethod(ProceedingJoinPoint point){
//创建一个新对象
Object object = new Object();
//拦截
MethodSignature signature = (MethodSignature) point.getSignature();
//现在我需要获取方法的注解,因为我需要获取里面的参数和方法名
//获取 GmallCache 注解实例
GmallCache annotation = signature.getMethod().getAnnotation(GmallCache.class);
//获取前缀组成skuKey
String prefix = annotation.prefix();
//SkuKey:prefix+skuId(这个是方法里面的参数)
//point.getArgs().toString(); 这个样子的不对吧
String key = prefix+ Arrays.asList(point.getArgs()).toString();
// 从缓存中获取数据
// 类似于skuInfo = (SkuInfo) redisTemplate.opsForValue().get(skuKey);
//我做的是切面,所以定义一个方法
object = this.getRedisDate(key,signature);
//判断是否获取到了缓存
if(object ==null){
//没有缓存,需要加锁
RLock lock = redissonClient.getLock(key + "lock");
//获取锁
boolean res = lock.tryLock(RedisConst.SKULOCK_EXPIRE_PX1, RedisConst.SKULOCK_EXPIRE_PX2, TimeUnit.SECONDS);
if(res){
try {
//获取到了锁
//执行业务逻辑:直接从数据库获取数据
//如何查询数据库---》方法体里面 就是查询数据
object = point.proceed(point.getArgs());
//如果object为空就得防止缓存穿透问题
if(object==null){
//Object obj = new Object();
//反射
Class aClass = signature.getReturnType();
Object obj = aClass.newInstance();
//放入
//不知道类型
// 将缓存的数据变为 Json 的 字符串
this.redisTemplate.opsForValue().set(key, JSON.toJSONString(object),RedisConst.SKUKEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
return obj;
}
//不为空的话就直接存储到redis中,但是还是需要转换成json
this.redisTemplate.opsForValue().set(key, JSON.toJSONString(object),RedisConst.SKUKEY_TIMEOUT,TimeUnit.SECONDS);
return object;
//释放锁
} finally {
lock.unlock();
}
}else{
//没有获取到锁
//睡眠
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}else {
return object;
}
//兜底
return point.proceed(point.getArgs());
}
private Object getRedisDate(String key, MethodSignature signature) {
// 从缓存中获取数据
//类似于skuInfo = (SkuInfo) redisTemplate.opsForValue().get(skuKey);
//返回类型--多种
String string = (String) this.redisTemplate.opsForValue().get(key);
//判断缓存中有没有数据
if(!StringUtils.isEmpty(string)){
//不为空,转换成json
return JSON.parseObject(string, signature.getReturnType());
}
return null;
}
}