使用redis锁使用分布式锁
可能存在的问题:
问题:setnx刚好获取到锁,业务逻辑出现异常,导致锁无法释放
解决:设置过期时间,自动释放锁。 在set时指定过期时间(保证原子性)
问题:可能会释放其他服务器的锁。比如,某一个业务正常执行需要3秒,但由于某种原因执行了7s,3s后锁自动释放,其他进程进入,7s后该进程del释放的是其他进程的锁
解决:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁
问题:删除操作缺乏原子性 进程1经过了uuid的判断,但此时刚好过了3秒,锁自动释放,此时进程2获得了锁,进程1的del释放的是进程2的锁
解决:使用lua脚本保证删除的原子性 lua在集群的情况下就不能保证删除的原子性
问题:redis集群状态下,进程1从master获取到锁,在master将锁同步到slave之前,master宕掉了,slaver节点晋升为master节点,这时进程2可以从新的master节点获取到锁。
解决:使用redlock 通过redisson实现redlock
public void testLock() {
// 设置uuId
String uuid = UUID.randomUUID().toString();
// 缓存的lock 对应的值 ,应该是index2 的uuid
Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);
// 判断flag index=1
if (flag){
// 说明上锁成功! 执行业务逻辑
String value = redisTemplate.opsForValue().get("num");
// 判断
if(StringUtils.isEmpty(value)){
return;
}
// 进行数据转换
int num = Integer.parseInt(value);
// 放入缓存
redisTemplate.opsForValue().set("num",String.valueOf(++num));
// 定义一个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脚本放入DefaultRedisScript 对象中
redisScript.setScriptText(script);
// 设置DefaultRedisScript 这个对象的泛型
redisScript.setResultType(Long.class);
// 执行删除
redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);
}else {
// 没有获取到锁!
try {
Thread.sleep(1000);
// 睡醒了之后,重试
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
使用redission实现分布式锁
1、导入依赖
<!-- redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.2</version>
</dependency>
2、配置redission
//配置redisson
package com.atguigu.gmall.common.config;
@Data
@Configuration
@ConfigurationProperties("spring.redis")
public class RedissonConfig {
private String host;
private String password;
private String port;
private int timeout = 3000;
private static String ADDRESS_PREFIX = "redis://";
/**
* 自动装配
*/
@Bean
RedissonClient redissonSingle() {
Config config = new Config();
if(StringUtils.isEmpty(host)){
throw new RuntimeException("host is empty");
}
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress(ADDRESS_PREFIX + this.host + ":" + port)
.setTimeout(this.timeout);
if(!StringUtils.isEmpty(this.password)) {
serverConfig.setPassword(this.password);
}
return Redisson.create(config);
}
}
3、redisson分布式锁实现
@Autowired
private RedissonClient redissonClient;
@Override
public void testLock() {
// 创建锁:
String skuId="25";
String locKey ="lock:"+skuId;
// 锁的是每个商品
RLock lock = redissonClient.getLock(locKey);
// 开始加锁
lock.lock();
// 业务逻辑代码
// 获取数据
String value = redisTemplate.opsForValue().get("num");
if (StringUtils.isBlank(value)){
return;
}
// 将value 变为int
int num = Integer.parseInt(value);
// 将num +1 放入缓存
redisTemplate.opsForValue().set("num",String.valueOf(++num));
// 解锁:
lock.unlock();
}
分布式锁 + AOP实现缓存
1、定义一个注解
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface GmallCache{
/**
* 缓存key的前缀
* @return
*/
String prefix() default "cache";
}
2、 定义一个切面类加上注解
import com.alibaba.fastjson.JSON;
import com.atguigu.gmall.common.constant.RedisConst;
import lombok.SneakyThrows;
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 org.springframework.util.StringUtils;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
/**
* @author mqx
* 处理环绕通知
* @date 2020-11-11 09:30:29
*/
@Component
@Aspect
public class GmallCacheAspect {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RedissonClient redissonClient;
// 切GmallCache注解
@SneakyThrows
@Around("@annotation(com.atguigu.gmall.common.cache.GmallCache)")
public Object cacheAroundAdvice(ProceedingJoinPoint joinPoint){
// 声明一个对象
Object object = new Object();
// 在环绕通知中处理业务逻辑 {实现分布式锁}
// 获取到注解,注解使用在方法上!
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
GmallCache gmallCache = signature.getMethod().getAnnotation(GmallCache.class);
// 获取到注解上的前缀
String prefix = gmallCache.prefix(); // sku
// 方法传入的参数
Object[] args = joinPoint.getArgs();
// 组成缓存的key 需要前缀+方法传入的参数
String key = prefix+ Arrays.asList(args).toString();
// 防止redis ,redisson 出现问题!
try {
// 从缓存中获取数据
// 类似于skuInfo = (SkuInfo) redisTemplate.opsForValue().get(skuKey);
object = cacheHit(key,signature);
// 判断缓存中的数据是否为空!
if (object==null){
// 从数据库中获取数据,并放入缓存,防止缓存击穿必须上锁
// perfix = sku index1 skuId = 32 , index2 skuId = 33
// public SkuInfo getSkuInfo(Long skuId)
// key+":lock"
String lockKey = prefix + ":lock";
// 准备上锁
RLock lock = redissonClient.getLock(lockKey);
boolean result = lock.tryLock(RedisConst.SKULOCK_EXPIRE_PX1, RedisConst.SKULOCK_EXPIRE_PX2, TimeUnit.SECONDS);
// 上锁成功
if (result){
try {
// 表示执行方法体 getSkuInfoDB(skuId);
object = joinPoint.proceed(joinPoint.getArgs());
// 判断object 是否为空
if (object==null){
// 防止缓存穿透
Object object1 = new Object();
redisTemplate.opsForValue().set(key, JSON.toJSONString(object1),RedisConst.SKUKEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
// 返回数据
return object1;
}
// 放入缓存
redisTemplate.opsForValue().set(key, JSON.toJSONString(object),RedisConst.SKUKEY_TIMEOUT,TimeUnit.SECONDS);
// 返回数据
return object;
} finally {
lock.unlock();
}
}else{
// 上锁失败,睡眠自旋
Thread.sleep(1000);
return cacheAroundAdvice(joinPoint);
// 理想状态
// return cacheHit(key, signature);
}
}else {
return object;
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
// 如果出现问题数据库兜底
return joinPoint.proceed(joinPoint.getArgs());
}
/**
* 表示从缓存中获取数据
* @param key 缓存的key
* @param signature 获取方法的返回值类型
* @return
*/
private Object cacheHit(String key, MethodSignature signature) {
// 通过key 来获取缓存的数据
String strJson = (String) redisTemplate.opsForValue().get(key);
// 表示从缓存中获取到了数据
if (!StringUtils.isEmpty(strJson)){
// 字符串存储的数据是什么? 就是方法的返回值类型
Class returnType = signature.getReturnType();
// 将字符串变为当前的返回值类型
return JSON.parseObject(strJson,returnType);
}
return null;
}
}
3、使用注解完成缓存
@GmallCache(prefix = RedisConst.SKUKEY_PREFIX)
@Override
public SkuInfo getSkuInfo(Long skuId) {
return getSkuInfoDB(skuId);
}