谷粒商城-个人笔记(高级篇四)
防止面试官问你RabbitMQ的知识,你除了把以前RabbitMQ的知识复习一遍外,你还要把雷丰阳老师在谷粒商城中的RabbitMQ也要复习一遍,二者还是有点差别的。
除了环境搭建,还整合了SpringSession、线程池
添加登录拦截
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberResponseVO> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
MemberResponseVO attribute = (MemberResponseVO) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute != null){
loginUser.set(attribute);
return true;
}else {
//没登录就去登录
request.getSession().setAttribute("msg","请先进行登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
总说:
前端发来请求访问http://order.gulimall.com/toTrade
,后台要带着必要的信息重定向到订单确认页。需要哪些信息呢?
List address
,需要使用OpenFeign远程查询gulimall-member,gulimall-member查询数据库ums_member_receive_address表即可List items
,需要使用OpenFeign远程查询gulimall-cart,在gulimall-cart里面当初我们把登录信息存到session中、把购物车信息存到redis中,所以根据登录拦截器返回的用户信息,只需要拿着用户信息查询redis中该用户的购物车中选中去结账的
商品,然后给这些商品结账时还要查询这些商品最新的价格(你添加商品到购物车一个月现在才结账,谁知道商品价格变了没),查询商品价格需要使用OpenFeign远程查询gulimall-product的pms_sku_info表即可。Integer integration;
,我们登录拦截器返回来的用户信息里面就包含用户积分信息BigDecimal total;
,(商品数量x价格)=订单总额BigDecimal payPrice;
,先不说String orderToken;
,你提交订单页面如果刷新多次就会造成提交多次订单,所以需要防重令牌。问题
p265的代码和思路没有问题,但是测试的时候发现gulimall-order使用OpenFeign远程查询gulimall-cart的购物车信息时,经过gulimall-cart的登录拦截器时居然是没有登陆状态。你明明已经在gulimall-order中登录了,为什么会被远程调用的gulimall-cart的登录拦截器拦截下来?
解释
因为浏览器先发送http://order.gulimall.com/toTrade
给gulimall-order时携带了请求头(有请求头就有cookie,有cookie就能查到session),但是使用OpenFeign远程查询gulimall-cart的购物车信息时会新创建一个请求头,丢弃原来的请求头。
解决办法
使用OpenFeign远程查询gulimall-cart的购物车信息时会新创建一个请求头,我们写一个feign的拦截器,在拦截器里面为请求加上cookie。
修改gulimall-order的config类
/**
* @Description: 请求拦截器
*/
@Configuration
public class GuliFeignConfig {
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor(){
@Override
public void apply(RequestTemplate requestTemplate) {
//1、RequestContextHolder拿到刚进来的请求
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();//老请求
//同步请求头数据。Cookie
String cookie = request.getHeader("Cookie");
//给新请求同步了老请求的cookie
requestTemplate.header("Cookie",cookie);
System.out.println("feign远程之前先执行RequestInterceptor.apply()");
}
};
}
}
1.原来的代码
@Override
public OrderConfirmVo confirmOrder() {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get();
//1、远程查询所有的收货地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVO.getId());
confirmVo.setAddress(address);
//2、远程查询购物车所有选中的购物项
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
//3、查询用户积分
Integer integration = memberResponseVO.getIntegration();
confirmVo.setIntegration(integration);
//4、其他数据自动计算
return confirmVo;
}
2.把原来的代码改为异步的方式
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get();
//异步任务编排
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
//1、远程查询所有的收货地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVO.getId());
confirmVo.setAddress(address);
}, executor);
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
//2、远程查询购物车所有选中的购物项
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
}, executor);
//3、查询用户积分
Integer integration = memberResponseVO.getIntegration();
confirmVo.setIntegration(integration);
CompletableFuture.allOf(getAddressFuture,cartFuture).get();
return confirmVo;
}
3.存在的问题
4.解决
需要在开启异步的时候将老请求的RequestContextHolder的数据设置进去
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get();
//获取之前的请求
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);//每一个线程都来共享之前的请求数据
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVO.getId());
confirmVo.setAddress(address);
}, executor);
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);//每一个线程都来共享之前的请求数据
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
}, executor);
Integer integration = memberResponseVO.getIntegration();
confirmVo.setIntegration(integration);
CompletableFuture.allOf(getAddressFuture,cartFuture).get();
return confirmVo;
}
稍微修改gulimall-order的“OrderServiceImpl”类里面的confirmOrder()方法
总说:
前台给gulimall-ware发过来/fare
请求(http://gulimall.com/api/ware/wareinfo/fare
),携带的参数是addrId(收货地址id),后台使用OpenFeign远程调用gulimall-member根据addrId(收货地址id)查询到addrInfo(收货地址信息)封装到MemberAddressVo里面返回,然后我们根据MemberAddressVo里面的电话的最后一位模拟计算运费
学习视频:接口幂等性讨论
何为幂等性?同一份订单提交一次和提交100次结果是一样的,用户不能因为网络不好提交了多词就下单多次。
1.什么是幂等性
接口幂等性就是用户对同一操作发起的一次请求和多次请求结果是一致的,不会因为多次点击而产生了副作用。比如支付场景,用户购买了商品,支付扣款成功,但是返回结果的时候出现了网络异常,此时钱已经扣了,用户再次点击按钮,此时就会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。。。这就没有保证接口幂等性
2.哪些情况需要考虑幂等性?
3.什么情况不用考虑幂等性?
4.幂等性的解决方法
每访问http://order.gulimall.com/toTrade
后台就会随机生成一个uuid(模拟token),然后存到redis中,也存到OrderConfirmVo中交给前端页面,在这个订单确认页面用户填好收货地址、支付方式以后,点击提交订单就把token带过去,后台比较前端带来的token和redis中的是否一致。假如用户点击多次“提交订单”,他每次带来的token都一样(因为页面并没有刷新),只有第一次能和redis匹配成功,其他的几次就是失败的,就能保证幂等性。
令牌机制的危险性:
前端发来“提交订单”的请求,我们要先根据用户id从redis中拿到token,然后比对前端的token和redis中的token,然后删掉redis中的令牌。
用户手速很快连点几次“提交订单”,第一次请求来了,后台根据用户id从redis中拿到token,和前台传进来的token进行比对,比对完了,还没有来得及删掉令牌,结果第二次带着token进来了又和redis匹配成功了,此时即使你删掉令牌也已经有两个业务进来了。
所以:从redis中拿token、比对token、删除token,一定要是原子性操做。
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get();
System.out.println("主线程..."+Thread.currentThread().getId());
//获取之前的请求
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//异步任务编排
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
//1、远程查询所有的收货地址列表
System.out.println("member线程..."+Thread.currentThread().getId());
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVO.getId());
confirmVo.setAddress(address);
}, executor);
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
//2、远程查询购物车所有选中的购物项
System.out.println("cart线程..."+Thread.currentThread().getId());
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
//feign在远程调用之前要构造请求,调用很多拦截器RequestInterceptor interceptor: requestInterceptors
}, executor).thenRunAsync(()->{
//查询库存信息
List<OrderItemVo> items = confirmVo.getItems();
List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
R hasStock = wmsFeignService.getSkusHasStock(collect);
List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() {
});
if (data != null){
Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(map);
}
},executor);
//3、查询用户积分
Integer integration = memberResponseVO.getIntegration();
confirmVo.setIntegration(integration);
//4、其他数据自动计算
//5、TODO 防重令牌
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX+memberResponseVO.getId(),token,30, TimeUnit.MINUTES);
confirmVo.setOrderToken(token);
CompletableFuture.allOf(getAddressFuture,cartFuture).get();
return confirmVo;
}
在购物车页选择要结算的商品,然后来到了订单确认页;
在订单确认页选择收货人地址信息、支付方式、显示商品、显示运费等等,然后点击“提交订单”后,就携带着数据来到了提交订单页;
提交订单页就是要锁定库存进行提交订单了,确认提交订单就会来到支付页;
支付页进行支付。
当你在订单确认页点击了"提交订单",前台带着OrderSubmitVo发送请求/submitOrder方法给后台,后台submitOrder里面到底干了什么呢?
submitOrder()
的解说public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo)
干了什么:
第一步,比较前台传过来的令牌和redis中的令牌是否一致,如果一致就继续往下执行
第二步,创建订单(创建订单时会再次从购物车里面查询用户下单的商品再次计算总价),和前端传来的价格进行比对,如果一致就继续往下执行
第三步,保存订单到数据库
第四步,锁定库存(如果用户买10件衬衣,但是没有一个仓库剩余衬衣多于10件,那就会锁定库存失败,远程调用的锁定库存方法就会回滚)
第五步,远程扣减积分
p275—p282只是“提交订单页”的初步代码,因为该服务调用了很多其他微服务,其中一个服务失败会自己回滚事务但如何保证其它服务也回滚事务呢?
p283—p289是使用分布式事务解决这个问题,一开始引入了seata,但发现seata可以解决事务但解决不了高并发的难题,
所以p290—p299就是使用“可靠消息+最终一致性方案”来解决这个问题(RabbitMQ+事务来解决)。
①public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo)
干了什么:
第一步,比较前台传过来的令牌和redis中的令牌是否一致,如果一致就继续往下执行
第二步,创建订单(创建订单时会再次从购物车里面查询用户下单的商品再次计算总价),和前端传来的价格进行比对,如果一致就继续往下执行
第三步,保存订单
第四步,锁定库存,远程调用gulimall-ware锁库存,锁库存是在gulimall-ware里面执行的,但它会返回状态码,根据状态码就可以找到远程锁库存成功了没有
第五步,远程扣减积分(这个功能还没有实现,但我们假装它已经实现了)
②存在的分布式事务问题:
③我们用到的@Transactional都是本地事务,本地事务只能控制住自己的回滚,控制不了其它服务的回滚,在分布式事务下不使用。
概念的讲解,讲了事务的基本性质
、事务的隔离级别
、事务的传播行为
、在springboot中本地事务时效问题
原子性:下订单、减库存、扣积分这三个要么同时成功要么同时失败,这就是原子性。原子性就是一系列操做不可拆分,同成同败。
一致性:A给B转200,转完帐必须保证A和B的总额没有变,不能一方扣减另一方没有增加
隔离性:事物之间互相隔离,100个人同时下单,一个人下单失败,不能影响其他人。
持久性:一旦事务成功,数据就一定要落盘在数据库里面。
事务的四大特征:
1. 原子性:是不可分割的最小操作单位,要么同时成功,要么同时失败。
2. 持久性:当事务提交或回滚后,数据库会持久化的保存数据。
3. 隔离性:多个事务之间。相互独立。
4. 一致性:事务操作前后,数据总量不变
* 概念:多个事务之间隔离的,相互独立的。但是如果多个事务操作同一批数据,则会引发一些问题,设置不同的隔离级别就可以解决这些问题。
* 存在问题:
1. 脏读:一个事务,读取到另一个事务中没有提交的数据
2. 不可重复读(虚读):在同一个事务中,两次读取到的数据不一样。
3. 幻读:一个事务操作(DML)数据表中所有记录,另一个事务添加了一条数据,则第一个事务查询不到自己的修改。
* 隔离级别:
1. read uncommitted:读未提交
* 产生的问题:脏读、不可重复读、幻读
2. read committed:读已提交 (Oracle)
* 产生的问题:不可重复读、幻读
3. repeatable read:可重复读 (MySQL默认)
* 产生的问题:幻读
4. serializable:串行化
* 可以解决所有的问题
* 注意:隔离级别从小到大安全性越来越高,但是效率越来越低
① a、b、c三个方法都标注了@Transactional说明它们三个都是事务,b事务隔离级别是REQUIRED那么它共用a的事务,c事务隔离级别是REQUIRES_NEW那么它自己用自己的事务,现在a方法调用b、c方法且执行时出现除0异常,那么a和b会回滚,c不会回滚。
@Transactional
public void a(){
b();
c();
int i = 10/0;
}
@Transactional(propagation=Propagation.REQUIRED)
public void b(){
}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void c(){
}
② a事务设置过期时间是30s,b事务设置过期时间是3s,由于b共用a的事务,所以b的实际过期时间也是30s
springboot中使用事务存在的坑:
查看 视频 的10:42以后
讲了CAP的概念
、Raft的领导选举、日志复制
概念这种东西,看视频不比看文字舒服?不必看文字理解的更深入?,所以回看视频即可
强一致
、弱一致
、最终一致
的概念
2PC模式
TCC事务补偿方案
最大努力通知型方案(重点)
可靠消息+最终一致性方案(重点)
1.相关概念的解释
2.环境准备
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
添加“com.atguigu.gulimall.order.config.MySeataConfig”类,代码如下:
@Configuration
public class MySeataConfig {
@Autowired
DataSourceProperties dataSourceProperties;
@Bean
public DataSource dataSource(DataSourceProperties dataSourceProperties){
//得到数据源
HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
if (StringUtils.hasText(dataSourceProperties.getName())){
dataSource.setPoolName(dataSourceProperties.getName());
}
return new DataSourceProxy(dataSource);
}
}
修改“com.atguigu.gulimall.ware.config.WareMybatisConfig”类,代码如下:
@Bean
public DataSource dataSource(DataSourceProperties dataSourceProperties){
//得到数据源
HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
if (StringUtils.hasText(dataSourceProperties.getName())){
dataSource.setPoolName(dataSourceProperties.getName());
}
return new DataSourceProxy(dataSource);
}
分别给gulimall-order和gulimall-ware加上file.conf和registry.conf这两个配置,并修改file.conf
给分布式大事务的路口标注@GlobalTransactional; 每一个远程的小事务用 @Transactional
OK了,这样下面的这两个分布式事务问题就解决了。
但是,我们这个场景还是不适合使用seata,我们下单是典型的高并发模式,seata中后台使用了很多锁,导致高并发串行化(一个一个执行),高并发不使用2PC
和TCC模式
,我们使用的就是最大努力通知型方案
或可靠消息+最终一致性方案
,接下来我们使用了可靠消息+最终一致性方案
p275—p282只是“提交订单页”的初步代码,因为该服务调用了很多其他微服务,其中一个服务失败会自己回滚事务但如何保证其它服务也回滚事务呢?
p283—p289是使用分布式事务解决这个问题,一开始引入了seata,但发现seata可以解决事务但解决不了高并发的难题,
所以p290—p299就是使用“可靠消息+最终一致性方案”来解决这个问题(RabbitMQ+事务来解决)。
p290就是比对了2PC
和TCC模式
,还有最大努力通知型方案
或可靠消息+最终一致性方案
,最后得出我们这个“订单确认页”要使用可靠消息+最终一致性方案
,所以就开启了下一节RabbitMQ的讲解
这一节是TTL、死信队列、延时队列的概念讲解,建议回看RabbitMQ—高级部分
p是订单服务,一下单成功就给RabbitMQ发消息,经过延时队列60000ms以后到达订单的释放服务
图片看不懂就回看视频,视频里面讲的相当清楚。
1.创建三个实体类
"wms_ware_order_task"这张表就是用来记录哪一天给哪个订单执行了锁库存操做。
"wms_ware_order_task_detail"表就是记录哪个订单中的哪个商品在哪个仓库中锁定了多少库存。
这两张表是干什么的?我们要根据订单号查询到"wms_ware_order_task"表的task_id
,然后拿着task_id
去查"wms_ware_order_task_detail"表就可以得到这个订单下所有商品的库存锁定详情
。
①添加“WareOrderTaskEntity”,这个实体类和数据库的"wms_ware_order_task"表绑定,
这个表字段很多,但是只需要关注两个字段——“orderSn”和“createTime”,哪一天给哪个订单执行了锁库存操做.
这张表就是用来记录哪一天给哪个订单执行了锁库存操做。
②添加“WareOrderTaskDetailEntity”类,这个类和数据库的"wms_ware_order_task_detail"表绑定。
这个表就是记录哪个订单中的哪个商品在哪个仓库中锁定了多少库存
③添加StockLockedTo实体类,这个实体类相当于上面"wms_ware_order_task"表和"wms_ware_order_task_detail"表的结合
2.锁定库存的方法
修改“com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl”类,
代码的逻辑是:
先说明“什么是锁定库存"?——假如顾客购买的商品之一是10件马甲,如果遍历了所有拥有马甲的仓库都没有找到一个剩余马甲数量大于10的仓库,那么这就是锁定库存失败;如果找到一个剩余马甲数量大于10的仓库,那么这就是锁定库存成功。
①submitOrder()方法里面锁定库存代码执行成功后结果后面的代码执行失败,submitOrder()方法就会回滚,锁定库存已经执行了没办法回滚,所以我们需要库存工作单表"wms_ware_order_task"
记录哪一天给哪个订单执行了锁库存操做,从而做到让库存回滚。
②由于一个订单下有多个商品,我们给当前订单下的所有商品一个一个遍历进行锁定库存操做,当前商品锁定成功,就立刻做两件事:①给表"wms_ware_order_task_detail"
保存工作单详情(记录哪个订单中的哪个商品在哪个仓库中锁定了多少库存)②将当前商品锁定了几件的工作单记录发送给MQ;
③某一件商品的锁定库存失败,那么抛出库存不足的异常,有异常代码就回滚,前面所有商品在数据库的"wms_ware_order_task"和"wms_ware_order_task_detail"这两张表中插入的数据就都被回滚了,但问题是前面商品的MQ消息发出去了,没关系,让MQ到数据库中用id查就可以,由于回滚了肯定查不到相关记录,只要查不到id就不用解锁
3.测试一下
1.库存解锁的场景:
1)、下订单成功,库存锁定成功,submitOrder()整个方法都执行无误,但由于订单过期没有支付被系统自动取消、或被用户手动取消。需要解锁库存。
2)、库存锁定成功,但submitOrder()方法中锁库存后面的代码执行失败,导致submitOrder()方法回滚。之前锁定的库存就要解锁。我们可以根据"wms_ware_order_task"和"wms_ware_order_task_detail"这两张表中记录进行解锁。
3)、库存锁定失败,导致ubmitOrder()方法回滚。库存锁定失败,那么锁定库存的代码会回滚,那么"wms_ware_order_task"和"wms_ware_order_task_detail"这两张表中插入的数据就都被回滚了,但问题是前面商品的MQ消息发出去了,没关系,让MQ到数据库中用id查就可以,由于回滚了肯定查不到相关记录,只要查不到id就不用解锁。
2.编写unlock()代码
unlock()代码的逻辑:
为保证幂等性,我们分别对订单的状态和工作单的状态都进行了判断,只有当订单过期且工作单显示当前库存处于锁定的状态时,才进行库存的解锁
4.解锁库存unLockStock()方法
wms_ware_sku表有个字段是stock_locked,表示已经被锁定的库存;
锁定10件库存就是把wms_ware_sku的stock_locked这个字段增加10;
解锁10件库存就是把wms_ware_sku的stock_locked这个字段减小10。
5.调试
测试时出现异常,先调试
由于gulimall-order添加了拦截器,只要使用该服务必须登录才行。因为gulimall-ware需要远程调用订单,但不需要登录,所以给这个路径放行
修改gulimall-order的interceptor的LoginUserInterceptor.java
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberResponseVO> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//放行远程调用订单的url
String uri = request.getRequestURI();
boolean match = new AntPathMatcher().match("/order/order/status/**", uri);
if (match){
return true;
}
MemberResponseVO attribute = (MemberResponseVO) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute != null){
loginUser.set(attribute);
return true;
}else {
//没登录就去登录
request.getSession().setAttribute("msg","请先进行登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
上面的图看不懂就立刻回看视频p298,视频里面用两分钟讲的清清楚楚
①修改submitOrder()方法,当它执行完锁库存、查积分等所有操作后,就给发送消息给MQ表示订单创建成功,
②创建订单的消息会进入延迟队列,最终发送至队列order.release.order.queue,因此我们对该队列进行监听,进行订单的关闭
③定时关单
④解锁库存
* 思考:我们之前解锁库存前先看一下订单的状态,但是在这里就不用,因为订单状态肯定是"已取消"才会发消息到这里来解锁库存的。
* 那我们需要不需要查一下库存解锁状态?需要,因为万一订单系统发的解锁库存的消息晚了一步,已经被解锁了的库存就没必要再次解锁了。所以在解锁之前先确认lock_status=1再进行解锁(只有库存锁定状态是"已锁定"的才需要解锁)。
“可靠消息+最终一致性”,我们必须保证消息的可靠性。
1.使用try…catch语句
2.创建存放消息的数据库
3.使用ACK机制
一定要开启手动ACK模式。消息的发送者、接收者都要开启手动ack
有机会再学
有机会再学
1、导入依赖
2.添加配置类:
3.添加“com.atguigu.gulimall.order.vo.PayVo”类,代码如下:
@Data
public class PayVo {
private String out_trade_no; // 商户订单号 必填
private String subject; // 订单名称 必填
private String total_amount; // 付款金额 必填
private String body; // 商品描述 可空
}
4.yml配置:
#支付宝相关的配置
alipay.app_id=2021000117698328
alipay.merchant_private_key=MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCmTEEsQLUp7nUo3+1oGpa+hFzDhvM1wKHBUd0aar64IXWcG4diXGfRyGmlKJYWxv8v3+P3jU0SQNXVQORQ1lmdK8ft1/EgqOAfDv0LY7zsUB/DmlljyDtnzSBgNA0sT1ZfKmjKmd5u26m5FRIBpW2Uh8Bw8mONb0eLQxFjJyQu/Yg5uJbB4rtmA9WyrPSqLwqbdYLQrOrss00SZ5AZz0dCHHMpWOaMuVOrx4b8IVoO4ShsfRGYQJ9zxV/xCJ3DgtC0cV7G5/ed5XQzJvaZrG0O7v5AVxoVvfXI7TUoZ8VMgYTqOEN7Wkv2Y+WeQbI9OkUkk/T7kp+NVrsNNi+RNTvjAgMBAAECggEAUUR0mQaqQeqZcLc10qkjv8j5eEgLtNoFcm7qKU2/FEatrfM6DxRvW/Kfxil2Z30qGiBEzKZN4ryygvuqV+LYell5476ixL4igKsXeChum+FwFGvqgTvJ5Ck3SCxHv76py+nyugfFztEkOSGV4h4Q1gQdRFT/149pHCJTbewj354WzRjoogmCMNuSNeKq69TY2OMnXv9kyWKD5hURPIjw8hoqoR4EzGiealmJgxXbf55+BEkWlJ7t2UYgO0wCSSKsBLC6DR8Z3YM2S1cmr7TyVE1e2eTp+o1+N2Oc2OoEWR855CKy3Y+xlNQs+CzjHT4oms8Cw2/2i6DzO2ic1JnI8QKBgQDXDMcQLRoEQAWSpa54Jd5ZyHe/x1/t1GBhuad/EeMO60G+0Jr47lfIYQyB7RnfSeSxQFD33keqiMbYWi7bR8+rpJy+AFshgACK5HP3SkV+0/5VLmBTfLoMJ3HoYq5GN4ywW1Vnsr6ctBYLXngbowgBe6g00N8vbpWkuWPoZ55VSwKBgQDF9utbaPjNECAlEHz1DRj/DF3pIYe5jcQyjccdMR5ZlTeAFaAXDWNWBGHdNRUIDfDSf00hM9VmIf79PRWd1T2WNBcLyl6kusLen79MeF7VSALuiZW9GRZjCpIBwF7FV2prQpX4rVrLUbN477xz+4UhsFNi+lPzX2ZVOTaNLHZMyQKBgQC5f4Ead/0QG3VzKM1VQD0LLzv0RnN+AArfYTiVCIXWcaH1iZWUEmvQIb6bOD1v+Rp2tubg2HDzLiZvq2LtrYT6JvU5g68YN4TASg2qCvvlSdICAg3/FgCZyVCdRrnTQcluumnyGCIJo+G8DtIF7NxUAyl13ZIXJQmZ3HzMlMzj/wKBgCX4/h5TnV3gWPojFoT+1SufGKhuWRV7nwW/clEkKdkvKS01eLbTR5mpT4hZ9UXNPsNxzb6vraBgpwO2Yt4amCymo0EMuWjJtjVz2QL3F+G7ZWySEZnrJQMsdONHHiamZPBcHl5MCl1zt4RcH/7zYQ8cPnJ+5/mH9B4m0lL0E2EZAoGAS9KtQxNezmhHAo0fF9K78EykF14a2jlXIfBlIxfAUr8ie18mwsdjfhxXBwkn+QyZi8W0S7kPYsZUY4au9ZpQPyAeUfHPbHjYId93Adob0bFBXbXzJqUXq2Jp1+mAHeAKUFJk1htdseHHCn2mzS37JIIJdEHEydA/ALDmB1BASz0=
alipay.alipay_public_key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8O6UgsH9DNS0cSaLScqCuacJyySGOg0XnkaPL2vxaeBhF8BHKfV3Q7uhaMbQyh0mFdHsnZr7iopMf/pNVqOrTVn1EZ5HvrgwsDSUoXP8qCC+9i53BlrD00gsxKT+Y19vlk/Hrx+H354ftRDETuU7MMShvDPVY3iodj0I6bjN1FffprhCkH8tADc/ixOhsnVP8LhblhDabgYiAd5jb1i3cWI8q+wjZXVopi9L2xEPxafY/l0B+2Muee4u7zF9TDq3o3XnSpnnUDMa7EIEUIZ4BQZnP3LPUz3dqFiw1haMxxGtyZXiXHykvAZrxr1ThLETlV73cT2Q4Ub+9xrj4ZfqTwIDAQAB
#alipay.notify_url=http://tzy.nat300.top/payed/notify
alipay.notify_url=http://member.gulimall.com/memberOrder.html
alipay.return_url=http://member.gulimall.com/memberOrder.html
alipay.sign_type=RSA2
alipay.charset=utf-8
alipay.gatewayUrl=https://openapi.alipaydev.com/gateway.do
添加“com.atguigu.gulimall.order.web.PayWebController”类,代码如下:
@Controller
public class PayWebController {
@Autowired
AlipayTemplate alipayTemplate;
@Autowired
OrderService orderService;
@ResponseBody
@GetMapping(value = "payOrder", produces = "text/html")
public String payOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {
PayVo payVo = orderService.getOrderPay(orderSn);
String pay = alipayTemplate.pay(payVo);//返回的pay是一个html页面。将此页面直接交给浏览器就行
return pay;
}
}
修改“com.atguigu.gulimall.order.service.OrderService”类,代码如下:
/**
* 获取当前订单地支付信息
*/
PayVo getOrderPay(String orderSn);
修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”类,代码如下:
@Override
public PayVo getOrderPay(String orderSn) {
PayVo payVo = new PayVo();
OrderEntity order = this.getOrderByOrderSn(orderSn);
BigDecimal bigDecimal = order.getPayAmount().setScale(2, BigDecimal.ROUND_UP);//支付金额设置为两位小数,否则会报错
payVo.setTotal_amount(bigDecimal.toString());
payVo.setOut_trade_no(order.getOrderSn());
List<OrderItemEntity> order_sn = orderItemService.list(new QueryWrapper<OrderItemEntity>().eq("order_sn", orderSn));
OrderItemEntity entity = order_sn.get(0);
payVo.setSubject(entity.getSkuName());//订单名称
payVo.setBody(entity.getSkuAttrsVals());//商品描述
return payVo;
}
yml配置中alipay.return_url=http://member.gulimall.com/memberOrder.html
就是配置了支付成功以后,我们要跳到用户的订单列表页,所以现在我们需要整合gulimall-member的用户订单列表页
动静分离、整合登陆拦截器、springsession数据共享
前台带着当前页码“pageNum”发送请求/memberOrder.html给gulimall-member,后台使用OpenFeign远程调用gulimall-order,在gulimall-order中从登陆拦截器中获取当前用户id,根据用户id查询"oms_order"表拿到OrderEntity,然后从OrderEntity中拿到订单号查询"oms_order_item"表拿到OrderItemEntity,把OrderItemEntity设置到OrderEntity中,然后采用分页工具返回OrderEntity就OK。
①OrderEntity里面有相当多的属性:
订单号
会员id
和会员名
(从LoginUserInterceptor中获取到)运费
还有 收货人姓名、电话、省份、地区、街道等等
,这些属性一般都是调用gulimall-ware根据addr_id远程查询订单状态、确认时长、确认状态
②OrderItemEntity里面有超多属性:
想要修改订单状态,有两种方式:
①url上面有订单号,你可以根据订单号修改订单状态,问题是不安全,别人随便发个这种请求带上订单号你就把订单状态改了?
②当订单完成后,其实支付宝官方会把订单相关的所有信息都以RabbitMQ发给你,而且只要你不签收它就不断地给你发。
本节课干了两件事,第一件事是配置内网穿透,这个流程看看就行了,这么做是保证你的项目可以被外网访问到,然后有用户完成订单,支付宝就会按照你的外网地址给你发送RabbitMQ消息
第二件事就重要了,你收到支付宝RabbitMQ消息(封装支付宝那边发来的消息到PayAsyncVo里),第一步先验证这个消息是支付宝发来的,“验证签名”操做的代码是官方给的;验证完签名然后就是获取支付宝发送来的消息然后提取重要的信息到“交易流水”表里面,另外更新mos_order表的“订单状态”
1.总说
2.查询关联的商品
前台传来(分页参数page、limit)和 (场次id)
后台先判断(场次id)是否为空,如果不为空就根据场次id查询sms_seckill_sku_relation表,
然后把查询结果List
分页返回
用了cron表达式
为了预告用户两天后什么东西要参与秒杀,我们将要秒杀的商品的相关信息提前三天从数据库上架到缓存中,而且这个定时任务是每天晚上凌晨3点执行(趁不是高峰期时间)。
三张表:
五个实体类:
“秒杀场次和商品的关联表”"sms_seckill_sku_relation
。“秒杀场次表”sms_seckill_session
对应,但它有一个数据库中不存在的字段(List
存放着SeckillSkuRelationEntity)List
存放着SeckiSkuVo“商品详情信息表”pms_sku_info
SeckillSkuVo
+SkuInfoVo
+开始时间
+结束时间
+随机码
,也就是说它记录了“秒杀场次和商品的关联表”"sms_seckill_sku_relation
+“商品详情信息表”pms_sku_info
+。开启一个定时任务,将要秒杀的商品的相关信息提前三天从数据库上架到缓存中:
List
,然后三天的所有活动封装到List
里面返回。1.定时任务:
@Slf4j
@Service
public class SeckillSkuScheduled {
@Autowired
SeckillService seckillService;
//定时任务
@Scheduled(cron = "0 0 3 * * ?")
public void uploadSeckillSkuLatest3Days(){
//重复上架无需处理
log.info("上架秒杀的信息......");
seckillService.uploadSeckillSkuLatest3Days();
}
}
2.service:
@Service
public class SeckillServiceImpl implements SeckillService {
......
private final String SESSIONS_CACHE_PREFIX = "seckill:sessions:";
private final String SKUKILL_CACHE_PREFIX = "seckill:skus:";
private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";//+商品随机码
@Override
public void uploadSeckillSkuLatest3Days() {
// 远程获取最近三天需要参与秒杀的活动
R session = couponFeignService.getLasts3DaySession();
if (session.getCode() == 0){
// 上架商品
List<SeckillSessionWithSkus> data = session.getData(new TypeReference<List<SeckillSessionWithSkus>>() {
});
// 缓存到redis
// 1、缓存活动信息
saveSessionInfos(data);
// 2、缓存获得关联商品信息
saveSessionSkuInfos(data);
}
}
}
3.远程获取最近三天需要参与秒杀的活动:
getLasts3DaySession()代码的逻辑就是:
gulimall-coupon会查询“秒杀场次表”获取到最近三天的秒杀活动,然后根据活动id查询“秒杀场次和商品的关联表”获取到每一场秒杀活动参与秒杀的所有商品。将每一场秒杀活动的所有商品封装到SeckillSessionEntity的List
,然后三天的所有活动封装到List
里面返回。所以最后返回的就是List
4.在redis中保存秒杀商品信息
①分布式系统下,三台机器可能同时都执行这个定时任务,所以我们可以加一个分布式锁Redisson,谁拿到锁谁执行这个上架功能。
②因为我们把提前三天的秒杀活动存到redis中这一定时任务是每天凌晨3点都要执行一遍,所以今天凌晨三天往redis中存放数据时先看看昨天凌晨三点存过了没有
商品详情页当初我们写在gulimall-product里面的,前台发送请求给gulimall-product,然后gulimall-product携带skuid远程查询gulimall-seckill来获取当前商品有没有参与秒杀活动,查询方法是写一个正则表达式"\d_4"来匹配redis中的那些keys,匹配上以后根据key获得值,封装到SeckillSkuRedisTo里面进行传输,传输之前看一下秒杀活动开始了没有,如果没有开始就把SeckillSkuRedisTo里的随机码置为空(我们的随机码只有在秒杀活动开始了才暴露)。
p320是理论讲解了如何解决秒杀开始后的高并发问题,面试谈的就是这些
SpringSession的配置:
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("gulimall.com");
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
return new GenericJackson2JsonRedisSerializer();
}
}
用户登录拦截器的配置:
(我们不能拦截所有请求,除了秒杀请求“/kill”需要判断登录外,像“/currentSeckillSkus”这些都是远程服务被别人调用的,不应该被拦截)
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberResponseVO> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
AntPathMatcher matcher = new AntPathMatcher();
boolean match = matcher.match("/kill", requestURI);
// 如果是秒杀,需要判断是否登录,其他路径直接放行不需要判断
if (match) {
MemberResponseVO attribute = (MemberResponseVO) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute != null){
loginUser.set(attribute);
return true;
}else {
//没登录就去登录
request.getSession().setAttribute("msg","请先进行登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
return true;
}
}
1.总说
开始秒杀时前台携带着killId
、randomCode
、num
发送/kill
请求给后台(假如killid=6_4那么活动的场次是6、skuid是4,randomCode是随机码,num是用户要秒杀的商品数量),后台就进行校验——首先要求根据killid能找到用户要秒杀的商品,其次要求当前时间在秒杀时间范围内,再者要求前台传来的killId
和randomCode
(随机码)和后台redis中存储的能够匹配,最后要求秒杀的数量num
符合数量限制,以上的条件都满足就可以去redis中给该用户占位了(占位是为了防止重复,如果redis中已经帮改用户占过位就会出现占位失败,占位的key是“用户id_场次id_商品id”
),占位成功然后获取信号量,获取信号量成功就发送RabbitMQ消息给gulimall-order表示要给该用户下单啦。
gulimall-order一直监听着那边发来的消息,收到消息后就保存订单信息
然后保存订单项信息
(本来还应该保存收货地址、锁定库存什么的,但是我们没有做这些操做)
2.运行结果
这一节主要是前台代码,略微修改了后台代码
运行结果
(概念)①对秒杀服务的总结;②Sentinel熔断、降级;③Hystrix和Sentinel的对比
视频不用看,直接看笔记SpringCloudAlibaba—Sentinel
这块视频就不用看了,因为他就是简简单单地演示了下sentinel的流控,还是直接看周阳老师的笔记SpringCloudAlibaba—Sentinel
这块对应的别人的笔记也不用看了,直接看自己的就行
整合sentinel的操做:
1)、导入依赖 spring-cloud-starter-alibaba-sentinel
2)、启动Sentinel的控制台
3)、yml中配置sentinel控制台地址信息
所有微服务都进行这些操做,这样sentinel才能监控所有的微服务
1)、导入依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
2)、启动Sentinel的控制台
https://github.com/alibaba/Sentinel去官网下载该版本的控制台
启动Sentinel的方式:
java -jar sentinel-dashboard-1.7.1.jar
就足够了,它默认使用8080端口,java -jar sentinel-dashboard-1.7.1.jar --server.port=8333
,既然你把控制台的端口改为8333了那么你在yml里面配置的时候也要记得配置为83333)、yml中配置
#Sentinel控制台地址(如果你把控制台的端口改为8333那么这里就是8333)
spring.cloud.sentinel.transport.dashboard=localhost:8080
#Sentinel传输端口
spring.cloud.sentinel.transport.port=8719
#设置暴露的endpoint
management.endpoints.web.exposure.include=*
这块雷丰阳老师讲的很乱,建议直接看周阳老师的笔记SpringCloudAlibaba—Sentinel
服务熔断和降级请看文章:SpringCloud—Hystrix
浏览器直接访问/test那么我们可以对/test做个流控(限制每秒请求不能超过多少啦等等),
但是浏览器直接访问gulimall-product然后product远程调用gulimall-seckill的/test就需要用熔断降级
服务熔断最常见的就是gulimall-product调用gulimall-seckill,结果gulimall-seckill宕机,以前不使用sentinel的熔断的时候直接调用会报错;现在使用sentinel的熔断保护机制,远程调用失败后gulimall-product调用自己的熔断回调方法。
1)、调用方的熔断保护开启 feign.sentinel.enabled=true
2)、调用方手动指定远程服务的降级策略。
3)、远程服务被降级处理后,触发我们的熔断回调方法
熔断的流程就是:先给对方服务进行降级,我们自己调用熔断回调方法,正常以后恢复链路(降级—>熔断—>恢复)
①在调用方(gulimall-product)的yml配置文件添加配置
#默认情况下,sentinel是不会对feign进行监控的,需要开启配置
feign.sentinel.enabled=true
②修改调用方(gulimall-product)的远程调用类
修改“com.atguigu.gulimall.product.feign.SeckillFeignService”类,代码如下:
@FeignClient(value = "gulimall-seckill",fallback = SeckillFeignServiceFallBack.class)
public interface SeckillFeignService {
@GetMapping("/sku/seckill/{skuId}")
R getSkuSecKillInfo(@PathVariable("skuId") Long skuId);
}
③我们会在调用方(gulimall-product)实现远程调用类,自定义返回信息给页面。
添加“com.atguigu.gulimall.product.fallback.SeckillFeignServiceFallBack”类(注意我们修改的是gulimall-product),代码如下:
//在调用方gulimall-product实现远程调用类,自定义返回信息给页面。
@Slf4j
@Component
public class SeckillFeignServiceFallBack implements SeckillFeignService {
@Override
public R getSkuSecKillInfo(Long skuId) {
log.info("熔断方法调用.....getSkuSecKillInfo");
return R.error(BizCodeEnume.TOO_MANY_REQUESTS_EXCEPTION.getCode(),BizCodeEnume.TOO_MANY_REQUESTS_EXCEPTION.getMsg());
}
}
我们一般都是在调用方gulimall-product指定降级策略,因为一旦提供方被降级了,调用方就不远程调用了,直接走熔断回调方法;
而如果在提供方gulimall-seckill指定降级策略,即使提供方被降级了,调用方还是会调用gulimall-seckill。
那么什么时候在提供方指定降级策略呢?
别人都来调用我,我忙不过来了,我必须牺牲一部分被调用的方法,我给自己的某些方法指定一个降级策略,当我这个方法被别人高并发访问时我这个方法就被降级了,我就不运行我这个方法里面的业务逻辑了,而是直接返回默认的降级数据。
1.指定降级策略
2.自定义默认的降级数据
在服务提供方gulimall-seckill添加一个配置类com.atguigu.gulimall.seckill.config.SeckillSentinelConfig,让这个配置类自定义返回降级数据
/**
* Sentinel-自定义流控响应的配置类,2.2.0以后的版本实现的是BlockExceptionHandler;以前的版本实现的是WebCallbackManager
*/
@Configuration
public class SeckillSentinelConfig implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
R error = R.error(BizCodeEnume.TOO_MANY_REQUESTS_EXCEPTION.getCode(), BizCodeEnume.TOO_MANY_REQUESTS_EXCEPTION.getMsg());
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json");
httpServletResponse.getWriter().write(JSON.toJSONString(error));
}
}
上面的代码中R error = R.error(BizCodeEnume.TOO_MANY_REQUESTS_EXCEPTION.getCode(),BizCodeEnume.TOO_MANY_REQUESTS_EXCEPTION.getMsg());
那么长你肯定没有认真看,其实他就是R error = R.error( code , message)
的格式而已
3.测试
讲解了@SentinelResource注解的使用,别看视频了,也别看别人的笔记了,这块雷丰阳老师讲的很肤浅,建议直接看周阳老师的笔记SpringCloudAlibaba—Sentinel
如果能在网关层就进行流控,可以避免请求流入业务,减小服务压力
gulimall-gateway引入依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-sentinel-gatewayartifactId>
<version>2.2.0.RELEASEversion>
dependency>
注意引入的依赖要和gulimall-common的pom里的阿里巴巴版本一致
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.2.0.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
具体内容回看视频p333和p334
1.所有微服务导入依赖(导入zipkin就把sleuth连同导入了)
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zipkinartifactId>
dependency>
2.所有的微服务yml进行配置
spring:
zipkin:
base-url: http://192.168.56.106:9411
sender:
type: web
# 取消nacos对zipkin的服务发现
discovery-client-enabled: false
#采样取值介于 0到1之间,1则表示全部收集
sleuth:
sampler:
probability: 1
3.虚拟机运行zipkin
docker run -d -p 9411:9411 openzipkin/zipkin
我非常建议你去看这块的视频p336—p337
可以结合周阳老师的课程笔记:SpringCloud—Sleuth
1.启动虚拟机
2.启动Nacos
3.启动seata
4.启动sentinel
原则上需要启动sentinel,但是由于它占用了8080端口,而且所有微服务给它配置的也是8080端口,懒得改了;8080端口和我们的renren-fast是冲突的,所以不启动它也无妨。
5.启动所有微服务
6.启动前台renren-fast-vue
7.操作流程