什么是秒杀(seckill)
seckill是一个老生常谈的场景
它一般出现在电商系统中,在某些特定的节日,限定特定商品数量以超低折扣进行促销引流
按照秒杀的特性,特价商品一般在一两秒内被抢光,剩下的人只会出现售罄页面
这一两秒会出现一个瞬间峰值,因为是短暂的活动,不能消耗太多服务器资源,所以需要达到最小代价做到最大的抗压,不直接冲垮服务器,还得保证不超卖,不丢单,不宕机等问题
seckill需要注意哪些问题
幂等
多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致
描述
- 前端重复提交:前端瞬间点击多次造成表单重复提交
- 接口超时重试:接口可能会因为某些原因而调用失败,处于容错性考虑会加上失败重试的机制。如果接口调用一半,再次调用就会因为脏数据的存在而产生异常
- 消息重复消费:在使用消息中间件来处理消息队列,且手动ack确认消息被正常消费时。如果消费者突然断开链接,那么已经执行了一半的消息会重新放回队列。被其他消费者重新消费时就会导致结果异常,如数据库重复数据, 数据库数据冲突,资源重复等
- 请求重发:网络抖动引发的nginx重发请求,造成重复调用
解决方案
- 给前端点击按钮设置点击置灰,等获取到正确结果再进行对应的处理【治标不治本,容易被绕过】
- 使用Token机制
通过Token机制实现接口的幂等性,这是一种比较通用性的实现方法
- 客户端会先发送一个请求去获取Token,服务端会生成一个全局唯一的UUID作为Token保存在Redis中,同时把这个UUID返回给客户端,并缓存到session
- 客户端第二次调用业务请求的时候必须携带这个Token
- 服务端会校验这个Token,如果校验成功,则执行业务,并删除Redis中的Token
- 如果校验失败,说明Redis中已经没有对应的Token,则表示重复操作,直接返回指定的结果给客户端
注意:
- 对 redis 中是否存在 token 以及删除的代码逻辑建议用 Lua 脚本或者setNX操作实现,保证原子性
- 全局唯一 ID 可以用百度的 uid-generator、美团的 Leaf 去生成,也可以用雪花算法实现
削峰(高并发)
对于秒杀系统瞬时会有大量用户涌入,所以在抢购一开始会有很高的瞬间峰值
高峰值流量是压垮系统很重要的原因【会造成雪崩效应】,所以如何把瞬间的高流量变成一段时间平稳的流量也是设计秒杀系统很重要的思路
常见削峰方案预约制:用户在提前固定时间内预约使用服务,在预约时间内服务繁忙时不再接受新的请求,以此来削峰。问答式验证:在请求访问服务时要求用户回答一些简单的问题,以此来减少恶意请求,并降低系统的峰值压力。动态限流:根据实时的系统负载情况动态调整请求的限流策略,以此来减少系统的峰值压力。动态分配资源:根据实时的系统负载情况动态分配资源,以此来减少系统的峰值压力。分段限流:将请求分为几个段,分别对每个段设置不同的限流策略,以此来减少系统的峰值压力。
描述
- 服务雪崩:服务提供者不可用导致服务调用者也跟着不可用,以此类推引起整个链路中的所有微服务都不可用
- 服务异步:异步处理不用阻塞当前线程来等待处理完成,而是允许后续操作,直至其它线程将处理完成,并回调通知此线程
- 资源控制:将整体流程中的资源调度进行控制,扬长避短,防止加载图片,js等静态资源增加服务器压力
解决方案
首先了解一下什么是CDN
平时用户访问的地址是直接指向我们的服务器,中间会经过多层转发,而距离比较远的用户访问我们的服务器可能会有网络拥堵
CDN的服务商在各地都有服务器群,可以解析DNS计算距离用户最近的服务器来获取缓存资源,以避免网络拥堵
我们可以把静态资源(图片,js,css等)丢到CDN服务商上面,以减少我们的服务器压力【解耦】
使用CDN的好处就是不浪费自己服务器资源和带宽,并且响应速度快,直接实现了动静分离
现在由于市场需求量大技术日渐成熟,CDN的收费也越来越便宜
传统动静分离
一般做动静分离主要是为了提高用户访问静态资源的速度,降低直接对后台应用的访问
最基本的做法就是用nginx来指向静态资源,因为nginx的吞吐量比tomcat高,响应更快
具体可以参考:
负载均衡
负载均衡的目的是将请求分摊到多台服务器上,从而降低单台服务器的负载,提高系统的吞吐量和响应速度。
通常在 Web 应用、数据库应用、邮件服务器、文件服务器等高并发环境中使用负载均衡技术
常用的负载一般是Nginx,Netflix Ribbon,Netflix Eureka,Apache Zookeeper
如果有钱也可以买云服务商的,比如亚马逊,微软,谷歌,阿里巴巴,腾讯云等
限流
网关过滤方案
对系统而言,如果我们可以在网关层面拦截掉用户请求,可以说这个方案的性价比很高。要是能在这层过滤 95% 以上的请求,整个系统也就很稳定
- 限定每个用户的访问频率:比如几秒钟可以下单一次
- 限定每个ip的访问频率:这种方式担心有些人通过脚本下单,因此错杀真实用户
算法过滤方案
在秒杀模块中,一般使用令牌桶算法或者固定窗口算法
一.令牌桶
每次请求需要消耗一个令牌,令牌生成速率是固定的,当令牌桶满时,请求将被阻塞或拒绝
可以通过Google Guava 的 RateLimiter 实现了令牌桶算法,它提供了一种简单的方式来限流RateLimiter 支持两种限流方式:
- 固定速率限流:固定速率限流是指固定每秒生成令牌的速率。例如,当令牌生成速率为 10 个每秒时,第一秒可以生成 10 个令牌,第二秒也可以生成 10 个令牌,以此类推。
- SmoothBursty 限流:SmoothBursty 限流是一种更加灵活的限流方式,它支持在短时间内的高速率限流,在长时间内的更加缓慢的限流。这种算法适用于那些具有短时间内的突发请求和长时间内的平稳请求的场景。例如,在一个时间段内可以生成大量令牌,但在另一个时间段内生成的令牌数可能会变少。
SmoothBursty 限流算法通过适当地调整令牌生成速率,以适应系统的需求情况,并同时保证在令牌桶中令牌的有效利用。它可以帮助实现更好的资源利用率,同时保证系统的稳定性。
二.漏桶算法
将请求缓存到漏桶中,漏桶的容量有限,请求以固定速率从漏桶中流出
三.滑动窗口
在一段时间内,对请求数量进行限制,并随时间窗口的滑动而改变限制的数量
四.固定窗口
在固定时间窗口内,对请求数量进行限制
五.随机算法
随机接受或者拒绝
注意:
漏桶算法和固定窗口算法都是限流算法,但它们有一些重要的差异:
- 原理:漏桶算法基于等待请求的思想,即请求被存储在漏桶中直到漏桶中有足够的令牌来处理请求。而固定窗口算法则是通过计算在固定时间窗口内处理的请求数来限制请求数量。
- 实现方式:漏桶算法通过设置固定的令牌生成速率和固定的漏桶容量来实现限流。固定窗口算法则是通过维护一个请求数的计数器,并在固定的时间窗口内重置该计数器来实现限流。
- 应用场景:漏桶算法适用于处理突发请求,并且请求处理速率相对稳定的场景。固定窗口算法则适用于请求处理速率需要在固定时间窗口内动态调整的场景。
总的来说,漏桶算法和固定窗口算法都是有效的限流算法,选择哪种算法取决于应用场景和限流需求。如果需要处理突发请求,漏桶算法可能更合适;如果需要动态调整请求处理速率,则固定窗口算法可能更合适。
MQ削峰、异步、解耦
为什么使用MQ进行削峰操作
- 处理高流量请求:当系统请求峰值很高时,可以将所有的请求投递到消息队列中,让后台异步处理请求,减少系统的峰值压力(缓冲请求)
- 可靠性:能够有效避免瞬时请求过量,导致的系统不稳定或者数据丢失(系统故障时候也不会丢失消息)
- 扩展性:可以有效将请求分散给多个系统处理,提高系统的处理能力,如果消费不过来可以临时加消费者进行处理
- 可用性提高:因为是异步处理请求,即使在请求量激增情况也不会出现系统故障,可以灵活调整队列长度和消息生产速率来控制系统消费速度
常见的MQ有ActiveMQ,RocketMQ,RabbitMQ和Kafka
使用MQ所需要注意的问题
- 队列管理:需要管理好队列的长度,避免过长导致的内存溢出
- 消息可靠性:需要设置消息的持久化,重试等机制
- 消息一致性:需要避免重复消费或者消息丢失
- 消息处理能力:需要根据实际需求调整生产速率和消费速率
- 资源利用率:需要监控系统资源使用情况,防止宕机
出现死信队列怎么办
死信队列一般是某些消息无法在规定时间内正常处理
- 检查队列的配置:检查消息的存活时间和队列大小,确保它们有足够的内存保存待处理消息
- 跟踪消息:查看日志和监控工具,跟踪消息处理的情况,定位问题
- 重试:对于一些可以重试的消息,用重试机制重新消费
- 恢复数据:在极端情况下,如果遇到无法处理消息,考虑从备份数据中恢复数据
- 人工干预:对于特定消息,自动处理不了,就人工干预消费处理
MQ消费不过来怎么办
出现这种情况一般是消息生产过快或者消费端处理能力不足,导致消息积压
- 提高消费端的处理能力:增加机器数量,配置更高的计算资源,代码调优等
- 减缓生产速度:可以限制生产者的生产速度,用令牌桶或者漏桶来限制,避免消息积压
- 长队列:可以通过分治队列,降低单队列的负荷,缩短消息处理时间
- 设置正确的队列长度限制:避免队列过长导致的积压【一般和这个无关】
MQ可靠性消息投递【高可靠必定牺牲性能】
- ack+重试机制【目前常用】
通过配置confirm的发送确认机制,当消息发送到消费者并且确认收到消息,通知生产者该消息已消费
- 消息多备
在生产者或者broker或者消费者保留一份,当消费者消费成功后再删除【 在消息的传输链路上每个节点冗余消息 】
如果 Broker 是集群部署,有多副本机制,即消息不仅仅要写入当前 Broker ,还需要写入副本机中。那配置成至少写入两台机子后再给生产者响应。这样基本上就能保证存储的可靠了。一台挂了另一台还在。
- 事务机制【太消耗性能】
1.将channel设置为事务模式
channel.txSelect();
2.提交事务
channel.txCommit();
3.事务回滚
channel.txRollback();
如果消息入队列后MQ宕机此时就需要:持久化,不止消息的持久化,还有队列和Exchange的持久化
MQ出现重复消费怎么办
- 做好幂等性
- 失败一定次数就通知人工处理
缓存
秒杀是一个典型的读多写少的应用场景,非常适用缓存
【一件商品只有10件的库存,有200w人来抢,实际就只有10个人可以下单成功,其它的都是查询库存】
缓存可以在秒杀前先缓存商品信息,库存信息和预热用户对该商品的请求,这样可以防止请求直接打到数据库里面,从而提高系统的效率
对于读请求,无论是 Memcached 还是 Redis ,单机抗10wQPS问题都不大,在上层的限流,削峰,人机校验,幂等中已经筛选掉了90%的请求,所以真正能打入数据库的数据量是很少的
基于Redis实现缓存 Cache-Aside Pattern(旁路缓存模式)
在请求达到后端之后,防止请求直接面向传统数据库【RDBMS】(传统数据库处理读写效果比较慢),一般会做一层NoSQL缓存操作。
先去Redis找有没有数据,如果有就返回,没有就去RDBMS里面查询数据,然后存入Redis并返回,方便下次直接从Redis拿到数据
如何保证数据一致性
**强一致性:**这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大
**弱一致性:**这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态
**最终一致性:**最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较推崇的模型
- 先更新缓存再更新数据库【脏数据】
线程1:先更新缓存成功,但是网络原因写数据库失败,就会导致缓存是最新数据,而数据库的数据为旧数据,那缓存就是脏数据
线程2:读取缓存中数据,而这个数据数据库中却不存在,数据库都不存在的数据,缓存并返回客户端就毫无意义了。
- 先更新数据库,再更新缓存【存在线程安全问题】
线程1:先更新数据库成功,但是由于网络卡顿更新缓存失败,从而导致缓存中的数据为旧数据
线程2:从缓存中读取数据,缓存中的数据为旧数,从而导致数据库与缓存数据不一致。
- 先删除缓存,再更新数据【线程安全问题】
线程1:先删除缓存成功,但是由于网络卡顿原因,更新数据库异常。
线程2:读取缓存由于缓存数据为空,则会查询数据库中的数据,查询成功并写入缓存,从而导致数据不一致。
- 先更新数据库,再删除缓存【短暂不一致问题】
线程1:写入数据库成功,由于网络卡顿原因,导致删除缓存数据失败
线程2:读取数据,读取为缓存中的数据,但是当网络恢复正常后,缓存中的数据会被删除,所以可能会存在短暂的数据不一致
- 延时双删
先删除缓存,再更新数据库,确保数据库事务提交成功,然后休眠一段时间在删除缓存。
我们都知道第三种情况是因为网络卡顿导致数据库更新失败,当网络恢复正常后,我们在执行更新数据库操作,然后再删除缓存,那么出现数据不一致的情况也就是在休眠的这短暂的时间内
- 双删+队列重试机制
在延时双删的方案上引入队列,需要删除失败的key存入消息队列中,采用异步的方式来进行删除,如果删除失败的次数已经超过了最大次数,发送警告邮件,需要人工介入解决【重试机制】
- 监听binlog日志删除
以 mysql 为例 可以使用阿里的 canal 将 binlog 日志采集发送到 MQ 队列里面,然后通过 ACK 机制确认处理这条更新消息,删除缓存,保证数据缓存一致性
如何防止穿透,击穿,雪崩
缓存穿透:请求在redis没命中缓存,在mysql也没有命中,查询失败,当请求量大的时候,一直有这种请求进来就会给数据库很大压力
1.使用布隆过滤器【如果布隆过滤器存在就进行请求,不存在直接进行空值或者失败】
2.缓存空对象,并且设置过期时间【缓存失效不宜过长,当数据库数据被写入时候要及时刷新,避免数据不一致的情况出现】
3.非法请求校验【前端页面和后台请求参数校验,避免出现非法请求直接打入数据库】
4.黑名单【对于请求频率过高不正常的进行黑名单限制】
缓存击穿:大量热点key值同时失效或者单个热点key,在不停抗着大并发,这个key失效的瞬间,大量的请求就会击破缓存,打入数据库
1.使用互斥锁【建议使用Redission分布式锁】,单机可以通过synchronized或者lock来实现,只让一个请求去请求数据库,其它的等待缓存构建后再去请求缓存
2.热点key不设置过期时间,后代定时异步更新缓存【适用于弱一致性场景】
3.续失效时长,在value内部设置一个比缓存过期时间短的标识,当异步线程发现该值快过期了,用互斥锁内置时间,并且重新去数据库加载该数据
缓存雪崩:当缓存中大量热点缓存同时失效,会导致缓存在某个时间达到峰值,请求全都去请求数据库,导致数据库压力骤增,甚至宕机。从而形成连锁反应,导致系统崩溃
一般是大量热点key同时过期或者缓存服务故障
1.热点key随机失效时间,防止同时失效
2.分布式锁读数据库
3.Redis采用高可用集群(必要时采用异地多活)
4.限流,降级,容灾,削峰等措施
热点key
请求次数过多,频率过高,又过于集中的一些key
大key(Big Key)
Big Key就是某个key对应的value很大,占用的redis空间很大,本质上是大value问题
原因:
- redis数据结构使用不当
- 未及时清理垃圾数据
- 对业务预估不准
- 明星,网红之类的粉丝列表或者热点新闻的评论列表
解决方式:
- 对大key进行拆分,拆分为小key,然后通过get不同的key或者使用mget批量获取
- 对大key进行清理
- 监控redis的内存,带宽,超时比例,然后进行预警
- 定时清理失效数据,最好加一个时效性
- 压缩value,采用序列化或者压缩算法方式将key大小控制,注意序列化和反序列化都会带来消耗
数据预热【提前做好热点探测】
缓存预热是指系统上线后,提前将相关的缓存数据加载到缓存系统。
避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据。
- 数据量不大的时候,工程启动的时候进行加载缓存动作
- 数据量大的时候,设置一个定时任务脚本,进行缓存的刷新
- 数据量太大的时候,优先保证热点数据进行提前加载到缓存
mysql
数据库同步问题
缓存和数据库一致性问题 在上面已经提及
数据库扩容问题
业务系统在设计初期一般数据量小,会采取单服务+单数据库
但是随着访问量上升,为了达到数据库的最佳储存容量,需要对数据库做垂直或者水平拆分,来提升服务响应速度,这时候就涉及到了扩容问题
难点
- 数据迁移问题
- 分片规则的改变
- 数据同步,时间点,数据一致性
解决方式
- 停机扩容【简单方案】
【适用于简单业务,数据库较少的时候】
停止服务 -》新增数据库,修改分片规则,迁移数据 -》重启服务
优点:简单,停机风险操作低
缺点:缺乏高可用,如果没配置好,容易造成数据丢失,难以修复
- 平滑扩容【大数据量】
【保持高可用】
平滑扩容可以实现n库扩容2n库,增加数据库服务能力
新增数据库 -》配置双主进行数据同步 -》数据同步完成后配置双主双写 -》删除双主同步,修改数据库配置并重启 -》 清空数据库多余数据
优点:不用停机,保持高可用,扩容遇到问题容易解决,不影响线上服务,可以减少每个数据库数据量
缺点:程序复杂,数据量大时,代价高
数据库冷热分离
冷热分离就是在处理数据时,数据库分为冷库和热库
**冷库:**存放那些走到了终态的数据的数据库
**热库:**还需要修改的数据的数据库
建议用es或者clickhouse作为冷库,因为冷库一般存储的数据量比较大,es属于数据量越多优势越大的数据库
- 业务识别【简单场景适用】
比如每次更新了订单的状态,就去触发这个逻辑
- 监听识别【业务代码复杂场景适用】
可通过监听数据库变更日志binlog的方式来触发(数据库触发器也可)
- 扫表触发【时间区分冷热适用】
数据库定时任务或通过程序定时任务来触发
超卖少卖问题
jvm级别的锁 Synchronized 是无法在分布式微服务下解决超卖问题,只能解决当前进程的超卖问题
这种时候就需要引入分布式锁
- Redis setnx实现简单分布式锁【不太建议使用】
在redis中有一条命令 setnx (set if not exists):如果不存在key,则可以设置成功,存在则失败
这种情况可以实现普通的分布式锁,但是遇到了极端情况比如断电,超时,宕机等会造成不释放锁或者死锁
- redisson 实现分布式锁【建议使用】
在redisson中有一种看门狗机制,可以实现锁续命
这是一种已经造好的轮子,开箱即用,它也可以使用多redis实现分布式锁,防止单点故障
- RedLock 高可用并发锁(红锁)【不建议使用】
RedLock为了保证高可用,在设置key的时候,会创建多个节点,单节点设置成功不会告诉程序获取到了锁,只有超过半数节点设置成功才会告诉程序获取到了锁
接口防刷
顾名思义,想让某个接口某个人在某段时间内只能请求N次。 在项目中比较常见的问题也有,那就是连点按钮导致请求多次,以前在web端有表单重复提交,可以通过token 来解决。 除了上面的方法外,前后端配合的方法。现在全部由后端来控制
- 对比前端请求带来的时间戳,防止出现提前请求
- 加ip限制,如果遇到在规定时间内请求多次【一般0.3s内连续两次都是不正常的】,可以直接加入黑名单,比如加入redis给一个被封的时间
- get换成post请求,防止直接通过url点击请求
- 利用注解aop做拦截器,设置每秒请求间隔和请求次数
秒杀url隐藏
隐藏URL并不能完全防止爬虫的恶意攻击,因为这些攻击者可能通过大量的尝试来猜测正确的URL
但是加密URL的意义在于增加攻击者猜测到URL的难度,无法轻易拿到正确的URL
- 系统在秒杀的时间点才生成秒杀url地址,在非秒杀时间返回空串,防止提前被知道地址
- 通过前端给的商品id+用户id+秒杀开始日期+秒杀结束日期+随机字符串来生成唯一uuid【记得加盐加密】,并且存入redis【60s】,并且返回给前端
- 前端拿到uuid进行地址拼接,秒杀固定地址+uuid进行请求
- 后端拿到请求地址的uuid,去redis查询有没有过期,没有过期可以进行下一步
- 后端在秒杀时候可以通过同ip秒杀频率来进行ip黑名单限制,防止通过工具自动化刷单【防不了动态ip代理】
限流熔断
防止大请求量打入顶不住导致集体宕机
一般分为监控,限流,熔断
监控:对系统的各个关键指标进行监控采集,包括响应时间,请求量,错误率等
限流:对系统的并发请求进行限制,控制系统的处理能力,防止系统超负荷运行导致的宕机或者响应变慢,推荐使用guava限流
熔断:当服务或者下游出现故障或者网络异常,自动切断该服务的调用,并且返回提前设置好的错误响应,防止服务调用的连锁反应,导致系统崩溃,并且给用户体验不好,推荐使用Hystrix或者Sentinel
服务降级
秒杀系统在高并发的情况下,很容易因为大量的请求同时涌入导致服务不可用
例如,在秒杀活动开始前,大量用户会提前准备,不断刷新商品页面,导致大量请求涌入系统,使得系统崩溃
因此,在秒杀系统中使用服务降级是必要的,以保证系统的可用性和稳定性
当系统出现异常或者超负荷的情况下,通过降低服务质量或者停用部分服务来保证核心服务的可用性
自动扩缩容
当请求量过大时,单机无法承受,需要通过自动扩缩容来保证系统的正常运行
自动扩缩容是指根据系统的负载情况自动增加或减少系统的节点,以适应不同的负载情况
监控系统
通过监控系统实时监控系统的负载情况,当负载超过一定阈值时,自动触发扩容操作,当负载降低时,自动触发缩容操作
云原生架构
在云原生架构中,可以通过云平台提供的自动扩缩容功能来实现秒杀系统的自动扩缩容,例如 Kubernetes 提供了自动扩缩容的功能,可以根据负载情况自动增加或减少容器的数量
弹性计算
通过弹性计算来实现自动扩缩容,例如云服务器 ECS 提供了自动伸缩组的功能,可以根据负载情况自动增加或减少实例的数量
分布式事务
下单事务:秒杀下单需要进行扣减库存和创建订单两个操作,这两个操作需要在同一个事务中完成,否则可能会出现库存扣减成功但是订单创建失败的情况。在分布式场景下,可以使用分布式事务来保证这两个操作的原子性,常用的分布式事务解决方案有 TCC、XA、SAGA 等。
支付事务:当用户下单成功后需要进行支付,支付也是一个需要保证原子性的操作。在分布式场景下,可以使用分布式事务来保证支付的原子性,但是相比于下单事务更为复杂,因为支付涉及到第三方支付平台的交互,可能需要处理退款等异常情况。
最经典的事务就是数据库事务(ACID)
A:原子性(Atomicity)
一个事务中间执行的操作要么全部成功,要么全部失败,不会结束在某个环节【不可分割的执行单元】
事务在执行的过程中如果发生错误,会进行回滚(Rollback)到事务开始前的状态,就像这个事务没有执行过一样
C:一致性(Consistency)
一个事务执行之前和执行之后数据库都必须处于一致性状态,数据库的完整性约束没有被破坏
I:隔离性(Isolation)
事务与事务之间是互相独立的,它们不会互相干扰,一个事务不会影响到另外一个事务的数据,也不会看到另外一个事务的数据
D:持久性(Durability)
当一个事务完成之后,执行的结果必须持久化保存,即使数据库崩溃,在数据库恢复后事务提交的结果仍然存在
注意:
事务只能保证数据库的高可靠性,即数据库本身发生问题后,事务提交后的数据仍然能恢复;而如果不是数据库本身的故障,如硬盘损坏了,那么事务提交的数据可能就丢失了。这属于『高可用性』的范畴。因此,事务只能保证数据库的『高可靠性』,而『高可用性』需要整个系统共同配合实现。
可能出现的问题
更新丢失
当有两个并发执行的事务,更新同一行数据,那么其中一个事务会覆盖另外一个事务的数据【当数据库没有加任何锁操作的情况下会发生】
脏读
一个事务读到另外一个未提交事务的数据【当第一个事务回滚时候,第二个事务拿着失效的数据去处理就会出现脏读】
不可重复读(虚读,幻读)
一个事务对同一行数据读两次,却拿到了不同的结果
- 虚读:事务1在两次查询的过程中,事务2进行了一次修改,导致事务1读到了不一样的记录
- 幻读:事务1在两次查询的过程中,事务2进行了插入,删除操作,导致事务1读取的结果发生了变化
注意:
脏读是尚未提交的数据,不可重复读是已提交的数据
数据库的四种隔离级别
读未提交【Read uncommitted】
一个事务对一行数据修改的过程中,不允许另外一个事务对该行数据进行修改,但是允许读取该行数据
不会出现更新丢失,但是会出现脏读,不可重复读
读已提交【Read committed】
未提交的写事务不允许其它事务访问该行,但是读取数据的事务允许其它的事务进行访问该行数据
不会出现脏读,但是会出现不可重复读
重复读【Repeatable read】
该级别禁止写事务,但是允许读事务
不会出现不可重复读
序列化【Serializable】
所有事务都必须串行执行
能避免一切因为并发引起的问题,但是效率低
小结
隔离级别越高,越能保证数据的完整性和一致性,但是,对于并发性能的影响也越大
大多数情况下选择读提交【Read committed】,它能避免脏数据,并且有较好的并发性能,虽然它会导致不可重复读,丢失更新的并发问题,可以通过采用悲观锁或者乐观锁来控制
分布式事务
上面的事务介绍的是数据库事务,目前数据库只支持单库事务,不支持跨库事务
随着微服务架构的普及,一个大型的业务系统往往是多个服务组成,每个服务都有自己的数据库,现在一整套业务流程走下去需要多个服务来共同完成,这种跨库的事务支持就是“分布式事务”
比较常见的就是:商品系统,订单系统,支付系统,积分系统等等
CAP理论(布鲁尔定理)
一致性【Consistency】
同一数据的多个副本是否相同,所有的变化都是同步的
可用性【Availability】
在可以接受的时间范围内正确的响应用户请求,请求不能无限被阻塞
分区容错性【Partition tolerance】
当出现网络分区后,系统还是能继续工作,满足一致性和可用性
注意
熟悉CAP的人都知道,三者不能共存,一般是AP或者CP
在分布式系统中,网络无法100%可靠,分区其实是一个必然现象
如果我们选择了CA而放弃了P,那么当发生分区现象时,为了保证一致性,这个时候必须拒绝请求
但是A又不允许,所以分布式系统理论上不可能选择CA架构,只能选择CP或者AP架构
CP
放弃可用性,追求一致性和分区容错性,例:Zookeeper就是追求的强一致性
单一节点 或 多个节点处于相同的网络环境下,那么会存在一定的风险,万一该机房断电、该地区发生自然灾害,那么业务系统就全面瘫痪了。
为了防止这一问题,采用分布式系统,将多个子系统分布在不同的地域、不同的机房中,从而保证系统高可用性。
AP
放弃一致性【这里的一致性是强一致性】,追求分区容错性和可用性,这是很多分布式系统设计时候的选择,后面的BASE理论也是根据AP扩展
BASE解决了CAP中理论没有网络延迟,在BASE中用软状态和最终一致,保证了延迟后的一致性。
BASE和 ACID 是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。
BASE理论
基本可用【Basically Available】
在分布式系统中出现故障时,允许损失部分功能,保证核心功能可用【比如延长响应时间】
软状态【Soft state】
同一数据的不同副本的状态,可以不需要实时一致性【状态可以一段时间不同步】
最终一致性【Eventually consistent】
经过一段时间后,所有节点数据最后都会达到一致【不是强一致性】
注意
ACID能够保证事务的强一致性,即数据是实时一致的,在本地事务中是没有问题的,在分布式事务中,强一致性会极大影响分布式系统的性能,因此分布式系统中遵循BASE理论即可
但分布式系统的不同业务场景对一致性的要求也不同。如交易场景下,就要求强一致性,此时就需要遵循ACID理论,而在注册成功后发送短信验证码等场景下,并不需要实时一致,因此遵循BASE理论即可。
因此要根据具体业务场景,在ACID和BASE之间寻求平衡
什么时候使用分布式事务
一般出现分布式事务的原因就是微服务过多
- 不同服务之间的调用
随着互联网快速发展,微服务,SOA等服务架构模式正在被大规模的使用,举个简单的例子,一个公司之内,用户的资产可能分为好多个部分,比如余额,积分,优惠券等等。在公司内部有可能积分功能由一个微服务团队维护,优惠券又是另外的团队维护
- 数据库不在同一个地区
我们的Mysql一般来说装千万级的数据就得进行分库分表,对于一个支付宝的转账业务来说,你给的朋友转钱,有可能你的数据库是在北京,而你的朋友的钱是存在上海,所以我们依然无法保证他们能同时成功。
因为事务会增加系统的复杂度,并且会导致响应变慢,这样的成本实在是太高了,不要因为追求某些设计,而引入不必要的成本
常见的分布式事务解决方案
2PC【二段式提交】
第一阶段:事务管理器通知所有资源管理器进行预备【prepare】,如果收到就绪状态则进入下一步,有一个未就绪则回滚
第二阶段:事务管理器收到所有资源管理器都准备完毕,通知资源管理器进行提交操作,如果有一个失败则进行回滚
优点:尽量的保证了数据的强一致性,实现成本低,现在各大主流数据库都有自己的实现
缺点:
- 单点问题:如果在第一阶段完成,第二阶段正准备提交的时候出现事务管理器宕机,资源管理器就会存在一直阻塞状态,导致数据库无法使用【需要备机进行容错】
- 同步阻塞:在准备就绪后,资源管理器是出于阻塞状态,当提交完成才释放资源,其它第三方节点范围公共资源的时候就得处于阻塞等待状态
- 数据不一致:在第二阶段中,假设协调者发出了事务commit的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性
3PC【三段式提交】
三段提交(3PC)是对两段提交(2PC)的一种升级优化,3PC在2PC的第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前,各参与者节点的状态都一致。
同时在协调者和参与者中都引入超时机制,当参与者各种原因未收到协调者的commit请求后,会对本地事务进行commit,不会一直阻塞等待,解决了2PC的单点故障问题,但3PC 还是没能从根本上解决数据一致性的问题。
3PC 的三个阶段分别是CanCommit、PreCommit、DoCommit
CanCommit:协调者向所有参与者发送CanCommit命令,询问是否可以执行事务提交操作。如果全部响应YES则进入下一个阶段。
3PC的CanCommit阶段其实和2PC的准备阶段很像。
协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
- 事务询问 协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
- 响应反馈 参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No
PreCommit:协调者向所有参与者发送PreCommit命令,询问是否可以进行事务的预提交操作,参与者接收到PreCommit请求后,如参与者成功的执行了事务操作,则返回Yes响应,进入最终commit阶段。一旦参与者中有向协调者发送了No响应,或因网络造成超时,协调者没有接到参与者的响应,协调者向所有参与者发送abort请求,参与者接受abort命令执行事务的中断。
协调者根据参与者的反应情况来决定是否可以记性事务的PreCommit操作。根据响应情况,有以下两种可能。 假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。
- 发送预提交请求 协调者向参与者发送PreCommit请求,并进入Prepared阶段。
- 事务预提交 参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
- 响应反馈 如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
- 发送中断请求 协调者向所有参与者发送abort请求。
- 中断事务 参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。
DoCommit: 在前两个阶段中所有参与者的响应反馈均是YES后,协调者向参与者发送DoCommit命令正式提交事务,如协调者没有接收到参与者发送的ACK响应,会向所有参与者发送abort请求命令,执行事务的中断。
TCC【二段式提交,补偿型】
TCC【Try-Confirm-Cancel】又被称为补偿事务,
TCC与2PC的思想很相似,事务处理流程也很相似,但是2PC应用于DB层面,TCC则应用于应用层面,需要编写业务逻辑来实现
它的核心思想:针对每个操作都要注册一个与其对应的确认(try)和补偿(cancel)
Try阶段:尝试执行,完成所有的业务检查(一致性),预留必须业务资源(准隔离性)
Confirm阶段:确认执行业务,不做任何业务检查,只使用try阶段预留的业务资源,Confirm阶段需要满足幂等性要求幂等设计,失败后进行重试或者其它处理
Cancel阶段:取消执行,释放try阶段预留的业务资源,并进行回滚
小结:
- 解决了协调者单点问题,由主业务方发起并且完成这个业务
- 解决同步阻塞问题,可以引入超时,超时后进行补偿,这样不会锁定整个资源,将资源转为业务逻辑形式,粒度变小
- 数据一致性问题,有了补偿机制,业务层可以控制一致性
缺点是应用侵入性强,每个操作都需要try-confirm-cancel,其次开发难度大,代码量多,需要保证幂等性和数据一致性【对于程序员水平要求较高】
MQ事务
本地消息表:将需要分布式处理的任务通过消息日志的方式进行异步执行。
消息日志可以存储到本地文本,数据库或者消息队列,再通过业务规则自动或者人工发起重试
本地消息表对应的是BASE理论,追求的是最终一致性,适用于对一致性要求不高的
MQ事务就是本地消息表的升级版
中间如果存在消息确认失败或者没有确认的情况,会进行一个重试机制,需要保证幂等
中间件推荐
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。AT模式是阿里首推的模式,阿里云上有商用版本的GTS(Global Transaction Service 全局事务服务)
官网:seata.io/zh-cn/index…
源码: github.com/seata/seata
官方Demo: github.com/seata/seata…
防止黄牛
黄牛为什么难防
- 模拟器作弊:模拟硬件设备,可以修改设备信息
- 设备牧场作弊:工作室里面一大批移动设备同时抢
- 人工作弊:靠佣金吸引兼职人员刷单
解决方案:
- 验证码,验证码符合91原则,90%时间都用在验证码输入上,如果使用自动化工具会降低影响
- 代码混淆,url隐藏,防止黄牛通过提前解析js来找到后端接口
- ip黑名单,对于同一个ip请求频率过多的直接拉黑,或者引入黄牛专用秒杀通道(现在秒杀,有很多可以通过频率来区分黄牛还是真人,然后负载到不同的服务器,让黄牛和黄牛抢,真人和真人抢,只是黄牛的通道里面秒杀的数量少于真人数量)
- 限购,每个用户只能购买一台【遇到牧场作弊的可能防不住,但是同网络的情况可以防】
- 风控系统,给每个用户做一个用户画像,打一个可疑度分数,来进行不同负载区分以及限流
秒杀时序图
秒杀流程图
秒杀架构
微服务架构【参考用】
参考
https://cloud.tencent.com/developer/article/1762003
https://blog.51cto.com/u_8238263/6020353#:~:text=使用%E2%80%8B%20%E2%80%8BToken%E2%80%8B%20%E2%80%8B%E2%80%8B机制,或使用%E2%80%8B%20%E2%80%8BToken%E2%80%8B%20%E2%80%8B%20%2B%20分布式锁的方案来解决幂等性问题%E3%80%82,4-幂等性解决方案实现思路%204-1-%20Token机制实现%20通过%E2%80%8B%20%E2%80%8BToken%E2%80%8B%20%E2%80%8B%20机制实现接口的幂等性,这是一种比较通用性的实现方法%E3%80%82
https://blog.csdn.net/a419240016/article/details/117236135
https://blog.csdn.net/wyouwd1/article/details/125705646
https://blog.csdn.net/qq_65567681/article/details/127712631
https://blog.csdn.net/weixin_35835508/article/details/112668412
https://www.cnblogs.com/hanease/p/15863393.html
https://juejin.cn/post/7112466891335008270
https://juejin.cn/post/6998416509026435108
https://blog.csdn.net/qq1309664161/article/details/118650154
https://juejin.cn/post/7126188464713760776#heading-7
https://juejin.cn/post/6862875289786662926
https://juejin.cn/post/6844903933182214151#heading-3
https://juejin.cn/post/6844903843516383245
https://juejin.cn/post/6844904087390011405#heading-5
https://juejin.cn/post/6844903573667446797#heading-13
https://juejin.cn/post/6844903647197806605#heading-15
https://juejin.cn/post/6899645923024355336#heading-1
https://juejin.cn/post/6844904176430874632
https://juejin.cn/post/6992767837702111269
https://juejin.cn/post/7044032901662375949#heading-10