目录:
(1)分布式锁改造获取sku信息
(2)使用Redisson
分布式锁 + AOP实现缓存
(3)定义缓存aop注解
(1)分布式锁改造获取sku信息
前面学习了本地锁的弊端,和Redis实现分布式锁和Redisson实现分布式锁的案例,都是为了搞糟sku
使用redis
RedisConst 类中追加一个变量
// 商品如果在数据库中不存在那么会缓存一个空对象进去,但是这个对象是没有用的,所以这个对象的过期时间应该不能太长,
// 如果太长会占用内存。
// 定义变量,记录空对象的缓存过期时间
public static final long SKUKEY_TEMPORARY_TIMEOUT = 10 * 60;
package com.atguigu.gmall.common.constant;
/**
 * Redis常量配置类
 * set name admin
 */
public class RedisConst {
    public static final String SKUKEY_PREFIX = "sku:";
    public static final String SKUKEY_SUFFIX = ":info";
    //单位:秒
    public static final long SKUKEY_TIMEOUT = 24 * 60 * 60;
    // 定义变量,记录空对象的缓存过期时间
    public static final long SKUKEY_TEMPORARY_TIMEOUT = 10 * 60;
    //单位:秒 尝试获取锁的最大等待时间
    public static final long SKULOCK_EXPIRE_PX1 = 100;
    //单位:秒 锁的持有时间
    public static final long SKULOCK_EXPIRE_PX2 = 1;
    public static final String SKULOCK_SUFFIX = ":lock";
    public static final String USER_KEY_PREFIX = "user:";
    public static final String USER_CART_KEY_SUFFIX = ":cart";
    public static final long USER_CART_EXPIRE = 60 * 60 * 24 * 30;
    //用户登录
    public static final String USER_LOGIN_KEY_PREFIX = "user:login:";
    //    public static final String userinfoKey_suffix = ":info";
    public static final int USERKEY_TIMEOUT = 60 * 60 * 24 * 7;
    //秒杀商品前缀
    public static final String SECKILL_GOODS = "seckill:goods";
    public static final String SECKILL_ORDERS = "seckill:orders";
    public static final String SECKILL_ORDERS_USERS = "seckill:orders:users";
    public static final String SECKILL_STOCK_PREFIX = "seckill:stock:";
    public static final String SECKILL_USER = "seckill:user:";
    //用户锁定时间 单位:秒
    public static final int SECKILL__TIMEOUT = 60 * 60 * 1;
}
在ManagerServiceImpl:中改造getSkuInfo方法:
把原来里面的代码抽取出来形成一个方法:Ctrl+Alt+M

 

然后再添加一个方法去缓存中查询数据:此时要解决高并发情况下,key不存在,这个时候需加锁,一个去查询,其他请求隔离


在实现类中引入
@Autowired
private RedisTemplate redisTemplate;
// 使用redis' 做分布式锁
private SkuInfo getSkuInfoRedis(Long skuId) {
    SkuInfo skuInfo = null;
    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);
}




10s后锁释放:

(2)使用Redisson


@Autowired
private RedissonClient redissonClient;
private SkuInfo getSkuInfoRedisson(Long skuId) {
    SkuInfo skuInfo = null;
    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){
            // 直接获取数据库中的数据,可能会造成缓存击穿。所以在这个位置,应该添加锁。
            // 第二种:redisson
            // 定义锁的key sku:skuId:lock  set k1 v1 px 10000 nx
            String lockKey = RedisConst.SKUKEY_PREFIX+skuId+RedisConst.SKULOCK_SUFFIX;
            RLock lock = redissonClient.getLock(lockKey);
            /*
            第一种: lock.lock();
            第二种:  lock.lock(10,TimeUnit.SECONDS);
            第三种: lock.tryLock(100,10,TimeUnit.SECONDS);
             */
            // 尝试加锁
            boolean res = lock.tryLock(RedisConst.SKULOCK_EXPIRE_PX1, RedisConst.SKULOCK_EXPIRE_PX2, TimeUnit.SECONDS);
            if (res){
                try {
                    // 处理业务逻辑 获取数据库中的数据
                    // 真正获取数据库中的数据 {数据库中到底有没有这个数据 = 防止缓存穿透}
                    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);
                    // 使用redis 用的是lua 脚本删除 ,但是现在用么? lock.unlock
                    return skuInfo;
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    // 解锁:
                    lock.unlock();
                }
            }else {
                // 其他线程等待
                Thread.sleep(1000);
                return getSkuInfo(skuId);
            }
        }else {
 
            return skuInfo;
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    // 为了防止缓存宕机:从数据库中获取数据
    return getSkuInfoDB(skuId);
}
测试结果跟上面的一样.
我们用页面测试一下:,分别点击一下各个销售属性的sku:

各个sku数据就进行 了缓存

这些缓存的数据,我们也可以进行设置一个缓存的时间:再添加缓存的时候设置缓存时间

分布式锁 + AOP实现缓存
上面的缓存代码我们发现添加了很多代码,如果其他地方也用到了缓存每个地方都需要添加很多代码,这是我们不想看到的,像事务一样我们需要事务了,我们加一个注解就实现了,我们也想这样,我们想要缓存我们加一个注解就解决了,怎么实现呢?、
我们把缓存代码抽取出来,最终进切入,利用aop实现
随着业务中缓存及分布式锁的加入,业务代码变的复杂起来,除了需要考虑业务逻辑本身,还要考虑缓存及分布式锁的问题,增加了程序员的工作量及开发难度。而缓存的玩法套路特别类似于事务,而声明式事务就是用了aop的思想实现的。


 
 

1. 以 @Transactional 注解为植入点的切点,这样才能知道@Transactional注解标注的方法需要被代理。
2. @Transactional注解的切面逻辑类似于@Around
模拟事务,缓存可以这样实现:
1. 自定义缓存注解@GmallCache(类似于事务@Transactional)
2. 编写切面类,使用环绕通知实现缓存的逻辑封装
(3)定义缓存aop注解

 
 
定义一个注解

 
 
 后面两个注解可加可不加
 后面两个注解可加可不加
package com.atguigu.gmall.common.cache;
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface GmallCache {
    /**
     * 缓存key的前缀
           * @return
     */
    String prefix() default "cache";
}
 
定义一个切面类加上注解
package com.atguigu.gmall.common.cache;
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 atguigu-mqx
 * 处理环绕通知
 */
@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 object;
            }return cacheHit(key, signature);
                }
            }else {
        } 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;
    }
}
使用注解完成缓存
@GmallCache(prefix = RedisConst.SKUKEY_PREFIX)
@Override
public SkuInfo getSkuInfo(Long skuId) {
    return getSkuInfoDB(skuId);
}
其他地方:
@GmallCache(prefix = "saleAttrValuesBySpu:")
public Map getSaleAttrValuesBySpu(Long spuId) {
....
}
@GmallCache(prefix = "spuSaleAttrListCheckBySku:")
public List<SpuSaleAttr> getSpuSaleAttrListCheckBySku(Long skuId, Long spuId) {
....
}
@Override
@GmallCache(prefix = "SpuPosterList:")
public List<SpuPoster> getSpuPosterList(Long spuId) {
    //  select * from spu_poster where spu_id = spuId;
    return spuPosterMapper.selectList(new QueryWrapper<SpuPoster>().eq("spu_id",spuId));
}
根据三级分类id获取获取分类信息
@GmallCache(prefix = "categoryViewByCategory3Id:")
public BaseCategoryView getCategoryViewByCategory3Id(Long category3Id) {
....
}



















