尚硅谷-谷粒商城-电商项目-秒杀系统-笔记

商城项目简介

项目主要实现了一个模拟电商的分布式秒杀系统,核心模块包括注册登录模块、订单模块、秒杀模块。
框架是spring一套,用到的组件包Nignx服务器,redis,Mysql数据库,rabbitMQ 中间件,ES检索工具,整个项目学习理解分布式微服务的一个思想

主要使用到了 nacos,用于服务注册,和配置管理,fegin用于远程调用,网关用于路由,sentinel用于服务降级熔断,redis用来作缓存。
项目部署
生产环境的集群部署都在linux机器上,单机可以考虑本机
服务
0 后台管理服务 / 代码逆向生成器服务 (生成基本代码) 二者均来自人人开源
1公共服务模块公共依赖(MySQL、mybatis、lombok、cloud相关依赖等)、bean、工具类等
2商品服务、3仓储服务、4订单服务、5 会员服务、6第三方功能服务:第三方服务调用:发送短信,查物流等、7检索服务 ES、8认证服务、9单点登录、10购物车、11订单 12秒杀 13网关 14优惠服务
数据库表
管理系统、商品、库存、用户、订单(不用外键,电商数据量太大,消耗性能)
商城业务
商品上架、商品检索、商品详情(缓存技术)、购物车、订单、秒杀系统

秒杀(高并发)系统关注的问题

服务单一职责
独立部署成一个微服务,秒杀服务即使自己扛不住压力,挂掉,不要影响别人

库存预热 + 快速扣减
秒杀读多写少,无需每次都校验库存,我们库存预热,以缓存技术放到redis中,信号量控制进来秒杀的请求。 此外还有一个问题,redis可能扛不住,单机并发两三W,做集群,分片

动静分离
nginx做好动静分离,保证秒杀和商品详情页的动态请求才打到后端的服务集群
或者使用 CDN 网络,分担服务器压力
CDN,全称Content Delivery Network,即内容分发网络,CDN是构建在网络上的内容分发网络,依靠部署在各地边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能,是用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率,核心技术是内容存储和分发技术。

恶意请求拦截
识别非法攻击的请求(脚本请求(数量多),伪造请求(需要带令牌但不带的))并进行拦截 ,网关层

秒杀链接加密
如果都知道秒杀发什么请求了,可能就会写脚本,恶意攻击模拟秒杀请求,1000次/s攻击,可以考虑链接加密比如 MD5、UUID等作为链接的值或者 采用之前提到的随机码,秒杀开始了 才暴露返回随机码,总之防止连接暴露,自己工作人员,提前秒杀商品

流量错峰
使用各种手段,将流量分担到更大宽度的时间点,比如验证码,加入购物车

流量消峰
1万个请求,每个1000件被秒杀,双11所有秒杀成功的请求,进入队列,慢慢创建订单,扣减库存即可

限流 & 熔断 & 降级
前端限流(按钮点击有间隔时间等,容易攻破,直接发ip请求) + 后端限流(多次点击 就取一次 等)+没登陆先滚去登录限流
集群一共就能处理10W请求,每次网关处理10W请求就等两秒再转发请求
限制次数,限制总量,快速失败降级运行,熔断隔离防止雪崩

SSO(单点登录)

1、什么是SSO
单点登录(SingleSignOn,SSO),就是通过用户的一次性鉴别登录。当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。这种方式减少了由登录产生的时间消耗,辅助了用户管理,是目前比较流行的。(新浪旗下,微博 视频 体育啥的 域名都不一样,springsession不能解决)
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第1张图片

尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第2张图片

传统的session弊端:

  • 每个用户的登录信息都会保存到服务器的session中,随着用户的增多,服务器开销会明显增大
  • 由于session是存在与服务器的物理内存中,所以在分布式系统中,这种方式将会失效。虽然可以将session统一保存到Redis中,但是这样做无疑增加了系统的复杂性,对于不需要redis的应用也会白白多引入一个缓存中间件
  • 对于非浏览器的客户端、手机移动端等不适用,因为session依赖于cookie,而移动端经常没有cookie
  • 因为session认证本质基于cookie,所以如果cookie被截获,用户很容易收到跨站请求伪造攻击。并且如果浏览器禁用了cookie,这种方式也会失效
  • 前后端分离系统中更加不适用,后端部署复杂,前端发送的请求往往经过多个中间件到达后端,cookie中关于session的信息会转发多次
  • 由于基于Cookie,而cookie无法跨域,所以session的认证也无法跨域,对单点登录不适用
  • SSO缺点:认证服务器访问压力较大。

JWT(JSON Web Token)
JWT的本质就是一个字符串,有签名信息,接收后可以校验是否被篡改,所以可以用于在各方之间安全地将信息作为Json对象传输。JWT认证流程如下:

  • 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口,这个过程一般是一个POST请求。建议的方式是通过SSL加密的传输(HTTPS),从而避免敏感信息被嗅探
  • 后端核对用户名和密码成功后,将包含用户信息的数据作为JWT的Payload,将其与JWT的Header分别进行Base64编码拼接后签名,形成一个JWT Token,形成的JWT-Token就是一个如同lll.zzz.xxx的字符串
  • 后端将JWT Token字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的JWT Token即可
  • 前端在每次请求时将JWT Token放入HTTP请求头中的Authorization属性中(解决XSS和XSRF问题)
  • 后端检查前端传过来的JWT Token,验证其有效性,比如检查签名是否正确、是否过期、token的接收方是否是自己等等
  • 验证通过后,后端解析出JWT Token中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果

JWT结构如下:
JWT头:签名使用的算法、令牌的类型…
有效载荷:包含用户信息的数据。默认情况下JWT是未加密的,采用base64算法,拿到JWT字符串后可以转换回原本的JSON数据,因此不要构建隐私信息字段,比如密码…
签名哈希部分:对上面两部分数据签名,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,以确保数据不会被篡改。

登录功能(生成token)
认证中心只负责认证和token的颁发
1、用接受的用户名密码核对后台数据库
2、将用户信息加载到写入redis,redis中有该用户视为登录状态。
3、用userId+当前用户登录ip地址+密钥生成token
4、重定向用户到之前的来源地址,同时把token作为参数附上。
验证登录(token)功能,用jwt解析
1、利用密钥和IP检验token是否正确,并获得里面的userId
2、用userId检查Redis中是否有用户信息,如果有延长它的过期时间。
3、登录成功状态返回。

业务模块的登录检查(@注解与拦截器)
1 、由认证中心签发的token如何保存?保存到浏览器的cookie中
2 、难道每一个模块都要做一个token的保存功能? 拦截器
3 、如何区分请求是否一定要登录?自定义注解
利用在springmvc中的拦截器功能,验证功能是每个模块都要有的,代码抽离到公共模块
自定义注解来检验方法是否需要验证
某个controller方法需要验证用户登录,在方法上加入自定义的注解。元注解考点涉及

