项目DinosaurStore2

Dinosaur Store2

商品点击上架之后,微服务把商品包装起来,然后远程调用es服务,将商品存储到es中,到后面便于检索。spuid中会有多个skuid,然后一个skuid会有多个attrid,当然skuid中的attrid内部不重复,每个skuid之间的attrid可能重复。然后考虑下结构,就是封装的时候,spuid中放skuidlist,list里面是多个属性,然后属性之存放是能够被检索的。

请求是如何走的

还有一个主要的就是,首先在本地机子做了hosts的域名映射,然后nginx监听到这个域名的80端口的时候,反向代理给网关,网关在转给各个微服务。

微服务

该项目,后台管理系统是使用前后端分离的vue框架,后台是微服务,里面划分了很多个微服务。然后介绍注册配置中心,远程调用等具体见DinosaurStore1

给用户访问的商城网页,前端使用nginx进行反向代理,然后做到了动静分离。

商品分类redis缓存

商品分类,即时性和一致性要求不高,即读多写少,或者商品介绍。 而写多读少的直接去数据库查询。

本项目中商品分类的一致性解决方案:(缓存数据一致性的解决方案)

1 缓存的所有数据都有过期时间,数据过期后下一次查询出发主动更新

2 读写数据的时候,加上分布式的读写锁。这个在读多写少的情况下,没有多少问题。

注意,商城首页中,商品的一级目录indexPage()的缓存,使用的是springcache,然后配置失效时间,指定key,指定缓存key,只要导入springcache依赖加上注解就行。而商品的getCatalogJson()二三级目录的缓存,使用的是分布式读写锁,然后使用redis查询不到,先更新数据库,再删除的方式,还有ttl。

查询所有的一二三级目录并封装,这个访问量大,不是经常更新,而且一致性要求不高。该项目中,将所有的对象转化成JSON存储到redis,用的是StringRedisOpionts的opsforvalue,而没有用另外的hash等的类型。

经常修改的数据,不写入缓存的,直接操作数据库方便。但是秒杀什么的肯定除外。先更新数据库,再删除缓存。

需要解决三个问题:

1
2
3
4
5
6
7
8
9
10
/**
* 给缓存中放入json字符串,拿出来的json字符串,还能逆转为能用的对象类型。(序列化与反序列化)
*
* 1 、空结果缓存,解决缓存穿透
* 2 、设置过期时间(加随机值),解决缓存雪崩
* 3 、加锁:解决缓存击穿,此处使用读写锁
*/

分布式锁(①redission或者②redis的string数据类型的setnx,需要ex来处理程序异常,释放所的时候要判断是自己uuid的),性能稍微慢一点,但是相比①,不可重入,锁续期等等无;
③本地悲观锁synchnorized,但是在分布式情况下,有多少个服务,还是有多少个线程能同时运行。

springcache对上面三个问题的解决方法:(读模式的第二个,是get方法上加了本地synchnorized锁,因为是不让大量并发过来,本地锁就足够了。然后一人一单等情况就不行。)

Screen-Shot-2022-07-01-at-10.51.55.png

Redission分布式锁

下面讲的是redission分布式锁

Screen-Shot-2022-06-30-at-16.42.51.png

redission的读写锁,可以保证一定能读取到最新的数据,修改期间,写锁是一个排他锁(互斥锁,写锁与读锁也互斥,就是写锁写完读锁才能读到)。读锁是一个共享锁。读读共享,只会在redis中记录好当前的读锁,只要有写,都必须等待。

redission,信号量,可以做分布式限流工作,拿不到信号量的话,证明没有多余的线程来处理,tryAcquire,不等待,没有就算了,用来限流;acquire,阻塞式等待。

redission闭锁:

Screen-Shot-2022-06-30-at-16.42.51.png

数据库,只承担持久化保存到一个地方;把第一次查出数据之后,写入到缓存,下次从redis中取出,加快响应。

适合放入缓存的:①即时性、数据一致性要求不高的,比如商品分类;②访问量大且更新频率不高的,比如商品信息。

canal

可以模拟一个mysql的从服务器,每次监控到mysql的更新之后,会自动更新redis缓存。就是多了一层中间件,需要多一层配置。

ES商城检索

本项目是将商品的spu以及里面有的sku属性都装进去了。

首先,将访问传入的参数封装。

然后进行es检索,模糊匹配(关键词),过滤(按照属性、分类、品牌,价格区间,库存),完成排序、分页、高亮。

然后对数据进行聚合,重新拿到现在我检索到的所有商品的属性,品牌的信息。

