简单秒杀系统设计
5. 使用乐观锁解决超卖
描述:比如某商品的库存为1,此时用户1和用户2并发购买该商品,用户1提交订单后该商品的库存被修改为0,而此时用户2并不知道的情况下提交订单,该商品的库存再次被修改为-1,这就是超卖现象
实现:
-
对库存更新时,先对库存判断,只有当库存大于0才能更新库存
-
对用户phone和商品id建立一个唯一索引,通过这种约束避免同一用户发送两个请求秒杀到两件相同商品
-
实现乐观锁,乐观锁的思路一般是给商品信息表增加一个version字段,更新时where语句中增加版本的判断,当提交前版本号等于更新前版本号,说明此时没有被其他线程影响到,正常更新,如果冲突了则不会进行提交更新。当库存是足够的情况下发生乐观锁冲突就进行一定次数的重试。
商品库存场景中当提交更新时需要判断数据是否已经被修改(AND number=#{number}),只有在 number等于上一次查询到的number时 才提交更新。number起到了版本控制(相当于version)的作用。
6. 使用RateLimiter实现限流
描述:当我们去秒杀一些商品时,此时可能会因为访问量太大而导致系统崩溃,此时要使用限流来进行限制访问量,当达到限流阀值,后续请求会被降级;降级后的处理方案可以是:返回排队页面(高峰期访问太频繁,等一会重试)、错误页等。
实现:项目使用RateLimiter来实现限流,RateLimiter是guava提供的基于令牌桶算法的限流实现类,通过调整生成token的速率来限制访问量,从而达到防止超大流量冲垮系统。当spring mvc在处理请求时候,从桶中申请令牌,申请到了就成功响应,申请不到时直接返回失败;
(令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务)
7、解决重复秒杀
插入秒杀明细 + 减库存操作需要事务。由于明细表设置了商品id和用户的唯一标识作为联合主键,所以能有效避免重复秒杀,如果在插入秒杀明细不成功的时候,抛出异常并进行rollback。
禁止重复提交:用户提交之后按钮失效,禁止重复提交
五、未来优化方向
1. session共享
验证用户账号密码都正确情况下,通过UUID生成唯一id作为token,再将token作为key、用户信息作为value模拟session存储到redis,同时将token存储到cookie,保存登录状态
好处: 在分布式集群情况下,服务器间需要同步,定时同步各个服务器的session信息,会因为延迟到导致session不一致,使用redis把session数据集中存储起来,解决session不一致问题。
2. 本地标记 + redis预处理 + RabbitMQ异步下单 + 客户端轮询
描述:通过三级缓冲保护,1、本地标记 2、redis预处理 3、RabbitMQ异步下单,最后才会访问数据库,这样做是为了最大力度减少对数据库的访问。
实现:
-
在秒杀阶段使用本地标记对用户秒杀过的商品做标记,若被标记过直接返回重复秒杀,未被标记才查询redis,通过本地标记来减少对redis的访问
-
抢购开始前,将商品和库存数据同步到redis中,所有的抢购操作都在redis中进行处理,通过Redis预减少库存减少数据库访问
-
为了保护系统不受高流量的冲击而导致系统崩溃的问题,使用RabbitMQ用异步队列处理下单,实际做了一层缓冲保护,做了一个窗口模型,窗口模型会实时的刷新用户秒杀的状态。
-
client端用js轮询一个接口,用来获取处理状态
https://tech.meituan.com/2018/11/15/java-lock.html
秒杀页面:
用户在秒杀活动开始前可能会频繁的刷新来确保活动开始,而将页面静态化且将图片和脚本文件存放在CDN中,可防止过高的带宽给服务带来的压力,提高响应时间
统一时钟:
存在客户端与服务端,各个服务器之间的时间不同步情况,使用统一时钟为准,保证活动开始的一致性
防刷策略:
使用动态验证码,ip限制,账户请求数限制等措施保证秒杀活动的公平性
限流策略:
因为秒杀活动的特性,库存量是少而有限的,所以可以将大部分流量拒绝,直接返回提示库存已不足信息
如库存有10,可使用队列或其他实现将有效请求限制在20,其他的直接拒绝。如一批处理完库存还有剩余(下单逻辑出现异常情况)则可再加入新的有效请求
限流后的有效请求数已不会对后端服务与DB造成压力
缓存:
缓存可以应对秒杀场景下读多写少的特点,减少DB压力
缓存的数据的准确性可以由设置过期时间,在DB更新库存时候将老的缓存淘汰来保证
缓存在秒杀场景下一定程度的延迟对用户体验角度是能接受的,如可能缓存还剩1,但是下单之后可能就提示库存不足了
库存:
库存数量防止超卖可以由加乐观锁或其他的原子操作来保证多线程情况下的准确性