注册

  • 使用到了 JSR303校验:用户、密码、手机号、验证码 不为空@NotEmpty,限制长度@Length
  • 前端验证码倒计时
  • 整合短信验证码:阿里云的短信服务,开通同时验证短信功能是否能发送,然后使用 Java 测试短信是否能进行发送
  • 考虑验证码防刷校验:防止恶意消耗资源。前台,接口防刷,限制一分钟后提交(界面刷新效果就没了,也需要后台)。后台,存入redis(key:手机号,vlue:验证码 +当前时间戳) 并且设置过期时间,过期了 说明验证码有效期过期了。当前时间戳作用:用户继续申请验证码,用手机定位上次发验证码时间,比如60S内发过就提示不能发送。
    尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第3张图片
  • 验证码校验成功(删除验证码)->注册还需要掉会员模块注册->检查手机号和用户名 邮箱等是否唯一 ->密码存储:安全性问题,MD5(信息摘要算法,,不可逆、压缩性,易计算,抗修改,抗碰撞)盐值(抗暴力破解,可选时间戳等随机值,表多个字段记录这随机值)加密
  • 重定向登录页面,重定向携带数据,利用session原理,将数据放在session中,只要跳转到下一个页面取出这个数据,同时session中的数据就会删掉(避免返回原页面后还保留数据,数据泄露)。用session 就会出现分布式session问题,要考虑解决
    尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第4张图片

Nginx

部署虚拟机中,作用:反向代理+负载均衡+动静分离+限流
反向代理
对于集群非常需要,集群在内网部署,不能暴露它的外网ip,容易被引起攻击,前置一个服务器(比如nginx,有一个公网IP),搭建域名访问环境,可以代理外部网络主机访问内部网络。(正向反向是相对于我们自己电脑,科学上网属于正向代理)
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第5张图片
电脑host文件配置域名映射,请求先访问到nginx,niginx请求负载均衡交给网关,网关从注册中心动态发现商品在哪儿,网关再负载均衡到商品服务

Nginx 有哪些负载均衡策略?

  • 1、轮询(默认)round_robin
    每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器 down 掉,能自动剔除。
  • 2、哈希 ip_hash url_hash
    每个请求按访问 ip 的 hash 结果分配,这样每个访客固定访问一个后端服务器,可以解决 session 共享的问题。实际场景下,一般不考虑使用 ip_hash 解决 session 共享。
  • 3、最少连接 least_conn
    下一个请求将被分派到活动连接数量最少的服务器
  • 4、指定权重
    指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。
  • 5、按后端服务器的响应时间来分配请求,响应时间短的优先分配。

限流
缓存、降级和限流是开发高并发系统的三把利器。
常用4种限流算法:
1、计数器(固定窗口)算法:计数器算法是使用计数器在周期内累加访问次数,当达到设定的限流值时,触发限流策略。下一个周期开始时,进行清零,重新计数。
使用redis的incr原子自增性和线程安全即可轻松实现。存在临界问题,两个窗口间短时间涌入大量流量,打坏系统。
2、滑动窗口算法:将固定窗口格子划分更多,流量更平滑,统计更精确,可解决临界问题。
3、漏桶算法:访问请求到达时直接放入漏桶,如当前容量已达到上限(限流值),则进行丢弃(触发限流策略)。漏桶以固定的速率进行释放访问请求(即请求通过),直到漏桶为空。
4、令牌桶算法:令牌桶算法是程序以r(r=时间周期/限流值)的速度向令牌桶中增加令牌,直到令牌桶满,请求到达时向令牌桶请求令牌,如获取到令牌则通过请求,否则触发限流策略。
Nignx也可限流:
一是控制速率:采用漏桶算法。
二是控制并发量(连接数):利用连接数限制 某一个用户的ip连接的数量来控制流量。
Spring Cloud Gateway 默认采用令牌桶算法限流,限流的维度 ip限流、用户限流、接口限流,采用redis(Ratelimter接口)+lua脚本实现。

性能优化
动态资源、静态资源分离,是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后我们就可以根据静态资源的特点将其做缓存操作。
假如2000条资源 1800静态+200动态 1800Nginx都处理了,剩下200再去微服务的tomcat处理,降低服务器压力和优化系统性能
静态资源大且多 还应该考虑 CDN——Content Delivery Network,内容分发网络:CDN就是采用更多的缓存服务器(CDN边缘节点),布放在用户访问相对集中的地区或网络中。当用户访问网站时,利用全局负载技术,将用户的访问指向距离最近的缓存服务器上,由缓存服务器响应用户请求。

Redis

考点太多,写点项目相关的,集群、事务、主从复制、持久化、淘汰策略等等另作复习
应用场景

  1. 配合关系型数据库做高速缓存:高频次,热门访问的数据,降低数据库IO + 分布式架构,做session共享
  2. 多样的数据结构存储持久化数据: List -->按时间最新N个数据、Set–> 去重、Zset --> 排行榜,TopN,底层是hash+跳表、Hash --> 购物车、原子性的INCR/DECR–>计数器,秒杀、Expire–>时效性验证,手机验证码…
  3. 分布式锁。实现的核心:加锁原子性: setnx(“lock”,UUID,10s) 加锁的过程设置过期时间(避免宕机死锁),自己的版本号(锁已经过期,误删他人锁)、 解锁原子性:lua脚本、 redis(AP)区分另外两种分布式锁:基于数据库实现排他锁+zookeeper(CP)。

Redis使用(支持)相同的Lua解释器,来运行所有的命令。Redis还保证脚本以原子方式执行:在执行脚本时,不会执行其他脚本或Redis命令。这个语义类似于MULTI(开启事务)/EXEC(触发事务,一并执行事务中的所有命令)。从所有其他客户端的角度来看,脚本的效果要么仍然不可见,要么已经完成。分布式锁 - Redisson
不推荐使用 setnx来实现分布式锁,使用Distributed locks with Redis中java版本的Redisson,具体有*可重入锁、读写锁、公平锁、信号量 等等…
可重入锁
看门狗机制---- 锁的自动续期,解决死锁问题
如果拿到分布式锁的节点宕机,且这个锁正好处于锁住的状态时,会出现锁死的状态,为了避免这种情况的发生,锁都会设置一个过期时间。这样也存在一个问题,加入一个线程拿到了锁设置了30s超时,在30s后这个线程还没有执行完毕,锁超时释放了,就会导致问题,Redisson给出了自己的答案,就是 watch dog 自动延期机制。

购物车数据结构
我们的购物车结构是一一个双层Map: Map>
外头key就是用户的id,里面的key 是商品 id, value 才是这个商品的购物车信息。
map查询key可以O(1)很快
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第6张图片

系统压力测试

压力测试考察当前软硬件环境下系统所能承受住的最大负荷并帮助找出系统的瓶颈所在,压测都是为了系统在线上的处理能力和稳定性维持在一个标准范围内,做到心中有数。使用压力测试,我们有希望找到很多种用其他测试方法更难发现的错误(百万级请求量下更明显),有两种错误类型是:内存泄漏:对象未复用,不停消耗内存/对象申请完内存后无法合理释放内存、并发与同步

有效的压力测试系统将应用以下这些关键条件:重复、并发、量级、随机变化