最后,将得到的结果封装。然后controller将结果传给自己的html,注意,这个动态页面是放在idea中的,静态页面nginx自己返回了。

然后,再点击分类页面的分类,再次重复上面的步骤,并且,达到了面包屑的效果。

其中,第二步和第三步,

1
2
3
SearchRequest searchRequest = buildSearchRequest(param);// 1 SearchRequest准备检索请求,动态构建出DSl语句
SearchResponse response = esRestClient.search(searchRequest, StoreElasticSearchConfig.COMMON_OPTIONS); // 2 执行检索请求
SearchResult result// 3 分析响应数据,封装成我们需要的格式

商城业务,异步执行

Screen-Shot-2022-07-02-at-09.33.59.png

其中,1,2,3可以并行执行,然后4,5,6等1完成后也可以并行执行

代码中使用了CompletableFuture,他的.supplyAsync可以得到结果,.thenAcceptAsync可以拿到钱一次运行到的结果,.runAsync就是异步执行所以,1用了.supplyAsync,3 4 5用了.thenAcceptAsync,表示都拿到1的结果并且执行,2的话.runAsync和1并行运行就好。注意里面使用了@ConfigurationProperties(prefix = “store.thread”),可以在application.yaml中执行属性(导入spring-boot-configuration-processor包后有提示),然后然后就可以自己在配置文件设置ThreadPoolExecutor的属性了。

登陆

注意,从index中拿到数据,使用的是提交表单(指定action),form中是减值对格式,不是json,输入的用input并指定对应的entity的name。

项目注册

其中的验证码,然后验证码生成放入redis中,设置过期时间5min,然后同时通过openfeign远程调用第三方服务这个微服务阿里云短信服务发送给用户,然后用户输入进行验证。然后openfeign远程调用meber微服务进行注册。后面用户的信息放到数据库之后,然后在存到springsesion中,是redis操作的。解决统一服务不同机器上(同一jsessionid),以及不同服务(指定父domain)。

账号密码登录

OAuth2 社交登陆

Screen-Shot-2022-07-02-at-23.57.24.png

其中5中,除了带code,还要带应用id,应用密码(不能泄漏),是第三方应用自己后台处理的。

使用code换取accesstoken,code只能用一次。

同一个用户的accesstoken一段时间是不会变化的,即使获取多次。

登陆成功后,将用户信息放入session中。

Screen-Shot-2022-07-03-at-00.13.07.png

拿到accesstoken之后,可以用这个,发其他用户接口的请求,然后拿到微博开放的用户的信息。

TikTok二面: 说下二维码登录的原理?

session共享问题

session其实就是服务器内存里面的一个数据,可以看作是map,session会为每个浏览器的用户创建一个session对象放在sessionmanager中,创建好session后会命令浏览器保存一个cookie,下次浏览器访问的时候会带上这个服务器给我们的cookie(jsessionid这个cookie),然后服务器拿到服务器中对应的session。

1 同一个服务,分布式下多台服务器,session不能共享。

2 不同服务下,session下不能共享。

同一服务多台服务器解决:

1 session,全量复制,每一个服务都存了所有的session,可扩展性就不高了

2 session信息存在客户端,但是会泄漏

3 hash一致性,每一次负载均衡的时候到同一个服务器。但是服务器重启可能部分session丢失,水平扩展后路由不到同一个服务端。

4 后端统一存储到session。存储到数据库,redis等nosql中间件中。优点:没有安全隐患水平扩展重启扩容不丢失。

缺点:增加了一次网络调用并且需要修改应用代码,比直接从内存中拿session会慢很多。

项目使用使用SpringSession然后存到redis。

不同服务之间的解决:

服务器发卡的时候,就是给浏览器cookie的时候,指定domain,本来默认域名是当前的域名,可以用springsession来写,项目中使用四定义springsession来写成“dinosaurstore.com”。

Screen-Shot-2022-07-03-at-11.03.47.png

后面统一存储,前端一个卡通用,用的是同一个jsessionid。

SpringSession原理

Screen-Shot-2022-07-03-at-14.08.23.png

简单来说,就是request进行了封装,本来可以request.getSession()获取原生session的,这里通过装饰着模式包装之后从sessionrepositury中拿到redis操作的session。然后也有过期时间喝自动刷新过期时间。(仿大众点评是通过两层拦截器,第一个拦截器拦截所有,redis有用户的话,刷新token,后面那个是需要拦截的请求网址,要是用户未登录,拦截到登录界面。)

3 单点登录(需要多一个认证中心服务器)

