谷粒商城分布式基础篇
谷粒商城分布式高级篇(上)
谷粒商城分布式高级篇(中)
谷粒商城分布式高级篇(下)
商城业务
订单服务
页面环境搭建
- 上传静态资源文件(等待付款、订单页、结算页、收银页)至order目录detail、list、confirm、pay目录下
- nginx站点配置文件 加入
order.gulimall.com
域名 - 网关配置
- 加入thymeleaf依赖并禁用他的缓存
- 注册进nacos
整合SpringSession
- 引入依赖 加入配置
spring.session.store-type=redis
,导入session配置类,导入线程池配置类 - 引入redis依赖和配置
- 启动开启reids session缓存
@EnableRedisHttpSession
订单基本概念
订单登录拦截
购物车列表点击去结算会跳转至Order模块,Order模块都需要加上拦截功能判断登录
- 创建一个拦截器,拦截器实现的是SringMvc的
HandlerInterceptor
重写 拦截前置方法preHandle
添加具体拦截逻辑
- 将拦截器注册在配置类中,并设置拦截路径为全路径
订单确认页模型抽取
订单确认页要用到的数据
订单确认页数据获取
各个信息的获取拼接
远程调用会员接口,查询地址列表,创建@FeignClient("gulimall-member")
接口,启动类启用远程注解,被调用的服务写好业务
Feign远程调用丢失请求头问题源码分析
问题:登录状态后 远程调用
购物车服务 发现购物车服务 session
中的 member
为空,而单独访问购物车服务 则不为空
原因:在业务方法 cartFeignService.getCurrentUserCartItems();
是 feign 远程调用,区别于页面直接访问购物车,页面放请求时 会携带 cookie
源码分析 feign 的远程调用
- 断点至购物车的远程调用,service 是 feignclien的代理对象
- stepinto进入方法实现,首先判断是否是object的公共方法,明显不是,而后获取当前方法再invoke,最后一段再 setpinto
- invoke内,因为远程调用的方法没有传参所以参数为空,真正的执行在
executeAndDecode
方法内,stepinto
- 传入的是上方法第一步创建的请求模板,模板内 GET方法 和 url地址 得到当前请求
Request
,然后用客户端去执行response = client.execute(request, options);
,先去看看怎么得到request
5. request 的返回,发现会遍历所有RequestInterceptor
拿所有的 request拦截器来 apply 增强,所以feign在远程调用之前要构造请求,调用很多的拦截器来增强。因为现在还没有配置拦截器,feign也没有进行增强,所以将原生默认的RequestTemplate
返回
默认的 request 没有携带请求头,而这个请求的总发起/toTrade
方法,是携带了 header 和 cookie信息的,但是feign在进行远程调用的时候 创建了一个新的请求头且没有任何信息。总结这就是feign的远程调用问题
- 解决请求头丢失:加上feign远程调用的请求拦截器,feign的实现会遍历所有拦截器
创建 一个feign的配置类,配置类中包含了拦 截器,通过,拦截器实现的方法apply 就是具体的增强,增强的目的就是加上 为feign的远程调用加上 这次请求发起的请求头。获取请求头:Spring
提供的RequestContextHolder
上下文环境保持器类得到当前请求的所有属性,将得到的请求属性放入feign的请求头信息
Feign异步调用丢失请求头问题
将两次远程调用做成异步查询
发现拦截器获取老请求为空 空指针异常,未能获取到当前请求的上下文信息
启用异步线程来完成远程调用会在不同线程中执行,打印一下线程号
发现确实是在不同的线程中,因为 RequestContextHolder
中 RequestAttributes
的存储用的ThreadLocal
,所以只要线程不一样,RequestAttributes
就不一样
解决 RequestAttributes
不一样的办法:同步主线程的 RequestAttributes
订单确认页渲染
订单确认页库存查询
在购物车查询完每个商品以后,继续.thenRunAsync
异步来查询所有商品的库存信息,拿到购物车所有商品的id集合,远程调用库存服务
在OrderConfirmVo
类中加入库存 Map<Long,Boolean> stocks;
属性 商品id为键值
订单确认页模拟运费效果
点击选择地址后发送http://gulimall.com/api/ware/wareinfo/fare?addrId=?
请求计算运费,库存服务远程调用member服务计算并返回运费
订单确认页细节显示
接口幂等性讨论
可以给订单号做唯一约束,保证幂等性
订单确认页完成
订单确认页生成一个token,以便在提交订单时验证
原子验令牌
主要操作为传统的java内对比替换为 redis的lua脚本操作
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
String redisToken = redisTemplate.opsForValue().get(key);
if (orderToken !=null && orderToken.equals(redisToken)){
//令牌验证通过
原子验证令牌和删除令牌
//0失败 - 1成功 | 不存在0 存在 删除?1:0
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
String orderToken = vo.getOrderToken();
//原子验证令牌 和 删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
Arrays.asList(key),
orderToken);
构造订单数据
构造订单项数据
订单验价
各种繁琐的类的装配,没意思
保存订单数据
锁定库存
提交订单的问题
问题记录:获取远程查询记录后,用fastJson进行逆转时字段为null,原因为json待转字段名必须与被转类字段名保存一致
分布式事务
本地事务在分布式下的问题
- 假失败,远程调用的业务成功,但是因为网络原因没有返回成功信息,本地事务回滚,造成了数据不一致
- 本地事务出现异常,而先前远程调用已经成功,无法对远程调用进行事务回滚
@Transactional
本地事务,在分布式系统,只能控制自己的回滚,控制不了其他服务的回滚
分布式事务:最大原因,网络问题+分布式机器
本地事务隔离级别&传播行为等复习
事务传播举例
本地事务失效问题,本类方法调用
spring-boot
中的事务的坑,若a、b、c
都在一个service
文件中,a
中调用b、c
方法,b、c
方法的事务设置不会起任何作用,原因是spring
的事务是使用事务来控制的。所以同一个对象内事务方法互调事务设置失效,原因是绕过了代理对象
解决:使用代理对象调用事务方法
1)、引入aop-starter; spring-boot-starter-aop
引入aspectj
2)、启动类@EnableAspectjAutoProxy(exposeProxy = true)
开启 aspectj
动态代理功能 以后所有动态代理都是aspectj
3) 、本类互调用代理对象
分布式CAP&Raft原理
为什么会有分布式事务
分布式系统中实现一致性的raft算法实现动画演示 官方演示
BASE
分布式事务常见解决方案
Seata&环境准备
Seata
Seata控制分布式事务
1、每一个微服务必须先创建undo_log
表
2、安装事务协调器 seata-server
Seata分布式事务体验
1、在common中导入seata依赖,注意seata-all版本是多少就启动对应的seata-server
2、解压seata-server 并配置conf目录下registry.conf
文件,配置为nacos和nacos地址,默认的配置在file.conf
中
3、开启全局事务 方法上加入@GlobalTransactional
注解,注意要在这之前注入seata的代理数据源 DataSourceProxy
,因为seata想要控制住事务,得让默认的数据源给 seata 包装后让seata代理数据源,seata才能控制事务。所以,所有想要用到分布式事务的微服务得使用seata DataSourceProxy
代理自的数据源
spring 默认数据源是在 DataSourceAutoConfiguration
中自动配置
其中会导入各种数据源,而spring的默认源数据是 Hikari
在配置Hikari数据源是在容器中添加@Bean
组件,组件就是HikariDataSource
,所有的配置 通过DataSourceProperties
和 spring.datasource.hikari
绑定
而值得注意的是 注解@ConditionalOnMissingBean(DataSource.class)
意思是在容器中没有自定义的数据源的时候才会导入下面的 Hikari 数据源,所以在我配置自己的数据源后,默认的数据源就会失效
seata的官方也提醒了应该用seata提供的代理数据源包装原有的数据源
所以得手动注入Hikari数据源,然后使用seata代理数据源包装返回
分析数据源自动配置类源码,数据源的导入都是在DataSourceConfiguration中
创建Hikari数据源就是调用了DataSourceConfiguration
的create方法,传入的是这两个参数
DataSourceProperties
和指定数据源
所以自定义的配置类 直接仿造create中的方法实现就能手动返回一个Hikari数据源了,最后用seata代理包装返回
4、将seata-server目录文件中的 file.conf registry.conf
复制在 resource 目录下,更改 file 中的配置
vgroup_mapping.{application.name}-fescar-service-group = "default"
5、给事务的入口标注@GlobalTransactional
远程的小事务用·@Transactional
最终一致性库存解锁逻辑
消息队列完成最终一致性