项目主要实现了一个模拟电商的分布式秒杀系统,核心模块包括注册登录模块、订单模块、秒杀模块。
框架是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请求就等两秒再转发请求
限制次数,限制总量,快速失败降级运行,熔断隔离防止雪崩
1、什么是SSO
单点登录(SingleSignOn,SSO),就是通过用户的一次性鉴别登录。当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。这种方式减少了由登录产生的时间消耗,辅助了用户管理,是目前比较流行的。(新浪旗下,微博 视频 体育啥的 域名都不一样,springsession不能解决)
传统的session弊端:
JWT(JSON Web Token)
JWT的本质就是一个字符串,有签名信息,接收后可以校验是否被篡改,所以可以用于在各方之间安全地将信息作为Json对象传输。JWT认证流程如下:
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方法需要验证用户登录,在方法上加入自定义的注解。元注解考点涉及
注册
@NotEmpty
,限制长度@Length
。部署虚拟机中,作用:反向代理+负载均衡+动静分离+限流
反向代理
对于集群非常需要,集群在内网部署,不能暴露它的外网ip,容易被引起攻击,前置一个服务器(比如nginx,有一个公网IP),搭建域名访问环境,可以代理外部网络主机访问内部网络。(正向反向是相对于我们自己电脑,科学上网属于正向代理)
电脑host文件配置域名映射,请求先访问到nginx,niginx请求负载均衡交给网关,网关从注册中心动态发现商品在哪儿,网关再负载均衡到商品服务
Nginx 有哪些负载均衡策略?
限流
缓存、降级和限流是开发高并发系统的三把利器。
常用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使用(支持)相同的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)很快
压力测试考察当前软硬件环境下系统所能承受住的最大负荷并帮助找出系统的瓶颈所在,压测都是为了系统在线上的处理能力和稳定性维持在一个标准范围内,做到心中有数。使用压力测试,我们有希望找到很多种用其他测试方法更难发现的错误(百万级请求量下更明显),有两种错误类型是:内存泄漏:对象未复用,不停消耗内存/对象申请完内存后无法合理释放内存、并发与同步
有效的压力测试系统将应用以下这些关键条件:重复、并发、量级、随机变化
压力测试软件:JMeter。作用:可以实现对服务器、网络、对象模拟巨大的负载,测试强度和分析整体性能
性能测试主要关注:
吞吐量:每秒系统能处理的请求数、任务数。 QPS TPS
响应时间:服务器处理一个请求或者一个任务的耗时。最大相应时间、最少相应时间、90%相应时间
错误率:一批请求中结果出错的请求所占比列(自己的端口被占完了,改端口占有时间分配)
并发量存在的瓶颈在哪
调优:
是分布式的全文搜索引擎,解决海量数据中查询效率地下的问题
为啥要使用:因为在商城中的数据,将来会非常多,所以采用以往的模糊查询,会放弃索引,导致全表查询,效率很低,如果用ES做一个全文索引,可以提高查询速度。
es采用倒排索引;如查找某个字符, 正排索引是遍历所有的内容, 看哪些里面含有要查找的内容,而倒排索引是将文章内容进行分词之后, 类似于以词作为key, 文章的索引作为value来存储, 搜索的时候就可以以关键字作为key来获取,通过得分返回最佳结果。Elastic 是 Lunce 的封装,提供了 REST API 的操作接口,开箱即用。
索引->数据库,类型->表(已删除类型),文档->行,属性->列
kibana ,es的可视化界面
哪些数据适合放入缓存?
缓存问题
击穿、雪崩、穿透、缓存与数据库数据不一致
业务流程
缓存数据一致性 - 双写模式
两个线程写 最终只有一个线程写成功,后写成功的会把之前写的数据给覆盖,这就会造成脏数据
缓存数据一致性 - 失效模式
三个连接 ,一号连接 写数据库 然后删缓存、二号连接 写数据库时网络连接慢,还没有写入成功、三号链接 直接读取数据,读到的是一号连接写入的数据,此时二号链接写入数据成功并删除了缓存,三号开始更新缓存发现更新的是二号的缓存
无论是双写模式还是失效模式,都会到这缓存不一致的问题,即多个实例同时更新会出事,怎么办?
先更新数据库,后更新缓存
先更新缓存,后更新数据库
先删除缓存,后更新数据库
先更新数据库,后删除缓存
第一种和第二种方案,没有人使用的,因为第一种方案存在问题是:并发更新数据库场景下,会将脏数据刷到缓存。
第二种方案存在的问题是:如果先更新缓存成功,但是数据库更新失败,则肯定会造成数据不一致。
目前主要用第三和第四种方案。
先删除缓存,后更新数据库:
先更新数据库,后删除缓存
高并发系统中 每一个请求都new thread start 很快资源就耗尽了,所以为了控制系统资源 要用线程池,所以异步任务都用线程池,线程池管理线程
面试
CompletableFuture
类 异步编排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中提取这些信息,当然这里面有了安全和性能问题需要我们考虑了
我们在auth.gulimall.com中保存session,但是网址跳转到 gulimall.com中,取不出auth.gulimall.com中保存的session,这就造成了微服务下的session不同步问题
0、Session同步解决方案-分布式下session共享问题
同一个服务复制多个,但是session还是只能在一个服务上保存,浏览器也是只能读取到一个服务的session
1、Session共享问题解决-session复制
2、Session共享问题解决-客户端存储
3、Session共享问题解决-hash一致性
4、Session共享问题解决-统一存储
方案3和4 都可以。
5、子域共享问题
SpringSession框架采用5和放大作用域解决这两个问题 。
异步处理:消息发送的时间取决于业务执行的最长的时间
应用解耦:原本是需要订单系统直接调用库存系统
只需要将请求发送给消息队列,其他的就不需要去处理了,节省了处理业务逻辑的时间
流量消峰:某一时刻如果请求特别的大,那就先把它放入消息队列,从而达到流量消峰的作用
实现最终一致性的时候很必要的一个工具
RabbitMQ简介:
RabbitMQ是一由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现
核心概念
Message
消息,消息是不具名的,它是由消息头和消息体组成,消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括 routing - key (路由键),priority(相对于其他消息的优先权),delivery - mode(指出该消息可能需要持久性存储)等
Publisher
消息的生产者,也是一个像交换器发布消息的客户端应用程序
Exchange
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列
Exchange有4种类型:direct(默认)、fanout、topic,和heades,不同类型的 Exchange 转发消息的策略有所区别
Queue
消息队列,用来保存消息直到发送给消费者,他是消息的容器,也是消息的重点,一个消息可以投入一个或多个队列,消息一直在队列里面,等待消费者连接到这个队列将其取走
Binding
绑定,用于消息队列和交换器之间的关联,一个绑定就是基于路由键将交换器和消息队列连接起来的规则,所有可以将交换器理解成一个由绑定构成的路由表
Connection
网路连接,比如一个TCP连接
Channel
信道,多路复用连接中的一个独立的双向数据流通道,信道是建立在真实的TCP连接的内的虚拟连接,AMQP 命令都是通过信息到发送出去的,不管是发布消息,订阅队列还是接收消息,这些动作都是通过队列完成,因为对应操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条TCP连接
Consumer
消息的消费者,表示一个消息队列中取得消息的客户端应用程序
Virtual Host
虚拟主机,表示交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个Virtual host本质上就是一个 mini 版的RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。Virtual host 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ默认的vhost是/。
Broker
表示消息队列服务器实体
Docker 安装RabbitMQ
RabbitMQ 运行机制
AMQP 中的消息路由
AMQP 中消息的路由过程和 Java 开发者熟悉的 JMS 存在一些差别,AMQP中增加了 Exchange 和 Binding 的角色 生产者把消息发布到 Exchange 上,消息最终到达队列并被消费者接收,而 Binding 决定交换器的消息应该发送给那个队列
Exchange 类型
Exchange 分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、tanout、topic、headers header匹配AMQP消息的 header 而不是路由键,headers 交换器和 direct 交换器完全一致,但性能差能多,目前几乎用不到了,所以直接看另外三种类型
spring.rabbitmq.publisher-confirms=true
在创建 connectionFactory
的时候设置 PublisherConfirms(true) 选项,开启 confirmcallback
。
CorrelationData
用来表示当前消息唯一性
消息只要被 broker 接收到就会执行 confirmCallback,如果 cluster 模式,需要所有 broker 接收到才会调用 confirmCallback
被 broker 接收到只能表示 message 已经到达服务器,并不能保证消息一定会被投递到目标 queue 里,所以需要用到接下来的 returnCallback
spring.rabbitmq.publisher-retuns=true
spring.rabbitmq.template.mandatory=true
confirm 模式只能保证消息到达 broker,不能保证消息准确投递到目标 queue 里。在有些模式业务场景下,我们需要保证消息一定要投递到目标 queue 里,此时就需要用到 return 退回模式
这样如果未能投递到目标 queue 里将调用 returnCallback,可以记录下详细到投递数据,定期的巡检或者自动纠错都需要这些数据
场景:
比如未付款的订单,超过一定时间后,系统自动取消订单并释放占有物品
常用解决方案:
Spring的schedule 定时任务轮询数据库
缺点:
消耗系统内存,增加了数据库的压力,存在较大的时间误差
解决:
rabbitmq的消息TTL和死信Exchange结合
使用场景
时效问题
上一轮扫描刚好扫描,而这个时候刚好下了订单,就没有扫描到,下一轮扫描的时候,订单还没有过期,等到订单过期后30分钟才被扫描到
那就是!TTL+死信队列组合的方式,实现和延迟队列一样的效果。订单系统在用户下单后发送了一条订单信息到MQ交换机交换机把这条消息根据routing key分发到了一个设置了30分钟过期时间队列中,但是该队列没有消费者,所以这些消息终将过期当消息过期变成死信后,会被转发给该队列绑定的死信交换机并根据死信routing key最终被分配到了一个死信队列中死信队列是有消费者的,这些死信消息将被这些消费者获取之后执行对应的业务逻辑
第一种方法是通过队列属性设置,队列中所有消息都有相同的过期时间
1、消息丢失
消息发送出去,由于网络问题没有抵达服务器
消息抵达Broker, Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚未持久化完成,宕机。
自动ACK的状态下。消费者收到消息,但没来得及消费然后宕机
2、消息重复
3、消息积压
分布式系统经常出现的异常
机器宕机、网络异常、消息丢失、消息乱序、不可靠的TCP、存储数据丢失…
分布式事务是企业中集成的一个难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在微服务架构中,几乎是无法避免的
1、CAP 定理
CAP 原则又称 CAP 定理指的是在一个分布式系统中
当发生网络分区的时候,如果我们要继续服务,那么强一致性和可用性只能 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 理论三要素
分布式一致性的 3 种级别:
强一致性 :系统写入了什么,读出来的就是什么。
弱一致性 :不一定可以读取到最新写入的值,也不保证多少时间之后读取到的数据是最新的,只是会尽量保证某个时刻达到数据一致的状态。
最终一致性 :弱一致性的升级版,系统会保证在一定时间内达到数据一致的状态。
1、2PC 模式
分为两个阶段:
第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反应是否可以提交
第二阶段:事务协调器要求每个数据库提交数据
其中,如果有任何一个数据库否认这次提交,那么所有数据库都会要求回滚他们在此事务中的那部分信息
柔性事务 - TCC 事务
刚性事务:遵循ACID原则,强一致性。
柔性事务:遵循BASE理论,最终一致性;
与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。
一阶段 prepare 行为:调用自定义的 prepare 逻辑。
二阶段 commit 行为:调用自定义的 commit 逻辑。
二阶段 rollback行为:调用自定义的 rollback 逻辑。
所谓TCC模式,是指支持把自定义的分支事务纳入到全局事务的管理中。
3、柔性事务 - 最大努力通知型方案
按规律进行通知,不保证数据定能通知成功, 但会提供可查询操作接口进行核对。这种方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种方案也是结合MQ进行实现,例如:通过MQ发送http请求,设置最大通知次数。达到通知次数后即不再通知。
案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询校对、对账文件),支付宝的支付成功异步回调
4、柔性事务 - 可靠信息 + 最终一致性方案(异步通知型)
实现:业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。
电商系列涉及到 3 流,分别为信息流、资金流、物流,而订单系统作为中枢将三者有机的集合起来
订单模块是电商系统的枢纽,在订单这个模块上获取多个模块的数据和信息,同时对这些信息进行加工处理后流向下个环节,这一系列就构成了订单的信息疏通
1、待付款
用户提交订单后,订单进行预下单,需要注意的是待付款状态下可以对库存进行锁定,锁定库存需要配置支付超时时间,超时后将自动取消订单,订单变更关闭状态。
2、已付款/代发货
用户完成订单支付,订单系统需要记录支付时间,支付流水单号便于对账,订单下放到WMS系统,仓库进行调动、配货、分拣,出库等操作
3、待收货/已发货
仓库将商品出库后,订单进入物流环节,订单系统需要同步物流信息,便于用户实时商品的物流状态
4、已完成
用户确认收货后吗,订单交易完成,后续支付则进行计算,如果订单存在问题进入售后状态
5、已取消
付款之前取消订单,包括超时未付款或用户商户取消订单都会产生这种订单状态
6、售后中 项目暂不实现
订单流程是指从订单产生到完成整个流转的过程。而不同的产品类型或业务类型在系统中的流程会千差万别,比如上面提到的线上实物订单和虚拟订单的流程等,不管类型如何订单都包括正向流程和逆向流程,对应的场景就是购买商品和退换货流程,正向流程就是一一个正常的网购步骤:订单生成>支付订单->卖家发货一确认收货>交易成功。
可概括如下图
1、订单创建与支付
2、逆向流程
订单登录拦截
任何请求都需要先经过拦截器的验证,才能去执行目标方法,访问订单的任何请求都需要登录。这里是用户是否登录,用户登录了则放行,否则跳转到登陆页面
订单确认页
假失败就是我们在订单服务调库存服务时, 库存锁定成功,然后由于服务器慢、卡顿、等故障原因,本地事务提交了之后,一直没返回到订单服务
此时再看订单服务,因为调用库存服务时间太长了,库存服务迟迟没有返回结果,可能就会触发 feign 的超时机制,在调用远程服务这里抛异常:read time out 读取超时,但是这个异常并不是我们手动抛的锁库存异常,而是 feign 的异常
并且订单服务,设计的回滚机制,是只要一出现异常就会全部回滚,
结果:库存锁定成功,订单服务因为 feign 的超时机制,出现异常,导致订单数据全部回滚,最终数据不一致
结果积分服务内部出现异常,积分数据回滚
此时再看订单服务,订单服务感知到我们手动抛的积分异常,订单数据回滚,但是库存服务,却不会有任何感知,
结果:积分、订单数据全部回滚,库存给锁定了,也是数据不一致
只需要在订单服务的库存执行成功之后,添加一个 int i = 10 / 0;,模拟积分服务出现异常,很容易就能复现这个问题
总结
本地事务,在分布式系统,只能控制住自己的回滚,控制不了其他服务的回滚
产生分布式事务的最大原因,就是网络问题 + 分布式机器
如何解决下单系统这个高并发里边的分布式事务呢?
首先,我们肯定不会用 2PC 模式、 TCC-事务补偿性方案,我们也不考虑
最终我们选择了可靠消息+最终一致性这种方式
为了保证高并发,订单这一块还是自己回滚,
库存服务自己怎么回滚?
有两种解决办法
第一种
我们在提交订单那里,当捕捉到异常要回滚的时候,给库存服务发一个消息,让库存服务自己把库存解锁
这样不需要让库存事务回滚,只需要给它发一个消息,不会损失什么性能
第二种
库存服务本身也可以使用自动解锁模式。
怎么自动解锁呢?
需要使用消息队列来完成。
如果你想让我这的哪些库存解锁,首先你需要给我发一个消息告诉我。
然后我们专门的库存解锁服务,去来订阅我们stock.release.stock.queue
这个队列里的消息。
那你给我发消息的时候,比如:用路由键stock.release
,我知道要库存解锁,
然后,你的消息发给我们这个交换机stock-event-exchange
。
交换机把这个消息路由给stock.release.stock.queue
这个队列。
然后,这个队列stock.release.stock.queue
里边存的这些消息都是库存要解锁的消息,我们的库存解锁服务只要收到了,它就会在后台慢慢的解锁消息。
我们不用保证强一致,我们哪怕是二十分钟、三十分钟,乃至于一天以后把这个库存解锁了,最终一致了就行。
所以我们可以来使用消息队列来完成我们的这个最终一致性。
我们想要锁库存的话,我们先来保存一个库存工作单和库存工作单详情
相当于只要我们想要锁库存,我们先给数据库里边保存记录,我要锁库存。
接下来我们就来进行锁,只要锁成功了,那一切ok。
如果锁失败了,数据库里边相当于没有这个锁库存记录。
因为锁失败呢,我们这个本身自己所失败会全部回滚。
但如果可能是这种失败,比如我们来到订单里边,我们库存其实自己锁成功了。但是我们订单下边的其他完了,然后库存要进行解锁。那怎么办呢?
我们可以使用定时任务
库存微服务,有一个它的库存交换机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
里的消息。
首先订单创建成功之后,使用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分钟 以后检查之前下单的订单是否已取消,如果是已取消,则解锁库存
结果,库存服务过来查询时,订单服务由于上述原因没有将订单修改为已取消,所以库存就不会解锁,此时的库存消息就算是消费了
等库存服务都检查完了,此时的订单服务才反应过来,然后把订单状态改为已取消了,但是此时库存服务会再有任何的操作了,因为检查订单的消息已经被消费了,库存永远得不到解锁。
解决
为了解决这个问题,我们在监听取消订单的消息时,再发一个消息,主动解锁库存。
具体是这样的,在释放订单之后,我们主动发一个消息解锁库存,
使用order.release.other
将消息路由到交换机,
交换机根据order.release.other
.#匹配到stock.release.stock.queue
这个队列,并将消息发了过去,
库存服务有对这个队列进行监听,所有一旦有数据来了,就会对其进行解锁库存服务
秒杀一:
秒杀只是做了秒杀的价格优惠信息,整个流程还是常规流程,加购物车,去结算,去支付
流量分散,不过分散到其他系统,如果真崩溃了,多个服务崩溃。本项目暂不考虑这种
秒杀二:
应该超过并发的流量
高并发有三宝:缓存、异步、队排好
controller到service到各种逻辑处理后的秒杀成功或者失败,都没有操作过数据库,没有做过远程调用,流程很快,只需要校验合法性
后台上架秒杀商品(设定场次、商品等)
秒杀商品提前上架,上架到缓存中,商品所有数据从缓存中拿,数据库压力减小
库存处理也不应直接扣数据库,库存也上架到缓存
保存的商品信息包括两个部分
自动生成随机码作用:知道商品id了 写个工具无限发恶意请求来秒杀 秒杀一开 大概率就抢到了,破坏公平性
随机码在秒杀开始那一刻再暴露出来(到了秒杀时间,加入购物车的按钮重写为抢购按钮,随机码也写进这个按钮,点击这个按钮也会把随机码信息附带到请求里),商品id + 随机码 才能秒杀 加强公平性
请求来了,我们需要减redis 计数,每个商品都应该设置库存信号量
信号量作用:限流,只限制这么多的流量进来。
信号量的值就是商品可以秒杀的库存量
信号量名称也采用 商品id + 随机码 随机码是秒杀开始生成的 此时匹配成功 说明请求是合法的。如果没随机码,只有id判定,秒杀还没开始也会扣信号量了。
商品上前一台服务器和它的副本服务器们 都设定了定时 上架商品,不过我们只应该上架一次,分布式锁可以解决上架问题,随后我们需要处理商品上架的幂等性问题。
如果redis中 有某个商品的key 说明已经上架 否则 上架设置信号量(如果多次上架,某个定时任务慢,就会刷新商品数量,超卖)
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;
比如 12306购票的验证码就是token,当此成功才OK , 错误 或者上一次验证码都不行
1、服务端提供了发送 token
的接口,我们在分析业务的时候,哪些业务是存在幂等性问题的,就必须在执行业务前,先获取 token
,服务器会把 token
保存到 redis 中
2、然后调用业务接口请求时, 把 token
携带过去,一般放在请求头部
3、服务器判断 token
是否存在 redis
,存在表示第一次请求,然后删除 token
,继续执行业务
4、如果判断 token
不存在 redis
中,就表示重复操作,直接返回重复标记给 client
,这样就保证了业务代码,不被重复执行
危险性:
1、先删除 token 还是后删除 token:
"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