比如一个公司有多个登陆,淘宝登陆了,咸鱼就不用再登陆了,但是其实他们不是父子域名的关系,怎么办呢?(当然其实这个例子不太好,这个例子其实是社交扫码登录的问题。)

用的许雪里写的框架。许雪里 / xxl-sso

image.png

image10d5bd91acf657f2.png

imaged42aad6978b25108.png

1 上面是其中一个客户端要访问,然后发现没有登陆,然后去主登陆服务器中心登陆,登陆完后生成一个token到redis中,token是键,值里面存储用户信息。

上面的9是:处理登陆请求,登陆成功保存用户登录状态信息,同时将token令牌返回出去。然后,给当前的浏览器域名(是登陆的中心的域名)留一个记号(cookie),用户token放进去。用户信息(当然是需要从mysql拿到的)存redis。

其中第10步有两步,1是重定向回来,2是setcookie。

2 第二个客户端再来的时候,因为1已经登陆了,第一次登陆第9步的时候存储了cookie,所以此次第4步的时候去访问登陆服务器的时候,带上cookie(里面有一个token),登陆服务器发现有这个token,那么直接让页面返回到原来的客户端服务器,然后根据这个token从redis拿到用户信息,然后存储到自己的session。

总的来说,1 就是中心服务器生成一个token,然后给浏览器cookie,2 浏览器下次访问另一个客户端时,先访问认证中心的时候带上token访问,认证中心发现有,让他直接返回,3 然后客户端根据这个token从redis拿到用户信息,然后保存到session。4 下次这个客户端在访问,直接从session中拿就可以了。

购物车

上面登录好了之后,添加购物车之前,需要先登录,所以没登录的时候,使用拦截器拦截,然后ThreadLocal保存用户信息给controller使用。具体的方法见redis-practice(其中还有两层拦截器,第一层所有,刷新token,第二层拦截需要登录的,这样会话就不容易过期)。

注意事项1(远程调用)

但是此处有一点需要注意,远程调用的话,会走拦截器(注意这个是会走的),但是没有带cookie这个过程(在请求头中),session拿不到,就拿不到user信息,那么ThreadLocal中的数据为空,这怎么解决?

1
2
3
// feign在远程调用之前要构造新的请求requestTemplate,也会会调用很多的拦截器
// 所以此处使用了RequestInterceptor,远程调用之前,首先给这个新的远程调用请求,加上之前从浏览器进来的老请求的请求头参数,特别是cookie参数
// 远程那个服务知道了有cookie了之后,一切就正常了。

注意事项2(异步编排)

之前:老请求->order服务->orderservice->远程调用member地址->远程调用购物车cart物品信息。

这几个都是在同一个线程中,所以同一个threadload可以管理到

现在:1 orderservice 2 interceptor->address 3 远程调用购物车cart物品信息 。

在2号线程中拿不到那个老请求的数据,(注意那个老请求的数据在threadlocal中的) ,所以又出现问题了。

但是此处异步模式下,上面的解决方法有问题,因为feign的拦截器不在同一个线程中,拿不到老请求数据。

1
2
3
4
5
// 解决方法,在刚开始的线程中拿到老请求(就是浏览器访问controller的时候,带有cookie信息的)数据
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

// 然后在别的线程中把老请求set进去,达到共享目的,这样拦截器中就能拿到了
RequestContextHolder.setRequestAttributes(requestAttributes);

RabbitMQ

订单

订单界面需要查询很多数据

1 远程查询member中用户的地址。

2 远程查询cart中选中的各种物品的价格等信息。此处远程查询就遇到了上面的拦截器ThreadLocal中数据为空的情况。

3 2查询到商品后,查询商品的库存用thenRunAsync

上面两个方法互不影响,可以使用线程池异步编排。 然后又有问题,见上面注意事项2

防止订单提交的重复(接口幂等性)

幂等性:就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。

删查,都是幂等的,带主键的插入也是幂等的,更新定值幂等

但是每次更新加一等就不是幂等的,而且插入不是按照主键的插入也不是幂等的。

解决方法一:

token机制,与redis的setnx差不多,需要原子行,用lua脚本

解决方法二:

悲观锁,Redission。

本项目中使用令牌保证幂等性。

令牌:服务器一个,页面一个。

在购物车到查看订单页面后,会有保存到redis的token(生成的uuid加userId)这个是服务器的,以及页面存一份,就是vo保存的,然后放到threadLocal中,如果提交了订单之后马上把服务器里面的也就是redis的删掉,此处redis和vo的判断相等以及删除需要保证原子性。