压力测试软件:JMeter。作用:可以实现对服务器、网络、对象模拟巨大的负载,测试强度和分析整体性能

性能测试主要关注:
吞吐量:每秒系统能处理的请求数、任务数。 QPS TPS
响应时间:服务器处理一个请求或者一个任务的耗时。最大相应时间、最少相应时间、90%相应时间
错误率:一批请求中结果出错的请求所占比列(自己的端口被占完了,改端口占有时间分配)

并发量存在的瓶颈在哪

  • 中间件的个数, 中间件的个数越多,网络交互损失越大,延迟越高并发量就越小
  • 业务逻辑中,重复查表
  • 静态资源没有分离
  • 客户端没有开启缓存

调优:

  • nginx动静分离,将项目的所有静态资源都放到nginx里(指定返回规则)
    - 提高jvm的内存配置
    - 优化业务逻辑,用redis缓存热点数据,数据库查询次数
    - 优化数据库,对经常查询的数据简历索引,避免全表查询

ElasticSearch

是分布式的全文搜索引擎,解决海量数据中查询效率地下的问题
为啥要使用:因为在商城中的数据,将来会非常多,所以采用以往的模糊查询,会放弃索引,导致全表查询,效率很低,如果用ES做一个全文索引,可以提高查询速度。
es采用倒排索引;如查找某个字符, 正排索引是遍历所有的内容, 看哪些里面含有要查找的内容,而倒排索引是将文章内容进行分词之后, 类似于以词作为key, 文章的索引作为value来存储, 搜索的时候就可以以关键字作为key来获取,通过得分返回最佳结果。Elastic 是 Lunce 的封装,提供了 REST API 的操作接口,开箱即用。
索引->数据库,类型->表(已删除类型),文档->行,属性->列
kibana ,es的可视化界面

缓存

哪些数据适合放入缓存?

  • 即时性、数据一致性要求不高的
  • 访问量大且更新频率不高的数据(读多、写少)

缓存问题
击穿、雪崩、穿透、缓存与数据库数据不一致
业务流程
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第7张图片

缓存数据一致性产生原因

缓存数据一致性 - 双写模式

尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第8张图片
两个线程写 最终只有一个线程写成功,后写成功的会把之前写的数据给覆盖,这就会造成脏数据
缓存数据一致性 - 失效模式
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第9张图片
三个连接 ,一号连接 写数据库 然后删缓存、二号连接 写数据库时网络连接慢,还没有写入成功、三号链接 直接读取数据,读到的是一号连接写入的数据,此时二号链接写入数据成功并删除了缓存,三号开始更新缓存发现更新的是二号的缓存

缓存数据一致性解决方案

无论是双写模式还是失效模式,都会到这缓存不一致的问题,即多个实例同时更新会出事,怎么办?

  • 1、如果是用户纯度数据(订单数据、用户数据),这并发几率很小,几乎不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
  • 2、如果是菜单,商品介绍等基础数据,也可以去使用 canal 订阅,binlog 的方式
  • 3、缓存数据 + 过期时间也足够解决大部分业务对缓存的要求
  • 4、通过加锁保证并发读写,写写的时候按照顺序排好队,读读无所谓,所以适合读写锁,(业务不关心脏数据,允许临时脏数据可忽略)
    Canal阿里开源中间件,模拟数据库从数据库订阅日志
    尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第10张图片
    总结
  • 我们能放入缓存的数据本来就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前的最新值即可
  • 我们不应该过度设计,增加系统的复杂性
  • 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点

如何保证缓存与数据库双写时的数据一致性?

先更新数据库,后更新缓存
先更新缓存,后更新数据库
先删除缓存,后更新数据库
先更新数据库,后删除缓存
第一种和第二种方案,没有人使用的,因为第一种方案存在问题是:并发更新数据库场景下,会将脏数据刷到缓存。
第二种方案存在的问题是:如果先更新缓存成功,但是数据库更新失败,则肯定会造成数据不一致。
目前主要用第三和第四种方案。
先删除缓存,后更新数据库:

  • 延时双删
  • 或者 更新与读取操作进行异步串行化:维护队列,发现数据不在缓存中,并且此时队列里有更新库存的操作,那么将重新读取数据+更新缓存的操作。

先更新数据库,后删除缓存

  • 采用中间件订阅 Mysql 数据库的 binlog 日志对缓存进行操作。

项目中的异步 & 线程池

高并发系统中 每一个请求都new thread start 很快资源就耗尽了,所以为了控制系统资源 要用线程池,所以异步任务都用线程池,线程池管理线程
面试

  • 计算题
    一个线程池 core 7、max 20 ,queue 50 100 并发进来怎么分配的?
    先有 7 个能直接得到运行,接下来 50 个进入队列排队,再多开 13 个继续执行,线程70个被安排上了,剩下30个默认拒绝策略
  • 开发中为什么使用线程池
    1、降低资源的消耗:通过重复利用已创建好的线程降低线程的创建和销毁带来的损耗
    2、提高响应速度:因为线程池中的线程没有超过线程池的最大上限时,有的线程处于等待分配任务的状态,当任务来时无需创建新的线程就能执行
    3、提高线程的客观理性:线程池会根据当前系统的特点对池内的线程进行优化处理,减少创建和销毁线程带来的系统开销,无限的创建和销毁线程不仅消耗系统资源,还降低系统的稳定性,使用线程池进行统一分配
  • 业务场景:
    查询商品详情页逻辑比较复杂,有些数据还需要远程调用,必然需要花费更多的时间
    尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第11张图片
    假如商品详情页的每个查询,需要如下标注时间才能完成
    那么,用户需要5.5s后才能看到商品相详情页的内容,很显然是不能接受的
    如果有多个线程同时完成这 6 步操作,也许只需要 1.5s 即可完成响应
    使用 CompletableFuture类 异步编排

分布式 Session不共享不同步问题

Session 是存放在服务器端的,类似于Session结构来存放用户数据,当浏览器 第一次发送请求时,服务器自动生成了一个Session和一个Session ID用来唯一标识这个Session,并将其通过响应发送到浏览器。当浏览器第二次发送请求,会将前一次服务器响应中的Session ID放在请求中一并发送到服务器上,服务器从请求中提取出Session ID,并和保存的所有Session ID进行对比,找到这个用户对应的Session。
一般情况下,服务器会在一定时间内(默认30分钟)保存这个 Session,过了时间限制,就会销毁这个Session。在销毁之前,程序员可以将用户的一些数据以Key和Value的形式暂时存放在这个 Session中。当然,也有使用数据库将这个Session序列化后保存起来的,这样的好处是没了时间的限制,坏处是随着时间的增加,这个数据 库会急速膨胀,特别是访问量增加的时候。一般还是采取前一种方式,以减轻服务器压力。
Cookie是由服务端产生的,再发送给客户端保存,它不是内置对象,却是由服务端产生的,产生完后给了客户端;比如客户端访问服务端,第一次访问结束后,我就会产生一个Cookie,把这个Cookie保留到客户端。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容。实际就是颁发一个通行证,每人一个,无论谁访问都必须携带自己通行证。
cookie最典型的应用是:
(一):判断用户是否登陆过网站,以便下次登录时能够直接登录。如果我们删除cookie,则每次登录必须从新填写登录的相关信息。
(二):另一个重要的应用是“购物车”中类的处理和设计。用户可能在一段时间内在同一家网站的不同页面选择不同的商品,可以将这些信息都写入cookie,在最后付款时从cookie中提取这些信息,当然这里面有了安全和性能问题需要我们考虑了

尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第12张图片
我们在auth.gulimall.com中保存session,但是网址跳转到 gulimall.com中,取不出auth.gulimall.com中保存的session,这就造成了微服务下的session不同步问题

0、Session同步解决方案-分布式下session共享问题
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第13张图片
同一个服务复制多个,但是session还是只能在一个服务上保存,浏览器也是只能读取到一个服务的session

1、Session共享问题解决-session复制
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第14张图片
2、Session共享问题解决-客户端存储
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第15张图片
3、Session共享问题解决-hash一致性
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第16张图片
4、Session共享问题解决-统一存储
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第17张图片
方案3和4 都可以。

5、子域共享问题
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第18张图片
SpringSession框架采用5和放大作用域解决这两个问题 。

消息队列 - MQ

使用场景

异步处理:消息发送的时间取决于业务执行的最长的时间
应用解耦:原本是需要订单系统直接调用库存系统
只需要将请求发送给消息队列,其他的就不需要去处理了,节省了处理业务逻辑的时间
流量消峰:某一时刻如果请求特别的大,那就先把它放入消息队列,从而达到流量消峰的作用
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第19张图片
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第20张图片

RabbitMQ

实现最终一致性的时候很必要的一个工具
RabbitMQ简介:
RabbitMQ是一由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现
核心概念
Message
消息,消息是不具名的,它是由消息头和消息体组成,消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括 routing - key (路由键)priority(相对于其他消息的优先权)delivery - mode(指出该消息可能需要持久性存储)
Publisher
消息的生产者,也是一个像交换器发布消息的客户端应用程序
Exchange
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列
Exchange有4种类型:direct(默认)fanouttopic,和
heades
,不同类型的 Exchange 转发消息的策略有所区别
Queue
消息队列,用来保存消息直到发送给消费者他是消息的容器,也是消息的重点,一个消息可以投入一个或多个队列,消息一直在队列里面,等待消费者连接到这个队列将其取走
Binding
绑定,用于消息队列和交换器之间的关联,一个绑定就是基于路由键将交换器和消息队列连接起来的规则,所有可以将交换器理解成一个由绑定构成的路由表
Connection
网路连接,比如一个TCP连接
Channel
信道,多路复用连接中的一个独立的双向数据流通道,信道是建立在真实的TCP连接的内的虚拟连接,AMQP 命令都是通过信息到发送出去的,不管是发布消息,订阅队列还是接收消息,这些动作都是通过队列完成,因为对应操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条TCP连接
Consumer
消息的消费者,表示一个消息队列中取得消息的客户端应用程序
Virtual Host
虚拟主机,表示交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个Virtual host本质上就是一个 mini 版的RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。Virtual host 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ默认的vhost是/。
Broker
表示消息队列服务器实体
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第21张图片
Docker 安装RabbitMQ
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第22张图片
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第23张图片
RabbitMQ 运行机制
AMQP 中的消息路由

AMQP 中消息的路由过程和 Java 开发者熟悉的 JMS 存在一些差别,AMQP中增加了 ExchangeBinding 的角色 生产者把消息发布到 Exchange 上,消息最终到达队列并被消费者接收,而 Binding 决定交换器的消息应该发送给那个队列
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第24张图片
Exchange 类型

Exchange 分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、tanout、topic、headers header匹配AMQP消息的 header 而不是路由键,headers 交换器和 direct 交换器完全一致,但性能差能多,目前几乎用不到了,所以直接看另外三种类型
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第25张图片
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第26张图片

RabbitMQ消息确认机制 - 可靠到达

  • 保证消息不丢失,可靠抵达,可以使用事务消息,性能下降250倍,为此引入确认机制
  • publisher confirmCallback 确认模式
  • publisher returnCallback 未投递到 queue 退回
  • consumer ack 机制
    在这里插入图片描述

可靠抵达 - ConfirmCallback

spring.rabbitmq.publisher-confirms=true

在创建 connectionFactory 的时候设置 PublisherConfirms(true) 选项,开启 confirmcallback

CorrelationData 用来表示当前消息唯一性

消息只要被 broker 接收到就会执行 confirmCallback,如果 cluster 模式,需要所有 broker 接收到才会调用 confirmCallback

被 broker 接收到只能表示 message 已经到达服务器,并不能保证消息一定会被投递到目标 queue 里,所以需要用到接下来的 returnCallback

可靠抵达 - ReturnCallback

spring.rabbitmq.publisher-retuns=true

spring.rabbitmq.template.mandatory=true

confirm 模式只能保证消息到达 broker,不能保证消息准确投递到目标 queue 里。在有些模式业务场景下,我们需要保证消息一定要投递到目标 queue 里,此时就需要用到 return 退回模式

这样如果未能投递到目标 queue 里将调用 returnCallback,可以记录下详细到投递数据,定期的巡检或者自动纠错都需要这些数据

可靠抵达 - Ack 消息确认机制

  • 消费者获取到消息,成功处理,可以回复Ack给Broker
    • basic.ack 用于肯定确认:broker 将移除此消息
    • basic.nack 用于否定确认:可以指定 beoker 是否丢弃此消息,可以批量
    • basic.reject用于否定确认,同上,但不能批量
  • 默认,消息被消费者收到,就会从broker的queue中移除
  • 消费者收到消息,默认自动ack,但是如果无法确定此消息是否被处理完成,或者成功处理,我们可以开启手动ack模式
    • 消息处理成功,ack(),接受下一条消息,此消息broker就会移除
    • 消息处理失败,nack()/reject() 重新发送给其他人进行处理,或者容错处理后ack
    • 消息一直没有调用ack/nack方法,brocker认为此消息正在被处理,不会投递给别人,此时客户端断开,消息不会被broker移除,会投递给别人

RabbitMQ 延时队列(实现定时任务)

场景:
比如未付款的订单,超过一定时间后,系统自动取消订单并释放占有物品
常用解决方案:
Spring的schedule 定时任务轮询数据库
缺点:
消耗系统内存,增加了数据库的压力,存在较大的时间误差
解决:
rabbitmq的消息TTL和死信Exchange结合

使用场景
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第27张图片
时效问题
上一轮扫描刚好扫描,而这个时候刚好下了订单,就没有扫描到,下一轮扫描的时候,订单还没有过期,等到订单过期后30分钟才被扫描到
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第28张图片

RabbitMQ没有提供延迟队列的功能,那怎么办呢?

那就是!TTL+死信队列组合的方式,实现和延迟队列一样的效果。订单系统在用户下单后发送了一条订单信息到MQ交换机交换机把这条消息根据routing key分发到了一个设置了30分钟过期时间队列中,但是该队列没有消费者,所以这些消息终将过期当消息过期变成死信后,会被转发给该队列绑定的死信交换机并根据死信routing key最终被分配到了一个死信队列中死信队列是有消费者的,这些死信消息将被这些消费者获取之后执行对应的业务逻辑

