目录
Day5 制作SKU
1. 制作SKU
2. 多表查询如何写?
3. 制作SKU
4. Thymeleaf
Day06 商品详情
1. 获取分类信息
2. 获取最新价格信息
3. 获取销售信息
4. 实现商品切换
5. 获取海报信息
6. Sku对应的平台属性 -- 规格参数
7. 创建远程feign 调用
8. service-item 汇总数据
9. 搭建 web、访问测试
Day07 Redis缓存、redssion分布式锁
1. 项目如何优化?
2. 讲讲 redis 主从、集群原理
3. 缓存穿透、缓存击穿、缓存雪崩 ★
4. 本地锁 -- Gateway网关配置
5. redis 分布式锁
6. redisson 解决分布式锁
7. 锁机制
SPU:一组可复用易检索的标准化信息集合
SKU:每种商品均对应唯一的编号
sku 图片 都应该是基于 SPU 选择的!
sku 应该有属于自己的url 地址!
SKU 相关表结构:
spuInfo:
id,spuName,category3Id,tmId,createTime,updateTime,isDelete
1, 红旗手机, 61 , 1skuInfo: 库存单元表
id,skuName,spuId,price,category3Id,defaultImage,isSale,createTime,updateTime,isDelete
1 ,红旗1 , 1 0/1
2 ,红旗2 , 1
skuImage: 库存单元图片表
id,imgName,skuId,imgUrl,createTime,updateTime,isDelete
1, xx1 1 http://
2, xx2 1
3, xx3 1sku 与 销售属性值Id 有关系么?
skuSaleAttrValue: sku 与 销售属性值Id 中间表 spuSaleAttrValue
id,skuId,spuSaleAttrId, spuSaleAttrValueId,createTime,updateTime,isDelete
1, 1 , 1, 1
2, 1 , 2, 1sku 与 平台属性值Id 有关系么?
skuAttrValue: sku 与 平台属性值Id 中间表 baseAttrValue
id,skuId,saleAttrId, saleAttrValueId,createTime,updateTime,isDelete
1, 1 , 1, 1
2, 1 , 2, 1商品数量:会单独有一个库存系统
skuId , skuNum
1, 1000
2, 10000
1. 找表 (字段从哪些表中去获取)
2. 找关联关系:主外键、名字相同的、名字不同含义相同...
3. 根据业务添加筛选条件,分组,排序,分页...
4. sql调优:小表Join大表、使用索引覆盖,避免全盘扫描...
为什么要回显这个功能?
在商品检索页面的时候,能够保证用户可以通过平台属性值Id 进行检索出 sku功能分析:
1. 回显平台属性与平台属性值
http://localhost/admin/product/attrInfoList/2/13/612. 回显销售属性与销售属性值
http://localhost/admin/product/spuSaleAttrList/12
ssa.id, ssa.spu_id, ssa.base_sale_attr_id, ssa.sale_attr_name, ssav.id sale_attr_value_id, ssav.sale_attr_value_name 3. 回显spuImage!
select * from spu_image where spu_id = ? and is_delete = 0;
skuImage 都是从spuImage 中选择的!http://localhost/admin/product/spuImageList/12
4. 保存skuInfo
http://localhost/admin/product/saveSkuInfo(需操作4张表)
测试:
小米:
skuId = 25 小米(MI)CC9 屏幕指纹美颜自拍手机 仙女渐变色(美图定制版)6GB+128GB
skuId = 26 小米(MI)CC9 屏幕指纹美颜自拍手机 仙女渐变色(美图定制版)6GB+64GB
华为:
skuId = 27 荣耀(HONOR) 荣耀V30 PRO 双模5G全网通手机 麒麟990处理器 V30 Pro【幻夜星河】 全网通(8GB+256GB)
skuId = 28 荣耀(HONOR) 荣耀V30 PRO 双模5G全网通手机 麒麟990处理器 V30 Pro【冰岛幻境】 全网通(8GB+256GB)5. 根据分类Id 查询skuInfo 列表
Request URL: http://localhost/admin/product/list/1/10?category3Id=61
/admin/product/list/{page}/{limit}select * from skuInfo where category3_id = ? and is_delete = 0 order by id desc limit 0, 10; mysql;
6. 商品上架- 下架:
http://localhost/admin/product/onSale/28
http://localhost/admin/product/cancelSale/227. 修改SKU (多表联查) 课后
//通过ID回显SkuInfo @Override public SkuInfo getSkuInfoById(Long skuId) { //skuImageList List
skuImageList = skuImageMapper.selectList(new QueryWrapper ().eq("sku_id", skuId)); //skuAttrValueList (Join 多表联查) List skuAttrValueList = skuAttrValueMapper.selectBySkuId(skuId); //skuSaleAttrValueList (Join 多表联查) List skuSaleAttrValueList = skuSaleAttrValueMapper.selectBySkuId(skuId); //skuInfo SkuInfo skuInfo = skuInfoMapper.selectById(skuId); skuInfo.setSkuSaleAttrValueList(skuSaleAttrValueList); skuInfo.setSkuImageList(skuImageList); skuInfo.setSkuAttrValueList(skuAttrValueList); return skuInfo; } //修改SKU @Override @Transactional(rollbackFor = Exception.class) public void updateSkuInfo(SkuInfo skuInfo) { skuInfoMapper.updateById(skuInfo); Long skuId = skuInfo.getId(); //先删除,后新增 skuAttrValueMapper.delete(new QueryWrapper
().eq("sku_id",skuId)); skuImageMapper.delete(new QueryWrapper ().eq("sku_id",skuId)); skuSaleAttrValueMapper.delete(new QueryWrapper ().eq("sku_id",skuId)); //复用新增,插入skuInfo时判断是否存在id this.saveSkuInfo(skuInfo); }
渲染:Thymeleaf ---> 使用!
th:text 显示文本
th:value 给 value 属性赋值 ,在表单提交的时候,都是提交的value 值点击提交的时候,会将value 属性值 传递到后台
@RequestMapping("login") public void login(String userName,String pwd){ userName = admin; pwd = "123456" }
th:if 符合判断:
th:unless 不符合判断:
th:each 循环遍历集合
th:include 内嵌页{页面,【css,js,图片】--- 静态资源 springmvc }
th:utext -- 页面解析样式!
th:session 接收session数据
th:href 超链接扩展:存储数据对象使用!
// 商品详情页面渲染使用!
HashMaphashMap = new HashMap<>();
hashMap.put("id","1002");
hashMap.put("stuName","atguigu");
hashMap.put("stuAge","9");model.addAllAttributes(hashMap);
配置:
1. Thymeleaf 有默认的前缀,后缀2. Thymeleaf 还可以开启热部署,修改页面之后,不需要重启服务!
spring.thymeleaf.cache=false1. Thymeleaf 有默认的前缀,后缀
2. Thymeleaf 还可以开启热部署,修改页面之后,不需要重启服务!
spring.thymeleaf.cache=false
功能划分:
1、 分类数据的获取!2、 skuInfo 的基本信息,skuName,defaultImage,weight
3、 价格单独查询 -- 保证价格实时性!
4、 skuImage 集合
5、 销售属性回显并锁定!
6、 点击不同的销售属性值实现切换功能!
7、 查询海报信息!
8、 规格与包装!
包含商品的平台属性数据!
暂时使用商品的平台属性进行渲染!----------------------------------------------------------------------------------------------
9、 商品评价 远程调用 spuId;10、 手机社区BBS 论坛!
模板划分:
web-all :页面渲染service-item : 商品详情微服务
service-product : 商品数据提供微服务
server-gateway : 接收用户所有的请求: http://item.gmall.com/28.html
解析域名 item.gmall.com --->web-all !
server-gateway ---> web-all ---> service-item{汇总数据使用map 进行存储!} ---> service-product{查询数据}
从分布式微服务来讲,service-item 汇总数据不能省。
sku是挂在三级分类下面的,我们的分类信息分别在base_category1、base_category2、base_category3这三张表里面,目前需要通过sku表的三级分类id获取一级分类名称、二级分类名称和三级分类名称
解决方案:
我们可以建立一个视图(view),把三张表关联起来,视图id就是三级分类id,这样通过三级分类id就可以查询到相应数据,效果如下
create or replace view base_category_view as
select c3.id id,
c1.id c1_id,c1.name c1_name,
c2.id c2_id,c2.name c2_name,
c3.id c3_id,c3.name c3_name
from base_category1 c1 join base_category2 c2
on c1.id = c2.category1_id
join base_category3 c3
on c2.id = c3.category2_id;
public BaseCategoryView getCategoryViewByCategory3Id(Long category3Id) {
//select * from base_category_view where id = 61
/*#使用一条语句:view视图:随着原数据的变化而变化 id = category3Id
# 如果数据频繁更变,开发组中明确规定不能使用视图
*/
return baseCategoryViewMapper.selectById(category3Id);
}
public BigDecimal getSkuPrice(Long skuId) {
//SkuInfo skuInfo = skuInfoMapper.selectById(skuId);
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.eq("id",skuId);
//设置指定查询字段
wrapper.select("price");
SkuInfo skuInfo = skuInfoMapper.selectOne(wrapper);
if(skuInfo!=null){
return skuInfo.getPrice();
}
return new BigDecimal("0");
}
查询销售属性+销售属性值;查询skuId 对应的销售属性值Id 是谁;将a,b 结合!
#单独限制某一张表,条件放在on后面
select
ssa.id,
ssa.spu_id,
ssa.base_sale_attr_id,
ssa.sale_attr_name,
ssav.id sale_attr_value_id,
ssav.sale_attr_value_name,
sv.sku_id,
if(sv.sku_id is null,0,1) is_checked
from spu_sale_attr ssa join spu_sale_attr_value ssav
on ssa.spu_id = ssav.spu_id
and ssa.base_sale_attr_id = ssav.base_sale_attr_id
left join sku_sale_attr_value sv
on sv.sale_attr_value_id = ssav.id
and sv.sku_id = ?
where ssa.spu_id = ?
and ssa.is_deleted = 0
and ssav.is_deleted = 0
and (sv.is_deleted = 0 or sv.is_deleted is null)
order by ssav.base_sale_attr_id,ssav.id;
用户通过点击不同的销售属性值,来切换到不同的skuId
思路:在后台将所有的销售属性值Id 与 skuId 进行组合
3742|3745 = 28、3743|3745 = 27
{"3742|3745":28,"3743|3745":27} --- Json 对象
必须是同一组:skuId , 过滤条件 必须是同一个商品 spuId
# 3746|3744 3747|3744
# group_concat(要拼接的字段 排序字段 分隔符);不写默认,
# 排序字段复杂一点,可以按照销售id排序,切换的时候要保证与回显数据一致,可能会导致数据拼接或切换顺序错乱
select group_concat(ssav.sale_attr_value_id order by ssav.id desc separator '|')value_ids,
sku_id
from sku_sale_attr_value ssav
where ssav.spu_id = ?
group by sku_id;
public List findSpuPosterBySpuId(Long spuId) {
List spuPosterList = spuPosterMapper.selectList(new QueryWrapper().eq("spu_id", spuId));
return spuPosterList;
}
根据 skuId 平台属性数据:---- 规格参数
select bai.id,bai.attr_name,bai.category_id,bai.category_level,
bav.id attr_value_id,bav.value_name
from base_attr_info bai join base_attr_value bav
on bai.id = bav.attr_id
join sku_attr_value sav
on sav.value_id = bav.id and sav.sku_id = ?
@Service
public class ItemServiceImpl implements ItemService {
@Qualifier("com.atguigu.gmall.product.client.ProductFeignClient")
@Autowired
private ProductFeignClient productFeignClient;
//根据skuId 获取渲染数据
@Override
public Map getItem(Long skuId) {
HashMap map = new HashMap<>();
// 获取商品的基本信息 + 商品图片列表
SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId);
//获取分类数据
BaseCategoryView categoryView = productFeignClient.getCategoryView(skuInfo.getCategory3Id());
//获取价格
BigDecimal skuPrice = productFeignClient.getSkuPrice(skuId);
//获取销售属性+属性值+锁定
List spuSaleAttrList = productFeignClient.getSpuSaleAttrListCheckBySku(skuId, skuInfo.getSpuId());
//获取海报
List spuPosterList = productFeignClient.getSpuPosterBySpuId(skuInfo.getSpuId());
//获取数据,转换 json 字符串
Map skuValueIdsMap = productFeignClient.getSkuValueIdsMap(skuInfo.getSpuId());
String strJson = JSON.toJSONString(skuValueIdsMap);
//获取商品规格参数--平台属性
List attrList = productFeignClient.getAttrList(skuId);
if(!CollectionUtils.isEmpty(attrList)){
List> attrMapList = attrList.stream().map(baseAttrInfo -> {
// 为了迎合页数据存储,定义一个map集合
HashMap hashMap = new HashMap<>();
hashMap.put("attrName", baseAttrInfo.getAttrName());
hashMap.put("attrValue", baseAttrInfo.getAttrValueList().get(0).getValueName());
return hashMap;
}).collect(Collectors.toList());
// 保存规格参数:只需要平台属性名:平台属性值
map.put("skuAttrList",attrMapList);
}
map.put("skuInfo",skuInfo);
map.put("categoryView",categoryView);
map.put("price",skuPrice);
map.put("spuSaleAttrList",spuSaleAttrList);
map.put("spuPosterList",spuPosterList);
map.put("valuesSkuJson",strJson);
return map;
}
}
访问测试: http://item.gmall.com/22.html
Loc锁 synchronized 区别?
线程间如何通信?
使用各种中间件
TomCat默认并发500,设置两个参数;服务器带宽
1. 架构优化:单体架构-垂直架构-rpc-soa-分布式微服务;时间成本(学习-代码重构)
2. 增加服务器:各种集群
mysql集群:mycat 垂直分库(按照业务) 水平分表(安装某个字段)
Redis主从复制+集群
3. 减少io:减少用户访问次数 -- redis缓存;
索引优化:explain: all --> index ---> 【range ---> ref】---> er_ref ---> const --->sysetm
单表:全值匹配、最佳左前缀、否定会导致索引失效 != <> is not null、范围右边会失效、like
关联:被驱动表建立索引,不用内连接
子查询:改成 A left join B where A.id is null;
分组-排序:无过滤不索引;顺序错,必排序;方向反,必排序
建议:不要写 * ,尽量使用覆盖索引;减少回表,尽量使用索引下推
4. 减少同步:减少同步操作 -- mq异步解耦
5. redis优化
集群原理:
16384个槽 (slot) 与 key进行运算:crc32 算法 获取到一个值,根据这个值决定在那一组节点上存放数据。
redis - 哨兵 主从复制原理:
1. 从机发送复制请求 sync 异步命令
2. 全量复制:主机进行bgsave: 拍个快照把当前所有状态全都发过去
3. 异步增量复制:后续将写入或修改命令 发送给从机!
缓存穿透:查询一个不存在的数据,由于缓存不存在,直接去查询数据库,但是数据库也无此记录,所以我们没有将null写入缓存。但这导致每次请求都会访问数据库,别人可以利用不存在的 key频繁攻击我们的应用。
解决方案:缓存null对象 或 使用布隆过滤器
缓存击穿:是指对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:如果这个key在大量请求同时进来之前正好失效,那么所有对这个key的数据查询都落到DB,我们称为缓存击穿。
解决方案:redis;redisson 分布式锁 在分布式的环境下,应使用分布式锁来解决,分布式锁的实现方案有多种,比如使用Redis的setnx、使用Zookeeper的临时顺序节点等来实现
缓存雪崩:Redis宕机;或在设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决方案:
1. 事前:redis高可用,主从+哨兵;事中:本地缓存ehcache+限流组件Hystrix;防止宕机;事后:redis持久化 快速恢复缓存数据
2. 原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
与缓存雪崩的区别:
1. 击穿是一个热点key失效
2. 雪崩是很多key集体失效
总结:本地锁的局限性 --- 分布式微服务集群部署时,锁不住资源!
routes:
- id: service-product
uri: lb://service-product
predicates:
- Path=/*/product/** # 路径匹配
小知识点:port 的优先级,nacos 的优先级高于 运行类配置 高于配置文件!Gateway网关负载均衡 默认轮询
改网关访问这三个微服务:
8206
8216
8226
ab -n 5000 -c 100 http://192.168.200.1/admin/product/test/testLock
synchronized 锁不住资源!
分布式锁实现的解决方案
1. 基于数据库实现分布式锁;IO限制
2. 基于缓存(Redis等) 性能高 基于iosetnx key value; 判断当前这个key是否存在,存在则不生效;
Boolean result = redisTemplate.opsForValue().setIfAbsent("lock","ok")
3. 基于Zookeeper 安全性高ZNode节点;持久化节点 非持久化节点,将节点看做锁
性能redis最高;可靠性zookeeper最高
Redis 分布式锁,电商方式
1、setnx key value:容易出现死锁;需要清空锁
2、设置锁并加默认的过期时间:多线程误删锁
expire key timeout 不具备原子性!
setex key timeout value --- key 与 过期时间具有原子性!redis 2.6.12 版本以后:setnx + setex 命令整合
set key value ex/px timeout nx/xx; 具有原子性
NX :键不存在时,才对键进行设置操作;XX :键已经存在时,才对键进行设置操作。3、设置一个uuid 防止误删锁:删除锁缺乏原子性
4、使用lua 脚本保证删除锁具有原子性:集群情况下锁不住资源
lock,unlock 可能产生锁死,不需等待自旋机制;trylock可设定过期时间,需要等待自旋机制。
// 这个脚本只在客户端传入的值和键的口令串相匹配时,才对键进行删除 String scriptText = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; DefaultRedisScript defaultRedisScript = new DefaultRedisScript<>(); defaultRedisScript.setResultType(Long.class); defaultRedisScript.setScriptText(scriptText); // 第一个参数:defaultRedisScript 第二个参数:键值 第三个参数:口令串 this.redisTemplate.execute(defaultRedisScript, Arrays.asList("lock"),uuid);
5、redisson:Redisson 提供了使用Redis的最简单和最便捷的方法,Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
官方文档地址:Home · redisson/redisson Wiki · GitHub
单节点:redisson - Lock、TryLock
集群:redisson - redLock
1、最常用的方式:lock.lock();lock.unlock();
redisson : 有个宠物 --- 作用防止锁死状态! --- 监控redis, 如果redis 实例宕机了,则会默认设置锁的有效期,默认是30秒钟; 也可以通过 Config.lockWatchdogTimeout 设置。
2、在加锁的时候自定义锁的有效期:加锁以后10秒钟自动解锁
lock.lock(10, TimeUnit.SECONDS);3、尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
// 业务逻辑
} finally {
lock.unlock();
}
}else {//等待、自旋
}
底层源码:lua脚本,AQS
可重入锁:某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
公平锁:按照申请锁的顺序去获得锁,线程会直接进入队列,先进先出。
连锁:若干锁同时上锁成功才算成功。new RedissonMultiLock(lock...)
红锁:大部分节点上加锁就算成功。new RedissonRedLock(lock...)
读写锁:读读可并发,其他都不可并发
信号量(Semaphore):多线程访问多资源,控制资源量;单资源加锁即可
闭锁(CountDownLatch倒计时线程控制):倒计时线程控制;减少计数为0时执行。