这个KV对存到session中模拟短信发送(仅仅将其打印出来)返回通用对象注册("/register")根据手机号从session中获取验证码若验证成功,则调用userService的注册方法(注册过程是一个事务)注册内容主要就是合法性判断,数据库的更新等等(使用MD5对密码">
当前位置:   article > 正文

秒杀项目总结(附秒杀系统设计)

秒杀项目总结(附秒杀系统设计)

GitHub:链接地址

UserController("/user")

获取验证码("/getotp")

  • 随机生成10000~99999的数字
  • 将验证码同对应手机号关联,即使将 < 手机号, 验证码 > 这个KV对存到session中
  • 模拟短信发送(仅仅将其打印出来)
  • 返回通用对象

注册("/register")

  • 根据手机号从session中获取验证码
  • 若验证成功,则调用userService的注册方法(注册过程是一个事务)
    • 注册内容主要就是合法性判断,数据库的更新等等(使用MD5对密码进行加密)

获取User信息("/get")

  • 调用UserService的接口方法,根据userid来获取用户信息
  • 返回

登录("/login")

  • 登录参数的合法性校验(手机号, 密码)
  • 根据登录参数,判断能否登录成功(即判断用户名密码是否正确)
  • 如果正确的话,就生成一个登录成功的Token,存储到Redis中(该Token为< UUID, userModel > ),并设置失效时间为1个小时
  • 返回TokenId(用UUID生成的Token的唯一标识符)

ItemController("/item")

创建商品("/create")

  • 根据传入参数来调用创建商品的Service(往数据库里写对应商品信息,没什么好说的)

促销商品的发布("/publishpromo")

  • 调用商品的促销模块来发布商品,除了创建对应的促销商品实体之外,最重要的是将促销商品的库存信息预存到redis中.(提前把火爆的热键存到redis中)
  • 同时设定 秒杀大闸 (这个是干嘛的忘了。。看到后面再补充把 todo

商品详情页的浏览("/get")

  • 先尝试从本地缓存获取数据(使用的是Guava中的Cache)
  • 再尝试从redis从根据商品id获取商品信息
  • 如果都没获取到,就去访问数据库(访问数据库的时候,需要先通过布隆过滤器的判断,防止缓存穿透)

商品列表("/list")

  • 没啥好说的,也是调用Item的服务…

OrderController("/order")

初始化方法:

  • 线程池初始化
  • RateLimiter初始化

生成验证码("/generateverifycode")

  • todo

生成促销商品下单令牌("/generatetoken")、

之所以需要使用这一步,是为了防止下单接口被刷。 在前端点下下单按钮的时候,会先申请一个促销商品下单的令牌,只有获取这个令牌,才能在之后购买促销的商品

  • 尝试从redis中根据tokenID获取用户登录信息(token),获取不到说明没登录
  • 验证希望获取的商品令牌的商品是否是正在促销的商品(这里的下单令牌仅对促销商品有用
  • 根据用户id,促销商品id, 商品本身id,来生成“促销商品下单令牌”,并返回

下单

本次实战,主要也是对该下单接口进行一个优化.

  1. 使用RateLimiter对象进行限流(tryAcquire()方法如果返回false,就直接返回对应异常)

  2. Session检查)根据request中的UUID,尝试获取redis中的用户信息(如果获取不到说明没有登录)

  3. (合法性检查)校验是否已经获取过“都小商品下单令牌”,并验证是否合法

  4. 生成一个库存流水(用于消息回查)

  5. 基于rocketmq,调用消息服务,发送一条事务消息。

    • 消息服务(MQProducer)是个单例Bean,在初始化方法中,会对自身进行一个初始化,包括设置NameservAddr,设置listener等等…
    • 事务消息的两个回调函数的实现内容:
      • 在回调方法中(executeLocalTransaction),执行本地事务,这里其实就是将本地事务的执行和消息的发送绑成了一个事务,若本地事务执行成功,则消息才能发送成功,若本地事务执行失败,则消息回滚,依然对消费者不可见。
      • 这里的本地事务就是对redis中的库存进行扣减,并生成对应的订单等操作…
      • 在回查方法中(checkLocalTransaction), 会根据库存流水的状态,来决定当前消息的状态。 即具体逻辑就是,传入的参数里有库存流水ID,根据这个ID去查询对应的下单流水信息,如果status状态是1的话,就说明是正在等待处理,依然是unknown,如果是2的话,则说明已经处理完了,直接返回commit,如果是3的话,说明发生了回滚,就返回回滚状态.
      • 同时在库存流水中也添加一个字段,即回查该消息的次数,如果超过的一定的次数则判定该消息出现问题,对其进行一个回滚…(例如在消息COMMIT之前出现了宕机,那么该消息就一直处于了UNKNOWN状态…所以,如果处于UNKNOWN状态过久的话,就判断出现了问题。。对其进行回滚)
  • 如果事务消息发送成功,则本地事务一定执行成功,所以也就表明下单成功。。。(这里只解决了保证消息投递成功,但没解决消费端一定能消费成功)

另一端:

  • 对于消费端而言,会消费生产者投递的库存扣减的消息。。。

如何设计一个秒杀系统

问题一:瞬时高并发

时间极短, 瞬间用户量大, 出现缓存雪崩,缓存击穿,缓存穿透等情况,打挂DB可能会造成系统的联级失效。

1.1 服务单一职责——防止联级失效

单独提供一个秒杀服务模块,数据库也采用分库的思想,单独提供一个秒杀库。 这样做的好处就是即使秒杀库挂了,服务挂了,也不会影响到其他业务。

1.2 Redis集群——解决单台redis性能瓶颈

单机的Redis顶不住高并发的问题可以通过搭建Redis集群来解决,写master,读slave,然后开启哨兵模式,开启持久化保证高可用。

1.3 Ngnix反向代理——解决单台服务器性能瓶颈

一台ngix服务器负责反向代理,多台web服务器提供服务。

1.4 缓存雪崩、缓存击穿、缓存穿透问题也要考虑

  • 缓存雪崩
    • 问题:当缓存层出现问题没法工作时,流量会打到后台数据库上,会出现问题。
    • 解决方法:
      • 保证redis高可用
      • 进行限流(可以使用线程池,信号量,Guava的RateLimiter等来进行限流)
  • 缓存穿透
    • 问题:访问缓存和数据库中都没有的数据,这种请求会穿过缓存层打到数据库上,若有大量的这种请求,那么缓存层就形同虚设了…
    • 解决方法:
      • 缓存空数据(不推荐,因为组合出数据库里不存在的数据的方式很多,这么做不仅没法解决问题甚至还会让redis挂掉…)
      • 布隆过滤器:布隆过滤器能够保证无法通过的数据一定不存在,所以,可以事先把数据库里的主键给预存到布隆过滤器里,只有通过了布隆过滤器的那些访问请求,才会去redis,数据库中去查找.
  • 缓存击穿
    • 问题: 一个超级热点数据如果因为超时失效或其他原因而从redis中被删除,那么短期大量的流量就会打到数据库上.
    • 解决办法:
      • 对于热点数据不设置失效时间
      • 提前将热点数据预存到redis中
      • 使用第三方缓存或本地缓存(例如Guava的Cache),如果是热点数据的话,频繁被访问的情况话就不会被置换出去
      • 限流(线程池、信号量、RateLimiter。。。)熔断(to study)

1.5 消息队列保证Redis和Mysql的最终一致性

  • 使用rocketmq的事务型消息 ( rocketmq的事务型消息保证了若成功发送了commit消息那么本地事务一定成功执行了(ps:只有commit的消息才能被消费者看到,而prepare的消息无法被看到) ):
    • 在本地事务中执行对Redis等其他不是太消时间的,比较关键的操作,对mysql的同步交给消费者去做.

问题二:超卖问题

就比如现在库存只剩下1个了,我们高并发嘛,4个服务器一起查询了发现都是还有1个,那大家都觉得是自己抢到了,就都去扣库存,那结果就变成了-3,是的只有一个是真的抢到了,别的都是超卖的。

  • 解决方法:
    • 增加全局售罄标志,如果发现秒杀商品已经售罄,那就直接返回了.
    • 使用Lua脚本实现Redis的CAS功能。Lua脚本是类似Redis事务,有一定的原子性,不会被其他命令插队,可以完成一些Redis事务性的操作。写一个Lua脚本把判断库存和扣减库存的操作都写在一个脚本丢给Redis去做。(to study)
    • 使用分布式锁来对redis的库存个数进行上锁然后再进行判断,扣减.

问题三:恶意刷单

  • 让前端控制,秒杀商品的下单按钮在秒杀前置灰。
  • 使用秒杀令牌,在页面端点下单按钮的时候,会先向服务器申请一个秒杀令牌,然后才是真正的下单,在下单时会去检查这个秒杀令牌是否存在,是否合法。
  • 服务端限制同一个用户的最大下单个数?
  • 封IP(从nginx这边来控制)
  • 使用验证码

考虑的点:

  1. 如果秒杀的流量单机无法抗住的话,就用集群+分布式来扩展…(1.3 1.2)
  2. 单独提供一个秒杀服务模块,数据库也采用分库的思想,单独提供一个秒杀库。 这样做的好处就是即使秒杀库挂了,服务挂了,也不会影响到其他业务,对于数据表也进行分表(库存单独分离出来)。
  3. 充分利用缓存(多级缓存的思路,本地缓存Guava的Cache,第三方缓存,redis缓存,缓存带来的问题(穿透,雪崩,击穿))
  4. 消息队列保证redis和mysql的最终一致性
  5. Redis中的共享数据(比如在redis中的库存),在多线程环境下使用分布式锁,用redis的setnx + px 来做
  6. 考虑到恶意下单,看上面的问题3
  7. 考虑到限流量,可以使用Guava的RateLimiter,信号量,线程池来做…

问题:

Q: 库存预热:
A: 这个一般给运营人员去做吧。。手动将秒杀商品的库存加载到redis里面,在规模比较大的时候的话,可以专门写一个这种管理平台系统吧…

Q: 扣减库存怎么保证线程安全:
A: 项目里是通过分布式锁来去做的(todo 实现一下分布式锁), 就是向上提供一个查询接口,如果获取不到锁就自旋…

Q: 缓存击穿
A: redis预热、永不失效、本地缓存或第三方缓存、限流策略…

Q: 客户的恶意下单怎么防范
A: 秒杀令牌机制。 其他还有: 验证码,封ip(nginx有个设置项,单个ip访问的频率和次数多了之后有个拉黑操作…服务端写逻辑封用户?),(接口隐藏 )秒杀前将下单接口置灰,使获取不到下单接口… 或者也可以限制同一个用户的最大下单数?

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/小桥流水78/article/detail/807317
推荐阅读
相关标签
  

闽ICP备14008679号