消息的TTL(Time To Live)

  • 消息的TTL 就是消息的存活时间
  • RabbitMQ可以对队列还有消息分别设置TTL
    • 对队列设置就是没有消费者连着的保持时间,也可以对每一个消息单独的设置,超过了这个时间我们可以认为这个消息他死了,称之为死信
    • 如果队列设置了,消息也设置了,那么会取小,所以一个消息如果被路由到不同的队列中,这个消息死亡时间有可能不一样的(不同队列设置),这里讲的是单个TTL 因为他是实现延时任务的关键,可以通过设置消息的 expiration 字段或者 x-message-ttl 来设置时间两者是一样的效果

Dead Letter Exchange(DLX)

  • 一个消息在满足如下条件下,会进死信路由,记住这里是路由不是队列,一个路由可以对应很多队列,(什么是死信)
    • 一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不被再次放在队列里,被其他消费者使用。(basic.reject/ basic.nack)
    • requeue= false上面的消息的TTL到了,消息过期了。
    • 队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上
  • Dead Letter Exchange其实就是一种普通的exchange, 和创建其他exchange没有两样。只是在某一个设置 Dead Letter Exchange的队列中有消息过期了自动触发消息的转发,发送到Dead Letter Exchange中去。
  • 我们既可以控制消息在一段时间后变成死信, 又可以控制变成死信的消息被路由到某一个指定的交换机, 结合C者,其实就可以实现一个延时队列

RabbitMQ可以对消息和队列设置 TTL,目前有两种方法可以设置

第一种方法是通过队列属性设置,队列中所有消息都有相同的过期时间

第二种方法是对消息进行单独设置,每条消息 TTL可以不同
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第29张图片
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第30张图片

下单场景

尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第31张图片
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第32张图片

如何保证消息可靠性 - 消息丢失 & 消息重复

1、消息丢失

  • 消息发送出去,由于网络问题没有抵达服务器

    • 做好容错方法(try-Catch) ,发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式
    • 做好日志记录,每个消息状态是否都被服务器收到都应该记录
    • 做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进行重发
      尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第33张图片
  • 消息抵达Broker, Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚未持久化完成,宕机。

    • 引入RabbitMQ的mirrored-queue即镜像队列,相当于配置了副本,当master在此特殊时间内crash掉,可以自动切换到slave,这样有效的保障了HA, 除非整个集群都挂掉,这样也不能完全的100%保障RabbitMQ不丢消息,但比没有mirrored-queue的要好很多。RabbitMQ的可靠性涉及producer端的确认机制、broker端的镜像队列的配置以及consumer端的确认机制,要想确保消息的可靠性越高,那么性能也会随之而降,鱼和熊掌不可兼得,关键在于选择和取舍。
  • 自动ACK的状态下。消费者收到消息,但没来得及消费然后宕机

    • 一定开启手动ACK,消费成功才移除,失败或者没来得及处理就noAck并重新入队

2、消息重复

  • 消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息重新由unack变为ready,并发送给其他消费者
  • 消息消费失败,由于重试机制,自动又将消息发送出去
  • 成功消费,ack时宕机,消息由unack变为ready, Broker又重新发送
    • 消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态标志
    • 使用防重表(redis/mysq|) ,发送消息每一 个都有业务的唯一 标识,处理过就不用处理
    • rabbitMQ的每一个消息都有redelivered字段, 可以获取是否是被重新投递过来的,而不是第一次投递过来的

3、消息积压

  • 消费者积压
  • 消费者消费能力不足积压
  • 发送者发送流量太大
    • 上线更多消费者,进行正常消费
    • 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理

分布式事务

分布式系统经常出现的异常

机器宕机、网络异常、消息丢失、消息乱序、不可靠的TCP、存储数据丢失…

尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第34张图片
分布式事务是企业中集成的一个难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在微服务架构中,几乎是无法避免的

CAP 定理与 BASE 理论

1、CAP 定理

CAP 原则又称 CAP 定理指的是在一个分布式系统中

  • 一致性(Consistency)
    • 在分布式系统中所有数据备份,在同一时刻是否是同样的值,(等同于所有节点访问同一份最新数据的副本)
  • 可用性(Avaliability)
    • 在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求,(对数据更新具备高可用性)
  • 分区容错性(Partition tolerance)
    • 分布式系统出现网络分区的时候,仍然能够对外提供服务。
      分布式系统中,多个节点之前的网络本来是连通的,但是因为某些故障(比如部分节点网络出了问题)某些节点之间不连通了,整个网络就分成了几块区域,这就叫网络分区

当发生网络分区的时候,如果我们要继续服务,那么强一致性和可用性只能 2 选 1。也就是说当网络分区之后 P 是前提,决定了 P 之后才有 C 和 A 的选择。也就是说分区容错性(Partition tolerance)我们是必须要实现的。
简而言之就是:CAP 理论中分区容错性 P 是一定要满足的,在此基础上,只能满足可用性 A 或者一致性 C。
2、面临的问
需要确保强一致性的场景如银行一般会选择保证 CP 。
对于大多数互联网应用的场景、主机众多、部署分散,而且集群规模越来越大,所以节点故障,网络故障是常态,而且要保证服务可用性达到99.999%,即保证P 和 A,舍弃C

3、BASE 理论

BASE 是 Basically Available(基本可用) 、Soft-state(软状态) 和 Eventually Consistent(最终一致性) 三个短语的缩写。BASE 理论是对 CAP 中一致性 C 和可用性 A 权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的,它大大降低了我们对系统的要求。
BASE 理论本质上是对 CAP 的延伸和补充,更具体地说,是对 CAP 中 AP 方案的一个补充。
因此,AP 方案只是在系统发生分区的时候放弃一致性,而不是永远放弃一致性。在分区故障恢复后,系统应该达到最终一致性。这一点其实就是 BASE 理论延伸的地方。

BASE 理论三要素

  • 基本可用(Basically Avaliable)
    • 基本可用是指分布式系统中在出现故障的时候,允许损失部分可用性(列入响应时间,功能上的可用性)允许损失部分可用性。需要注意的是基本可用不等价于系统不可用
      1 响应时间上的损失,正常情况下搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房断电断网故障),查询的结果响应时间增加到了1~2秒
      2 功能上的损失,购物网站双十一购物高峰,为了保证系统的稳定性,系统的部分非核心功能无法使用,部分消费者会被引入到一个降级页面
  • 软状态(Soft State)
    软状态指允许系统中的数据存在中间状态(CAP 理论中的数据不一致),并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
  • 最终一致性( Eventual Consistency)
    最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

分布式一致性的 3 种级别:
强一致性 :系统写入了什么,读出来的就是什么。
弱一致性 :不一定可以读取到最新写入的值,也不保证多少时间之后读取到的数据是最新的,只是会尽量保证某个时刻达到数据一致的状态。
最终一致性 :弱一致性的升级版,系统会保证在一定时间内达到数据一致的状态。