提交订单

在订单页面点击提交订单之后,会有一系列的操作,去创建、下订单、验令牌(幂等性问题)、验价格、锁定库存(去每个仓库找库存,一个个试过来)…

分布式事务问题

然后注意这里面的,①前面一个远程调用成功了,后面一个远程调用失败了,前面一个远程调用的回滚问题。

或者②(远程服务假失败)一个远程调用是成功的,回来的时候网络慢跑出了超时异常,那么这个调用远程调用的事务就要被回滚了。

image.png

所以说,Transactional是本地事务,只能控制住自己的回滚,控制不了其他服务的回滚。

分布式事务:最大原因,网络问题 + 分布式机器。

原子性:下订单、减库存、减积分账户扣款同时成功或者失败

一致性:A转账B,转完后总量还是一致的

隔离性:事务之间隔离,不同的人下订单,一个人失败了其他人不影响

持久性:数据库断电,还是存在的

原子性,①下订单、②减库存、③减积分账户是在三个服务,在第一个机器中调用了②,③,但是由于假异常,或者后一个远程调用失败,前一个远程也不能回滚,所以出现了分布式事务。

解决:①使用死信队列解决前面说的 最终一致性,自动解锁库存。

Screen-Shot-2022-07-07-at-00.24.06.png

Screen-Shot-2022-07-07-at-00.25.24.png

这个问题的解决是下面那一张图片,右上角释放订单服务一旦释放之后,再发一个mq给交换机,交换机直接转给解锁库存服务。

Screen-Shot-2022-07-07-at-00.14.36.png

Screen-Shot-2022-07-07-at-00.15.24.png

image6524b2612af439fa.png Screen-Shot-2022-07-06-at-21.55.14.png

上面第二个,复用了交换机,省去了一个交换机。上图使用的交换机是topic类型的,可以模糊匹配。

还有另外一个问题:② 消息可靠性的问题(核心)

image886f550d3b1fed94.png

还有一个:③ 消息重复问题

image.png

④ 消息积压

imagecb0d10c0fbba2b85.png

使用RabbitMQ的延时队列:TTL+死信路由

定时任务的话,全盘扫描耗时,而且每一段时间扫描的话,要是刚扫完一个过期那就要等到下一次扫描才能取消,时间误差大。

CAP定理(一致性、可用性、分区容错性)(强一致性)

这三个不能三者兼顾,分区容错性一定要满足,然后一致性和可用性得做选择了。

比如要满足CP,那么不可用了还怎么一致呢?

分布式系统中实现一致性的raft算法(感觉跟redis的领导选举然后领导有变动随从日志复制)、paxos算法等等。

BASE定理

如果做不到强一致性,那么采用适当的弱一致性,即最终一致性。

就是刚才举的例子,我们在本地系统的话,下订单口库存减积分可以强一致,要么都回滚;但是在分布式中,强一致性很难,可以让他们损失一些响应时间,多尝试几次,或则限制流量,让他们首先访问失败,或者等待。

image5a5bc7636bf01519.png

一些分布式事务解决方案

可以使用阿里巴巴的seata依赖

里面的AT模式:然后会有一个全局协调器,所有远程调用成功后都会告诉协调器成功,有一个不成功回滚。

TCC模式:

imagec28ac0aba71182ad.png

柔性模式

Screen-Shot-2022-07-06-at-17.01.59.png
下面是一些原理:

事务是用代理对象来控制的,如果像下面直接调用b c方法,相当于跳过了代理,相当于直接a方法吧bc方法复制粘贴过来了,和a是共用一个事务的。

本地事务失效问题,同一个对象内食物方法互相调用默认失效,原因是绕过了代理对象,事务使用对象来控制的。

解决:

使用代理对象来调用事务方法

① 导入依赖spring-boot-starter-aop。

② @EnableAspectJAutoProxy开启动态代理功能。以后所有的动态代理都是aspectj创建的(即使没有接口也可以创建动态代理)。对外暴露代理对象

③ 用代理对象 进行 本类方法互调。不过我们的项目中没有本类方法的互调,而是另外的service的远程调用

image.png

支付页面

此处吗,点击支付宝后,直接算他成功支付了,oms_payment_info的信息也给提交了。然后订单的信息也更新了oms_order里面的pay_type是1代表已经付款等待发货,其他的4什么的表示已经关闭等等,解锁库存的订单就是收到消息后要先看这个状态(库存服务的unlockStock的getOrderStatus方法就是看订单状态的)(数据表厘前面几个0是因为一开始没有加死信队列没有处理30min关闭的情况,后面的4是加了之后然后订单没有完成付款的情况,在后面出现1是因为付款成功的功能也给我加上了)。

