Java面试集锦(一)之秒杀系统设计

  • 作者: 凯哥Java
  • 面试宝典
  • 时间:2020-08-04 22:07
  • 124人已阅读
简介 秒杀系统设计1.主要做到以下两点:尽量将请求过滤在上游。尽可能的利用缓存(大多数场景下都是查多于写)。如果流量巨大,导致各个层的压力都很大可以适当的加机器横向扩容。如果加不了机器那就只有放弃流量直接返回失败。快速失败非常重要,至少可以保证系统的可用性。业务分批执行:对于下单、付款等操作可以异步执行提高吞吐率。主要目的就是尽量少的请求直接访问到DB。2.架构图image.png前端请求进入web层,

秒杀系统设计

1.主要做到以下两点:

  • 尽量将请求过滤在上游。

  • 尽可能的利用缓存(大多数场景下都是查多于写)。

  • 如果流量巨大,导致各个层的压力都很大可以适当的加机器横向扩容。如果加不了机器那就只有放弃流量直接返回失败。快速失败非常重要,至少可以保证系统的可用性。

  • 业务分批执行:对于下单、付款等操作可以异步执行提高吞吐率。

  • 主要目的就是尽量少的请求直接访问到 DB。

2. 架构图

3899969e5b524875d8e7d6fb4d932f0f.png


  • 前端请求进入 web 层,对应的代码就是 controller。

  • 之后将真正的库存校验、下单等请求发往 Service 层(其中 RPC 调用依然采用的 dubbo,只是更新为最新版本,本次不会过多讨论 dubbo 相关的细节,有兴趣的可以查看 基于dubbo的分布式架构)。

  • Service 层再对数据进行落地,下单完成

其实抛开秒杀这个场景来说正常的一个下单流程可以简单分为以下几步:

  • 校验库存

  • 扣库存

  • 创建订单

  • 支付

3.常见问题

3.1 超卖现象(使用乐观锁更新)

3.2 提高吞吐量

为了进一步提高秒杀时的吞吐量以及响应效率,这里的 web 和 Service 都进行了横向扩展。

  • web 利用 Nginx 进行负载。

  • Service 也是多台应用

当并发量达到几百万时(分布式限流)

我们将并发控制在一个可控的范围之内,然后快速失败这样就能最大程度的保护系统。

3.3 sql查询太多(redis缓存)

这种数据我们完全可以放在内存中,效率比在数据库要高很多。

由于我们的应用是分布式的,所以堆内缓存显然不合适,Redis 就非常适合。

这次主要改造的是 Service 层:

  • 每次查询库存时走 Redis。

  • 扣库存时更新 Redis。

  • 需要提前将库存信息写入 Redis(手动或者程序自动都可以)。

3.4请求同步转异步(kafka)

这里我们将写订单以及更新库存的操作进行异步化,利用 Kafka 来进行解耦和队列的作用。

每当一个请求通过了限流到达了 Service 层通过了库存校验之后就将订单信息发给 Kafka ,这样一个请求就可以直接返回了。

消费程序再对数据进行入库落地。

因为异步了,所以最终需要采取回调或者是其他提醒的方式提醒用户购买完成。

4. 总结

其实经过上面的一顿优化总结起来无非就是以下几点:

  • 尽量将请求拦截在上游。

  • 还可以根据 UID 进行限流。

  • 最大程度的减少请求落到 DB。

  • 多利用缓存。

  • 同步操作异步化。

  • fail fast,尽早失败,保护应用。

5、悲观锁

简单理解下悲观锁:当一个事务锁定了一些数据之后,只有当当前锁提交了事务,释放了锁,其他事务才能获得锁并执行操作。

这里使用select for update的方式利用数据库开启了悲观锁,锁定了id=1的这条数据(注意:这里除非是使用了索引会启用行级锁,不然是会使用表锁,将整张表都锁住。)。之后使用commit提交事务并释放锁,这样下一个线程过来拿到的就是正确的数据。

悲观锁一般是用于并发不是很高,并且不允许脏读等情况。但是对数据库资源消耗较大。

6.乐观锁

那么有没有性能好,支持的并发也更多的方式呢?

那就是乐观锁。

乐观锁是首先假设数据冲突很少,只有在数据提交修改的时候才进行校验,如果冲突了则不会进行更新。

通常的实现方式增加一个version字段,为每一条数据加上版本。每次更新的时候version+1,并且更新时候带上版本号

实践:基于分布式微服务的秒杀抢购功能的实现

借下图

aaff331e53edab63a35e768afca49e14.png


秒杀设计到的微服务

  1. 注册中心(Eurake) : @EnableEurekaServer开启注册中心,实现对各种微服务的集中管理

  2. 网关徽服务(zuul) : @EnableDiscoveryClient将服 务注册到到注册中心,@EnablezuulProxy开启 网关服务,对微服务路口做统一管理, 实现路由,降级(容错回退),限流的功能。如果多台服务器,可以通过路径和服务的绑定path: /user-service/* ; serviceld: user-service2,实现负载均衡(默认是Ribbon轮询,还有随机)

  3. 用户中心微服务(user-service) :@EnableDiscoveryClient将 用户中心微服务注册到到注册中心,实现注册和登录功能

  4. 授权中心微服务(auth-service) : @EnableDiscoveryClient将用户中心微服务注册到到注册中心实现对登录的鉴权。

  5. 商品微服务(item-service) : @EnableDiscoveryClient将商品微服务注册到到注册中心,做商品的添加和查询。

具体秒杀流程逻辑

  1. 网关对部分不需要登录认证的接口放行(要优化)1.注册用户,网关对注册放行

  2. 登录接口到网关,被路由到授权中心,授权中心微服务调用用户中心的登录接口进行校验,校验成功,利用JWT生成token,然后利用RSA非对称加密token,生成公钥和私钥保存,然后将token返回到客户端

秒杀业务

  1. 在商品微服务中设置秒杀参数,根据参数的商品Id查询商品,构建商品秒杀表,添加,然后更新redis缓存

BoundHashOperations<StringObjectObject> hashOperations = this.stringRedisTemplate.boundHashOps(KEY PREFIX);

1/判断是否存在此K值

if (hashOperations.hasKey(KEY PREFI){

hashOperations.delete(KEY_ PREFIX);

seckiloods.forEach(goods > hashOperatiosput(goos.getkud(.totring(), goods.getstock).totrin));))
  1. 使用秒杀功能需要登录验证,创建登录拦截(LoginInterceptor extends HandlerinterceptorAdapter)对token进行验证,认证通过将用户信息存放到线程域中,并且走一个限流拦截AccessInterceptor extends HandlerinterceptorAdapter)实现限流功能

  2. 构建秒杀路径(限流),加密,保存到redis缓存,隐藏秒杀路径,防止刷单。

4. 秒杀

4.1. 验证秒杀路径

4.2. 读取库存, 减1后更新缓存

4.3. 库存不足直接返回“排队中”

4.4. 库存充足, 将商品信息封装入队MQ,然后直接返回“排队中”

  1. 然后订单微服务监听队列,消费队列,

5.1判断库存不足,将该商品设置成不可秒杀状态,

5.2查看是否秒杀到,秒杀到直接返回,

5.3没有秒杀到,创建订单


Top Top