分布式事务的几种方案

1、2PC 模式
分为两个阶段:
第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反应是否可以提交

第二阶段:事务协调器要求每个数据库提交数据

其中,如果有任何一个数据库否认这次提交,那么所有数据库都会要求回滚他们在此事务中的那部分信息
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第35张图片
柔性事务 - TCC 事务

刚性事务:遵循ACID原则,强一致性。
柔性事务:遵循BASE理论,最终一致性;
与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第36张图片

一阶段 prepare 行为:调用自定义的 prepare 逻辑。
二阶段 commit 行为:调用自定义的 commit 逻辑。
二阶段 rollback行为:调用自定义的 rollback 逻辑。
所谓TCC模式,是指支持把自定义的分支事务纳入到全局事务的管理中。

3、柔性事务 - 最大努力通知型方案

按规律进行通知,不保证数据定能通知成功, 但会提供可查询操作接口进行核对。这种方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种方案也是结合MQ进行实现,例如:通过MQ发送http请求,设置最大通知次数。达到通知次数后即不再通知。

案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询校对、对账文件),支付宝的支付成功异步回调

4、柔性事务 - 可靠信息 + 最终一致性方案(异步通知型)

实现:业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。

订单

1、订单中心

电商系列涉及到 3 流,分别为信息流、资金流、物流,而订单系统作为中枢将三者有机的集合起来
订单模块是电商系统的枢纽,在订单这个模块上获取多个模块的数据和信息,同时对这些信息进行加工处理后流向下个环节,这一系列就构成了订单的信息疏通
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第37张图片

2、订单状态

1、待付款
用户提交订单后,订单进行预下单,需要注意的是待付款状态下可以对库存进行锁定,锁定库存需要配置支付超时时间,超时后将自动取消订单,订单变更关闭状态。

2、已付款/代发货
用户完成订单支付,订单系统需要记录支付时间,支付流水单号便于对账,订单下放到WMS系统,仓库进行调动、配货、分拣,出库等操作

3、待收货/已发货
仓库将商品出库后,订单进入物流环节,订单系统需要同步物流信息,便于用户实时商品的物流状态

4、已完成
用户确认收货后吗,订单交易完成,后续支付则进行计算,如果订单存在问题进入售后状态

5、已取消
付款之前取消订单,包括超时未付款或用户商户取消订单都会产生这种订单状态

6、售后中 项目暂不实现

3、订单流程

订单流程是指从订单产生到完成整个流转的过程。而不同的产品类型或业务类型在系统中的流程会千差万别,比如上面提到的线上实物订单和虚拟订单的流程等,不管类型如何订单都包括正向流程和逆向流程,对应的场景就是购买商品和退换货流程,正向流程就是一一个正常的网购步骤:订单生成>支付订单->卖家发货一确认收货>交易成功。
可概括如下图
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第38张图片
1、订单创建与支付

  1. 订单创建前需要预览订单,选择收货信息等
  2. 订单创建需要锁定库存,库存有才可创建,否则不能创建
  3. 订单创建后超时未支付需要解锁库存
  4. 支付成功后,需要进行拆单,根据商品打包方式,所在仓库,物流等进行拆单
  5. 支付的每笔流水都需要记录,以待查账
  6. 订单创建,支付成功等状态都需要给MQ发送消息,方便其他系统感知订阅

2、逆向流程

  1. 修改订单,用户没有提交订单,可以对订单一些信息进行修改,比如配送信息,优惠信息,及其他一些订单可修改范围的内容,此时只需对数据进行变更即可。
  2. 订单取消 ,用户主动取消订单和用户超时未支付 ,两种情况下订单都会取消订单,而超时情况是系统自动关闭订单,所以在订单支付的响应机制上面要做支付的

4、订单业务

尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第39张图片

订单登录拦截
任何请求都需要先经过拦截器的验证,才能去执行目标方法,访问订单的任何请求都需要登录。这里是用户是否登录,用户登录了则放行,否则跳转到登陆页面
订单确认页

  • 根据商品信息抽取成Vo(value object值对象)
    订单确认页需要的数据
  • 订单确认页数据查询(异步查询)
    询用户地址信息、查询购物车中选中给的购物项、商品是否有库存、查询积分信息
    尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第40张图片
    加上 feign 远程调用的请求拦截器,在每次发送远程请求之前,把老请求的数据同步过来,这样就可以解决请求头的丢失问题了。
    尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第41张图片
    线程在ThraedLocal保存上下文,但是异步情况下ThraedLocal保存线程1上下文,获取address、cart所使用的线程,与主任务的线程不同,所以异步任务无法获取主任务的上下文环境(请求头数据就为空)。解决方案:每次开启新线程复制老线程的上下文
    防重令牌
    /**
    * 接口幂等性就是用户对同一操作发起的一次请求和多次请求结果是一致的
    * 不会因为多次点击而产生了副作用,比如支付场景,用户购买了商品,支付扣款成功,
    * 但是返回结果的时候出现了网络异常,此时钱已经扣了,用户再次点击按钮,
    * 此时就会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,
    * 流水记录也变成了两条。。。这就没有保证接口幂等性
    */
    // 先是在页面中生成一个随机码把他叫做token先存到redis中,然后放到对象中在页面进行渲染。
    // 用户提交表单的时候,带着这个token和redis里面去匹配如果一直那么可以执行下面流程。
    // 匹配成功后再redis中删除这个token,下次请求再过来的时候就匹配不上直接返回
    // 生成防重令牌
    String token = UUID.randomUUID().toString().replace(“-”,“”);
    // 存到redis中 设置30分钟超时
    redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId(),token,30, TimeUnit.SECONDS);
    // 放到页面进行显示token,然后订单中带着token来请求
    confirmVo.setOrderToken(token);
    创建订单
    //下单:去创建订单,验令牌,验价格,锁库存…
    //下单成功来到支付选择页
    //下单失败回到订单确认页重新确认订单信息
    尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第42张图片
    尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第43张图片
    本地事务在分布式下的问题
  • 1、假失败
    如果保存订单成功,远程锁库存假失败,那就会出现问题

假失败就是我们在订单服务调库存服务时, 库存锁定成功,然后由于服务器慢、卡顿、等故障原因,本地事务提交了之后,一直没返回到订单服务

此时再看订单服务,因为调用库存服务时间太长了,库存服务迟迟没有返回结果,可能就会触发 feign 的超时机制,在调用远程服务这里抛异常:read time out 读取超时,但是这个异常并不是我们手动抛的锁库存异常,而是 feign 的异常

并且订单服务,设计的回滚机制,是只要一出现异常就会全部回滚,

结果:库存锁定成功,订单服务因为 feign 的超时机制,出现异常,导致订单数据全部回滚,最终数据不一致

  • 2、调用新服务出现异常之后,已经执行的服务不会回滚
    假设库存锁定成功,将结果返回到了订单服务,我们根据结果又调用了积分服务,让它扣减积分,

结果积分服务内部出现异常,积分数据回滚