秒杀

Screen-Shot-2022-07-08-at-16.03.49.png Screen-Shot-2022-07-08-at-16.05.09.png

05,在网关层

06,验证码输入有快有慢,购物车结账锁库存用户时间都不一样

07,前端只能一秒点一下,后端限制次数总次数,链路上有问题马上失败而不是等待,将一部分流量引导到失败页面

​ 限流、熔断、降级。

​ 熔断:自动,以前是远程调用超时,才断,现在知道你会调用超时,所以以后直接断开,不调用这个服务了。

​ 降级:人工,基于全局考虑。停止一些正常服务的运行。

​ 限流:从网关放回来的请求只有10000,其他的直接丢弃。

08,所有拿到信号量的请求,进入队列,然后慢慢创建订单

后台上架秒杀服务

独立的微服务运行秒杀

三天内秒杀的商品在提前缓存到redis中,同时库存也缓存进redis。所以要用到定时任务。QUARTZ,Job Scheduler,cron表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 定时任务
* 1 @EnableScheduling 开启定时任务
* 2 @Scheduled 开启一个定时任务
* 3 自动配置类 TaskSchedulingAutoConfiguration
*
* 异步任务
* 1 @EnableAsync 开启异步任务功能
* 2 @Async 给希望异步执行的方法上标注
* 3 自动配置类 TaskExecutionAutoConfiguration
*/

@Slf4j
@Component
@EnableScheduling
@EnableAsync
public class HelloSchedule {

/**
* 1 Spring中6位组成,不允许第七位的年
* 2 在最后一位周的位置,1-7代表周一到周日
* 3 定时任务不应该阻塞,比如远程调用会阻塞,默认是阻塞的。
* 1 可以让业务运行以异步的方式,completableFuture
* 2 定时任务的线程池的个数默认是1,可以给池子的默认线程个数改多一点呢(这个版本不好使)
* 3 让定时任务异步进行 @EnableAsync,@Async给希望异步执行的方法上面标注
*
* 解决:使用异步任务+定时任务来完成定时任务不阻塞的功能
*/
@Async
@Scheduled(cron = "* * * * * ?")
public void hello() throws InterruptedException {
System.out.println("hello..");
Thread.sleep(3000);
}
}

秒杀上架的幂等性问题、分布式锁

分布式系统中,使用分布式锁,拿到锁的机器,才能进行上架。

对于同一商品重复上架的问题,判断下redis中是有对应的key了。

首页展示了秒杀的商品,然后点进去正在秒杀的话会看到消息

image60577c377c0f8058.png

Screen-Shot-2022-07-08-at-16.21.02.png

Screen-Shot-2022-07-08-at-16.19.42.png

右上角优点:业务统一,需要一系列普通的流程确认页面,支付页面,等,每个服务都有一定的参与但是若流量非常大其他服务可能也扛不住。

第二个优点:一开始没有操作任何一次数据库,只在缓存中很快,MQ保证流量削峰。后续处理跟独立的业务不一样,MQ的可靠性要保证。

登陆判断,拦截器;

秒杀时间,数量,此人是否买过(setnx,不存在才占位,成功可以(幂等性))。

MQ:把订单号码等等信息封装,然后发到队列中,让订单服务去处理。流量削峰,让订单服务的处理平稳的一个固定的请求数量中。

Screen-Shot-2022-07-08-at-16.32.31.png

信号量

在redis中存一个库存的,设置一个商品对应的随机码,保护了,不然时间还没到但是可能被恶意的请求信号量弄完了。

Sentinel

上面秒杀中,一些熔断、降级、限流,是通过alibabacloud的sentinel实现的。

这个sentinel也是一个服务,需要先启动起来,比如利用docker

Sleuth+Zipkin(可视化) 服务链路追踪

YNInoMX86DFvi3p

Ag5QSNf47aVGWuY

CjBOrKhGfqcHoPe

1
docker run -d -p 9411:9411 openzipkin/zipkin

高并发有三宝:缓存、异步、队排好(当然还有主从高并发读、集群高并发写)

VXYCbHgrOREJs5M

redis做缓存

结合线程池、进行异步编排

将所有高峰流量,或者要做分布式事务的消息,放到,后台服务一个个处理

Author: Jcwang

Permalink: http://example.com/2022/06/23/%E9%A1%B9%E7%9B%AEDinosaurStore2/