商城电商day07 商品详情页面优化
day07 商品详情页面优化
一般分为两个层面,一是提高数据库sql本身的性能,二是尽量避免直接查询数据库。
重点要讲的是另外一个层面:尽量避免直接查询数据库。
解决办法就是:缓存
1.2 整合redis到工程
开始开发先说明redis key的命名规范,由于Redis不像数据库表那样有结构,其所有的数据全靠key进行索引,所以redis数据的可读性,全依靠key。
企业中最常用的方式就是:object🆔field
比如:sku:1314:info
user:1092:info
:表示根据windows的 /一个意思
二、分布式锁
2.1 本地锁的局限性
接下来启动8206 8216 8226 三个运行实例。
运行多个service-product实例:
server.port=8216
server.port=8226
**集群情况下又出问题了!!!
以上测试,可以发现:
本地锁只能锁住同一工程内的资源,在分布式系统里面都存在局限性。
此时需要分布式锁。。
**
2.2 分布式锁实现的解决方案
- 多个客户端同时获取锁(setnx)
- 获取成功,执行业务逻辑{从db获取数据,放入缓存},执行完成释放锁(del)
- 其他客户端等待重试
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
- 加锁和解锁必须具有原子性
2.4 使用redisson 解决分布式锁
1 获得skuKey
2 先查询缓存 通过skuKey从redis获取SkuInfo
3 缓存没有 就要去查数据库
4,先获取锁 若果数据库也没有
5 就把空的数据放入 缓存
6,如果数据库有数据 就把数据放入redis缓存
7,try catch 有异常打印 finally 释放锁
8,如果没获取到锁 睡一会 在尝试
9,缓存有就加入缓存
10 为了防止缓存宕机:从数据库中获取数据 return getSkuInfoDB(skuId);
if (skuInfonull){//先查询缓存 通过skuKey从redis获取SkuInfo
if (res){//先获取锁
if (skuInfonull){ //若果数据库也没有
}
}else{
// 其他线程等待 如果没获取到锁 睡一会 在尝试
Thread.sleep(1000);
return getSkuInfo(skuId);
}
}else{
return skuInfo;
}
return getSkuInfoDB(skuId);
// 使用redis’ 做分布式锁
package com.atguigu.gmall.product.service.impl;import com.alibaba.nacos.client.utils.StringUtils;
import com.atguigu.gmallmon.constant.RedisConst;
import com.atguigu.gmall.model.product.SkuImage;
import com.atguigu.gmall.model.product.SkuInfo;
import com.atguigu.gmall.product.mapper.SkuImageMapper;
import com.atguigu.gmall.product.mapper.SkuInfoMapper;
import com.atguigu.gmall.product.service.TestService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
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.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;import javax.swing.*;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;@Service
public class TestServiceImpl implements TestService {//都是源码的内容 用redis时使用的@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate RedissonClient redissonClient;@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate SkuInfoMapper skuInfoMapper;@Autowiredprivate SkuImageMapper skuImageMapper;/*// 使用redis' 做分布式锁private SkuInfo getSkuInfoRedis(Long skuId) {SkuInfo skuInfo = null;try {// 缓存存储数据:key-value// 定义key sku:skuId:infoString 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 nxString 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脚本返回类型为LongredisScript.setResultType(Long.class);redisScript.setScriptText(script);// 删除key 所对应的 valueredisTemplate.execute(redisScript, Arrays.asList(lockKey),uuid);return skuInfo;}else {// 其他线程等待Thread.sleep(1000);return getSkuInfoRedis(skuId);}}else {return skuInfo;}} catch (InterruptedException e) {e.printStackTrace();}// 为了防止缓存宕机:从数据库中获取数据return getSkuInfoDB(skuId);}
*/@Overridepublic void testLock() {}@Overridepublic SkuInfo getSkuInfoDB(Long skuId) {SkuInfo skuInfo = skuInfoMapper.selectById(skuId);if (skuInfo!=null){QueryWrapper<SkuImage> skuImageQueryWrapper = new QueryWrapper<>();skuImageQueryWrapper.eq("sku_id",skuId);List<SkuImage> skuImageList = skuImageMapper.selectList(skuImageQueryWrapper);skuInfo.setSkuImageList(skuImageList);}return skuInfo;}
Redisson做分布式锁
private SkuInfo getSkuInfoRedisson(Long skuId) {SkuInfo skuInfo = null;try {// 缓存存储数据:key-value// 定义key sku:skuId:infoString 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 nxString 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;}// 查询数据库的时候,有值 放入到redis里边redisTemplate.opsForValue().set(skuKey,skuInfo,RedisConst.SKUKEY_TIMEOUT,TimeUnit.SECONDS);// 使用redis 用的是lua 脚本删除 ,但是现在用么? lock.unlockreturn 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);}@Overridepublic SkuInfo getSkuInfo(Long skuId) {// 使用框架redisson解决分布式锁!return getSkuInfoRedisson(skuId);// return getSkuInfoRedis(skuId);}
}
四、分布式锁 + AOP实现缓存
package com.atguigu.gmallmon.cache;
import com.alibaba.fastjson.JSON;
import com.atguigu.gmallmon.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 {@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate RedissonClient redissonClient;// 切GmallCache注解//小辣椒异常处理注解@SneakyThrows@Around("@annotation(com.atguigu.gmallmon.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;}
}
day 08 二、首页商品分类实现
思路:
1,首页属于并发量比较高的访问页面,我看可以采取页面静态化方式实现,或者把数据放在缓存中实现
2,我们把生成的静态文件可以放在nginx访问或者放在web-index模块访问
2.2 封装数据接口
由于商品分类信息在service-product模块,我们在该模块封装数据,数据结构为父子层级
商品分类保存在base_category1、base_category2和base_category3表中,由于需要静态化页面,我们需要一次性加载所有数据,前面我们使用了一个视图base_category_view,所有我从视图里面获取数据,然后封装为父子层级
数据结构如下:json 数据结构
分析返回的数据格式 list
list集合里不是特定的对象 用JSONObject
[{"index": 1,"categoryChild": [ // 获取一级分类下面的所有集合{"categoryChild": [{"categoryName": "电子书", # 三级分类的name"categoryId": 1},{"categoryName": "网络原创", # 三级分类的name"categoryId": 2},...],"categoryName": "电子书刊", #二级分类的name "categoryId": 1},...],"categoryName": "图书、音像、电子书刊", # 一级分类的name// 一级分类名称"categoryId": 1// 获取一级分类Id},
// 变量迭代
index++;
2.2.1 ManageService接口
public class JSONObject extends JSON implements Map
/*** 获取全部分类信息* @return*/List<JSONObject> getBaseCategoryList();
查询所有分级的标题并且返回list集合
@Override@GmallCache(prefix = "category")public List<JSONObject> getBaseCategoryList() {//存储所有的分类信息ArrayList<JSONObject> jsonObjects = new ArrayList<>();// 声明获取所有分类数据集合List<BaseCategoryView> baseCategoryViewList = baseCategoryViewMapper.selectList(null);// 循环上面的集合并安一级分类Id 进行分组Map<Long, List<BaseCategoryView>> category1Map = baseCategoryViewList.stream().collect(Collectors.groupingBy(BaseCategoryView::getCategory1Id));int index = 1;// 获取一级分类下所有数据 list集合里边是json对象所以用for (Map.Entry<Long, List<BaseCategoryView>> entry1 : category1Map.entrySet()) {Long category1Id = entry1.getKey();List<BaseCategoryView> category2List1 = entry1.getValue();//JSONObject category1 = new JSONObject();category1.put("index", index);category1.put("categoryId", category1Id);// 一级分类名称category1.put("categoryName", category2List1.get(0).getCategory1Name());// 变量迭代index++;// 循环获取二级分类数据Map<Long, List<BaseCategoryView>> category2Map = category2List1.stream().collect(Collectors.groupingBy(BaseCategoryView::getCategory2Id));// 声明二级分类对象集合List<JSONObject> category2Child = new ArrayList<>();// 循环遍历for (Map.Entry<Long, List<BaseCategoryView>> entry2 : category2Map.entrySet()) {// 获取二级分类IdLong category2Id = entry2.getKey();// 获取二级分类下的所有集合List<BaseCategoryView> category3List = entry2.getValue();// 声明二级分类对象JSONObject category2 = new JSONObject();category2.put("categoryId", category2Id);category2.put("categoryName", category3List.get(0).getCategory2Name());// 添加到二级分类集合category2Child.add(category2);List<JSONObject> category3Child = new ArrayList<>();// 循环三级分类数据category3List.stream().forEach(category3View -> {JSONObject category3 = new JSONObject();category3.put("categoryId", category3View.getCategory3Id());category3.put("categoryName", category3View.getCategory3Name());category3Child.add(category3);});// 将三级数据放入二级里面category2.put("categoryChild", category3Child);}// 将二级数据放入一级里面category1.put("categoryChild", category2Child);jsonObjects.add(category1);}return jsonObjects;}
商城电商day07 商品详情页面优化
day07 商品详情页面优化
一般分为两个层面,一是提高数据库sql本身的性能,二是尽量避免直接查询数据库。
重点要讲的是另外一个层面:尽量避免直接查询数据库。
解决办法就是:缓存
1.2 整合redis到工程
开始开发先说明redis key的命名规范,由于Redis不像数据库表那样有结构,其所有的数据全靠key进行索引,所以redis数据的可读性,全依靠key。
企业中最常用的方式就是:object🆔field
比如:sku:1314:info
user:1092:info
:表示根据windows的 /一个意思
二、分布式锁
2.1 本地锁的局限性
接下来启动8206 8216 8226 三个运行实例。
运行多个service-product实例:
server.port=8216
server.port=8226
**集群情况下又出问题了!!!
以上测试,可以发现:
本地锁只能锁住同一工程内的资源,在分布式系统里面都存在局限性。
此时需要分布式锁。。
**
2.2 分布式锁实现的解决方案
- 多个客户端同时获取锁(setnx)
- 获取成功,执行业务逻辑{从db获取数据,放入缓存},执行完成释放锁(del)
- 其他客户端等待重试
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
- 加锁和解锁必须具有原子性
2.4 使用redisson 解决分布式锁
1 获得skuKey
2 先查询缓存 通过skuKey从redis获取SkuInfo
3 缓存没有 就要去查数据库
4,先获取锁 若果数据库也没有
5 就把空的数据放入 缓存
6,如果数据库有数据 就把数据放入redis缓存
7,try catch 有异常打印 finally 释放锁
8,如果没获取到锁 睡一会 在尝试
9,缓存有就加入缓存
10 为了防止缓存宕机:从数据库中获取数据 return getSkuInfoDB(skuId);
if (skuInfonull){//先查询缓存 通过skuKey从redis获取SkuInfo
if (res){//先获取锁
if (skuInfonull){ //若果数据库也没有
}
}else{
// 其他线程等待 如果没获取到锁 睡一会 在尝试
Thread.sleep(1000);
return getSkuInfo(skuId);
}
}else{
return skuInfo;
}
return getSkuInfoDB(skuId);
// 使用redis’ 做分布式锁
package com.atguigu.gmall.product.service.impl;import com.alibaba.nacos.client.utils.StringUtils;
import com.atguigu.gmallmon.constant.RedisConst;
import com.atguigu.gmall.model.product.SkuImage;
import com.atguigu.gmall.model.product.SkuInfo;
import com.atguigu.gmall.product.mapper.SkuImageMapper;
import com.atguigu.gmall.product.mapper.SkuInfoMapper;
import com.atguigu.gmall.product.service.TestService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
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.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;import javax.swing.*;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;@Service
public class TestServiceImpl implements TestService {//都是源码的内容 用redis时使用的@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate RedissonClient redissonClient;@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate SkuInfoMapper skuInfoMapper;@Autowiredprivate SkuImageMapper skuImageMapper;/*// 使用redis' 做分布式锁private SkuInfo getSkuInfoRedis(Long skuId) {SkuInfo skuInfo = null;try {// 缓存存储数据:key-value// 定义key sku:skuId:infoString 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 nxString 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脚本返回类型为LongredisScript.setResultType(Long.class);redisScript.setScriptText(script);// 删除key 所对应的 valueredisTemplate.execute(redisScript, Arrays.asList(lockKey),uuid);return skuInfo;}else {// 其他线程等待Thread.sleep(1000);return getSkuInfoRedis(skuId);}}else {return skuInfo;}} catch (InterruptedException e) {e.printStackTrace();}// 为了防止缓存宕机:从数据库中获取数据return getSkuInfoDB(skuId);}
*/@Overridepublic void testLock() {}@Overridepublic SkuInfo getSkuInfoDB(Long skuId) {SkuInfo skuInfo = skuInfoMapper.selectById(skuId);if (skuInfo!=null){QueryWrapper<SkuImage> skuImageQueryWrapper = new QueryWrapper<>();skuImageQueryWrapper.eq("sku_id",skuId);List<SkuImage> skuImageList = skuImageMapper.selectList(skuImageQueryWrapper);skuInfo.setSkuImageList(skuImageList);}return skuInfo;}
Redisson做分布式锁
private SkuInfo getSkuInfoRedisson(Long skuId) {SkuInfo skuInfo = null;try {// 缓存存储数据:key-value// 定义key sku:skuId:infoString 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 nxString 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;}// 查询数据库的时候,有值 放入到redis里边redisTemplate.opsForValue().set(skuKey,skuInfo,RedisConst.SKUKEY_TIMEOUT,TimeUnit.SECONDS);// 使用redis 用的是lua 脚本删除 ,但是现在用么? lock.unlockreturn 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);}@Overridepublic SkuInfo getSkuInfo(Long skuId) {// 使用框架redisson解决分布式锁!return getSkuInfoRedisson(skuId);// return getSkuInfoRedis(skuId);}
}
四、分布式锁 + AOP实现缓存
package com.atguigu.gmallmon.cache;
import com.alibaba.fastjson.JSON;
import com.atguigu.gmallmon.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 {@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate RedissonClient redissonClient;// 切GmallCache注解//小辣椒异常处理注解@SneakyThrows@Around("@annotation(com.atguigu.gmallmon.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;}
}
day 08 二、首页商品分类实现
思路:
1,首页属于并发量比较高的访问页面,我看可以采取页面静态化方式实现,或者把数据放在缓存中实现
2,我们把生成的静态文件可以放在nginx访问或者放在web-index模块访问
2.2 封装数据接口
由于商品分类信息在service-product模块,我们在该模块封装数据,数据结构为父子层级
商品分类保存在base_category1、base_category2和base_category3表中,由于需要静态化页面,我们需要一次性加载所有数据,前面我们使用了一个视图base_category_view,所有我从视图里面获取数据,然后封装为父子层级
数据结构如下:json 数据结构
分析返回的数据格式 list
list集合里不是特定的对象 用JSONObject
[{"index": 1,"categoryChild": [ // 获取一级分类下面的所有集合{"categoryChild": [{"categoryName": "电子书", # 三级分类的name"categoryId": 1},{"categoryName": "网络原创", # 三级分类的name"categoryId": 2},...],"categoryName": "电子书刊", #二级分类的name "categoryId": 1},...],"categoryName": "图书、音像、电子书刊", # 一级分类的name// 一级分类名称"categoryId": 1// 获取一级分类Id},
// 变量迭代
index++;
2.2.1 ManageService接口
public class JSONObject extends JSON implements Map
/*** 获取全部分类信息* @return*/List<JSONObject> getBaseCategoryList();
查询所有分级的标题并且返回list集合
@Override@GmallCache(prefix = "category")public List<JSONObject> getBaseCategoryList() {//存储所有的分类信息ArrayList<JSONObject> jsonObjects = new ArrayList<>();// 声明获取所有分类数据集合List<BaseCategoryView> baseCategoryViewList = baseCategoryViewMapper.selectList(null);// 循环上面的集合并安一级分类Id 进行分组Map<Long, List<BaseCategoryView>> category1Map = baseCategoryViewList.stream().collect(Collectors.groupingBy(BaseCategoryView::getCategory1Id));int index = 1;// 获取一级分类下所有数据 list集合里边是json对象所以用for (Map.Entry<Long, List<BaseCategoryView>> entry1 : category1Map.entrySet()) {Long category1Id = entry1.getKey();List<BaseCategoryView> category2List1 = entry1.getValue();//JSONObject category1 = new JSONObject();category1.put("index", index);category1.put("categoryId", category1Id);// 一级分类名称category1.put("categoryName", category2List1.get(0).getCategory1Name());// 变量迭代index++;// 循环获取二级分类数据Map<Long, List<BaseCategoryView>> category2Map = category2List1.stream().collect(Collectors.groupingBy(BaseCategoryView::getCategory2Id));// 声明二级分类对象集合List<JSONObject> category2Child = new ArrayList<>();// 循环遍历for (Map.Entry<Long, List<BaseCategoryView>> entry2 : category2Map.entrySet()) {// 获取二级分类IdLong category2Id = entry2.getKey();// 获取二级分类下的所有集合List<BaseCategoryView> category3List = entry2.getValue();// 声明二级分类对象JSONObject category2 = new JSONObject();category2.put("categoryId", category2Id);category2.put("categoryName", category3List.get(0).getCategory2Name());// 添加到二级分类集合category2Child.add(category2);List<JSONObject> category3Child = new ArrayList<>();// 循环三级分类数据category3List.stream().forEach(category3View -> {JSONObject category3 = new JSONObject();category3.put("categoryId", category3View.getCategory3Id());category3.put("categoryName", category3View.getCategory3Name());category3Child.add(category3);});// 将三级数据放入二级里面category2.put("categoryChild", category3Child);}// 将二级数据放入一级里面category1.put("categoryChild", category2Child);jsonObjects.add(category1);}return jsonObjects;}