此时再看订单服务,订单服务感知到我们手动抛的积分异常,订单数据回滚,但是库存服务,却不会有任何感知,

结果:积分、订单数据全部回滚,库存给锁定了,也是数据不一致

只需要在订单服务的库存执行成功之后,添加一个 int i = 10 / 0;,模拟积分服务出现异常,很容易就能复现这个问题

总结
本地事务,在分布式系统,只能控制住自己的回滚,控制不了其他服务的回滚

产生分布式事务的最大原因,就是网络问题 + 分布式机器

如何解决下单系统这个高并发里边的分布式事务呢?
首先,我们肯定不会用 2PC 模式、 TCC-事务补偿性方案,我们也不考虑

最终我们选择了可靠消息+最终一致性这种方式

为了保证高并发,订单这一块还是自己回滚,

库存服务自己怎么回滚?
有两种解决办法

第一种
我们在提交订单那里,当捕捉到异常要回滚的时候,给库存服务发一个消息,让库存服务自己把库存解锁

这样不需要让库存事务回滚,只需要给它发一个消息,不会损失什么性能

第二种
库存服务本身也可以使用自动解锁模式。

怎么自动解锁呢?

需要使用消息队列来完成。
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第44张图片
如果你想让我这的哪些库存解锁,首先你需要给我发一个消息告诉我。

然后我们专门的库存解锁服务,去来订阅我们stock.release.stock.queue这个队列里的消息。

那你给我发消息的时候,比如:用路由键stock.release,我知道要库存解锁,

然后,你的消息发给我们这个交换机stock-event-exchange

交换机把这个消息路由给stock.release.stock.queue这个队列。

然后,这个队列stock.release.stock.queue里边存的这些消息都是库存要解锁的消息,我们的库存解锁服务只要收到了,它就会在后台慢慢的解锁消息。

我们不用保证强一致,我们哪怕是二十分钟、三十分钟,乃至于一天以后把这个库存解锁了,最终一致了就行。

所以我们可以来使用消息队列来完成我们的这个最终一致性。

尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第45张图片
我们想要锁库存的话,我们先来保存一个库存工作单和库存工作单详情

相当于只要我们想要锁库存,我们先给数据库里边保存记录,我要锁库存。

接下来我们就来进行锁,只要锁成功了,那一切ok。

如果锁失败了,数据库里边相当于没有这个锁库存记录。

因为锁失败呢,我们这个本身自己所失败会全部回滚。

但如果可能是这种失败,比如我们来到订单里边,我们库存其实自己锁成功了。但是我们订单下边的其他完了,然后库存要进行解锁。那怎么办呢?

我们可以使用定时任务

订单服务的完整消息队列

尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第46张图片
库存微服务,有一个它的库存交换机stock-event-exchange.

如果想要解锁库存,应该是这样的。

首先订单创建成功之后,库存锁定成功,然后发一个消息给交换机,

这个消息里面的内容有订单编号、仓库编号、哪个商品锁了几个库存,

这个交换机,绑定了两个队列,

一个是按照stock.release.#模糊匹配的路由键绑定的stock.release.stock.queue队列

一个是stock.delay.queue队列

第一次发的库存锁定成功的消息,先使用路由键叫stock.locked

交换机按照这个路由键,找到stock.delay.queue延时队列

延时队列50分钟以后,用stock.release这个路由键,将死信交给库存交换机stock-event-exchange

交换机收到以后,按照这个路由键查找,发现stock.release.#这个模糊匹配的路由键跟它是一样的,然后被交换机路由到我们这个stock.release.stock.queue队列。

接下来的解锁库存服务,专门来处理stock.release.stock.queue里的消息。

定时关闭订单

尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第47张图片
首先订单创建成功之后,使用order.create.order路由键将消息路由到order-event-exchange交换机

交换机发现order.create.order这个路由键绑定的是order.delay.queue这个延时队列,然后就把它放到order.delay.queue队列里

过了30分钟,这个延时队列里面的消息,也就是死信,通过order.release.order又路由回order-event-exchange交换机

然后交换机发现这个路由键对应的是order.release.order.queue这个队列,然后就放到order.release.order.queue这个队列里

最终监听order.release.order.queue这个队列的释放订单服务,发现有消息进来了,就会针对里面的数据对其进行关闭订单
问题
这种关闭订单方式会有一些问题

假设订单创建成功之后,订单服务的机器由于卡顿、消息延迟等原因,导致订单未及时取消

此时库存服务的逻辑是订单创建成功之后,它自己会发一个消息,等 40分钟 以后检查之前下单的订单是否已取消,如果是已取消,则解锁库存

结果,库存服务过来查询时,订单服务由于上述原因没有将订单修改为已取消,所以库存就不会解锁,此时的库存消息就算是消费了

等库存服务都检查完了,此时的订单服务才反应过来,然后把订单状态改为已取消了,但是此时库存服务会再有任何的操作了,因为检查订单的消息已经被消费了,库存永远得不到解锁。
解决
为了解决这个问题,我们在监听取消订单的消息时,再发一个消息,主动解锁库存。

主动解锁库存
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第48张图片
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第49张图片

具体是这样的,在释放订单之后,我们主动发一个消息解锁库存,

使用order.release.other将消息路由到交换机,

交换机根据order.release.other.#匹配到stock.release.stock.queue这个队列,并将消息发了过去,

库存服务有对这个队列进行监听,所有一旦有数据来了,就会对其进行解锁库存服务

秒杀系统流程

秒杀一:
秒杀只是做了秒杀的价格优惠信息,整个流程还是常规流程,加购物车,去结算,去支付
流量分散,不过分散到其他系统,如果真崩溃了,多个服务崩溃。本项目暂不考虑这种

尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第50张图片
秒杀二:
应该超过并发的流量
高并发有三宝:缓存、异步、队排好
controller到service到各种逻辑处理后的秒杀成功或者失败,都没有操作过数据库,没有做过远程调用,流程很快,只需要校验合法性
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第51张图片

后台上架秒杀商品(设定场次、商品等)
秒杀商品提前上架,上架到缓存中,商品所有数据从缓存中拿,数据库压力减小
库存处理也不应直接扣数据库,库存也上架到缓存
保存的商品信息包括两个部分

  • 名字就是活动信息(最近几天有哪些活动) + key就是开始时间和结束时间,我们判断当前时间是否在这个区间,从而判定时候在秒杀 + value 当前活动所有参与的商品
  • 商品的详细信息 单独做一个缓存

自动生成随机码作用:知道商品id了 写个工具无限发恶意请求来秒杀 秒杀一开 大概率就抢到了,破坏公平性
随机码在秒杀开始那一刻再暴露出来(到了秒杀时间,加入购物车的按钮重写为抢购按钮,随机码也写进这个按钮,点击这个按钮也会把随机码信息附带到请求里),商品id + 随机码 才能秒杀 加强公平性

请求来了,我们需要减redis 计数,每个商品都应该设置库存信号量
信号量作用:限流,只限制这么多的流量进来。
信号量的值就是商品可以秒杀的库存量
信号量名称也采用 商品id + 随机码 随机码是秒杀开始生成的 此时匹配成功 说明请求是合法的。如果没随机码,只有id判定,秒杀还没开始也会扣信号量了。

商品上前一台服务器和它的副本服务器们 都设定了定时 上架商品,不过我们只应该上架一次,分布式锁可以解决上架问题,随后我们需要处理商品上架的幂等性问题。
如果redis中 有某个商品的key 说明已经上架 否则 上架设置信号量(如果多次上架,某个定时任务慢,就会刷新商品数量,超卖)
尚硅谷-谷粒商城-电商项目-秒杀系统-笔记_第52张图片

幂等性

1 什么是幂等性

接口幂等性就是用户对同一操作发起的一次请求和多次请求结果是一致的,不会因为多次点击而产生了副作用,比如支付场景,用户购买了商品,支付扣款成功,但是返回结果的时候出现了网络异常,此时钱已经扣了,用户再次点击按钮,此时就会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。。。这就没有保证接口幂等性
2 那些情况需要防止
用户多次点击按钮
用户页面回退再次提交
微服务互相调用,由于网络问题,导致请求失败,feign触发重试机制
其他业务情况
3 什么情况下需要幂等
以 SQL 为例,有些操作时天然幂等
SELECT * FROM table WHERE id =? 无论执行多少次都不会改变状态是天然的幂等
UPDATE tab1 SET col1=1 WHERE col2=2 无论执行成功多少状态都是一致的,也是幂等操作
delete from user where userid=1 多次操作,结果一样,具备幂等
insert into user(userid,name) values(1,’ a’ ) 如userid为唯一主键,即重复上面的业务,只会插入一条用户记录,具备幂等


UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次执行的结果都会发生变化,不是幂等的。insert into user(userid,name) values(,a")如userid不是主键,可以重复,那上面业务多次操作,数据都会新增多条,不具备幂等性。
4 幂等解决方案

4.1 token机制
1.服务端提供了发生token的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去
获取token,服务器会把token保存到redis中。
2.然后在调用业务接口请求时,把token携带过去,一般放在请求头部。
3.服务器判断token是否存在于redis中,存在表示第一次请求,然后删除token,继续执行业务
4.如果判断token不存在redis中,就表示是重复操作,直接返回重复标记给客户端,这样就保证了业务代码,不被重复执行

危险性:
1、先删除token还是后删除token
    如果是后删除令牌,用户点提交订单,连续点两次,第一次提交订单,还没执行完,还没来得及删除令牌,
    第二次的提交订单就进来,这样就出现数据不一致的问题了。

    如果是先删除令牌,一般分布式下,token存redis里,假设用户还是点的很快,连续点了两次提价订单,
    两个请求同时去redis里获取token,同时对比成功,同时删令牌,同时执行业务逻辑,这就会出现一定的
    风险

    因此,如果是先删除令牌,获取redis里的令牌操作,和前端带来的token比较的操作以及删除令牌的操作,
    即:获取,对比,删除 这三个操作必须是原子的,给上个分布式锁

4.2 各种锁机制
1、数据库悲观锁
select * from XXX where id=1 for update;
悲观锁使用时伴随着事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用。另外要注意的是,id
字段一定是主键或者唯一索引,不然可能造成锁表的结果,处理起来非常麻烦。
2、数据库乐观锁
这种方式适合在更新的场景中。
3、业务层分布式锁
如果多个机器可能在同一时间同时处理相同的数据,比如多台机器定时任务都拿到了相同数据处理,我们就可以
加分布式锁,锁定此数据,处理完成后释放锁。获取到锁的必须先判断这个数据是否被处理过。
4.3 各种唯一约束
1、数据库唯一约束
插入数据,应该按照唯一索引进行插入,比如:订单号,相同的订单号就不能有两条记录插入。
我们在数据库层面防止重复。
这个机制是利用了数据库的主键唯一约束的特性,解决了在insert场景时幂等问题。
但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键。
如果是分库分表场景下,路由规则要保证相同请求下,落在同一个数据库和同一表中,要不然数据库约束就不起
效果了,因为是不同的数据库和表主键不想关
2、redis set防重
很多数据需要处理,只能被处理一次,比如我们可以计算数据的MD5将其放入redis的set,每次处理数据,先看
md5是否存在,存在就不处理。
4.4 防重表
使用订单号orderNo做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且他们在同一个事务中,
这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。注意,去重表和业务表应该在
同一数据库中,这样就保证了在同一个事务,即使业务操作失败,也会把去重表的数据回滚。这个很好的保证了数
据一致性。
4.5 全局请求唯一id
调用接口时,生成一个唯一id,redis将数据保存到集合中(去重),存在即处理过,可以使用nginx设置每一个
请求的唯一id proxy_set_header X-Request-Id $request_id;

1、token 机制

比如 12306购票的验证码就是token,当此成功才OK , 错误 或者上一次验证码都不行
1、服务端提供了发送 token 的接口,我们在分析业务的时候,哪些业务是存在幂等性问题的,就必须在执行业务前,先获取 token,服务器会把 token 保存到 redis 中
2、然后调用业务接口请求时, 把 token 携带过去,一般放在请求头部
3、服务器判断 token 是否存在 redis,存在表示第一次请求,然后删除 token,继续执行业务
4、如果判断 token 不存在 redis 中,就表示重复操作,直接返回重复标记给 client,这样就保证了业务代码,不被重复执行
危险性:
1、先删除 token 还是后删除 token:

  1. 先删除可能导致,业务确实没有执行,重试还得带上之前的 token, 由于防重设计导致,请求还是不能执行
  2. 后删除可能导致,业务处理成功,但是服务闪断,出现超时,没有删除掉token,别人继续重试,导致业务被执行两次
  3. 我们最后设计为先删除 token,如果业务调用失败,就重新获取 token 再次请求
    2、Token 获取,比较 和删除 三个操作一起的,必须是原子性
  4. redis.get(token),token.equals、redis.del(token),如果说这两个操作都不是原子,可能导致,在高并发下,都 get 同样的数据,判断都成功,继续业务并发执行
  5. 可以在 redis 使用 lua 脚本完成这个操作
"if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"

定时任务
框架:quartz (spring 原生的timer也支持定时任务)
什么时候执行,需要执行计划 ,cron表达式设定执行时间(按规则写,或者网上在线生成器)
秒杀商品上架流程:
设置定时任务上架需要秒杀的商品(分布式锁防止重复上架)->封装最新的秒杀商品信息到redis中 商品的基本信息,商品的随机码,结束时间->设置秒杀商品的分布式信号量作为库存扣减的信息->结束
秒杀:
立即抢购->判定登录->合法性校验:秒杀时间,随机码,对应关系,幂等性->获取信号量->创建秒杀号->用户->订单号->商品->发送MQ消息->收货地址确认->支付确认 结束
项目杂记
页面显示三级分类:一级分类查出二级分类数据,二级分类中查询出三级分类数据
spu :Standard product unit 标准化产品单元,可以理解为类 Iphone X 是 spu
sku:stock keeping unit 库存量单元 phone X 64G 黑 是sku

你可能感兴趣的:(java)