一. 项目概述
项目亮点:
- 使用分布式Session,实现多台服务器的Session共享
- 使用Redis缓存提高访问速度和并发量,减少数据库压力
- 通过三级缓冲过滤大量秒杀失败请求,减小后端服务压力
- 使用分布式锁防止商品超卖
- 对页面进行静态化处理,加快用户访问速度
- 使用消息队列完成异步下单,实现流量削峰,提升用户体验
- 安全性优化:双重md5密码校验,接口限流防刷
项目难点:
8. 并发量的提升
页面静态化和Redis高速缓存提升QPS,三级缓冲过滤大量请求,流量削峰避免请求量过大压垮后端服务
9. 超卖问题
数据库乐观锁悲观锁,Redis,Zookeeper分布式锁
10. 缓存击穿,缓存雪崩,缓存一致性
二. 用户模块
1. 模块功能
用户模块分为以下功能:
- 用户注册
- 用户登录
- 更改密码
- 从Session中获取用户信息
¥2. 用户登录功能流程
- Controller接收用户id(手机号)和密码
- 根据用户id获取用户信息(如果Redis缓存中有则从缓存中获取,没有则从数据库查出后放入Redis)
- 将传来的密码MD5加密(利用用户的Salt值)后与用户的密码比较是否一致
- 密码核对成功后,利用UUID生成token,在Response中添加值为token的Cookie,在Redis缓存中设置键为token,值为user的记录。Cookie的有效期和缓存过期时间一致(分布式Session)
- 返回值为token,传给前端
- 用户再次访问页面时根据Cookie中的token去Redis缓存中查找用户信息完成自动登录
¥3. 用户模块主要技术点
- 两次MD5加密:在前端对输入的密码进行第一次加密,防止密码明文在网络被截取;在密码入库时进行第二次加密,防止数据库被盗后用户信息被窃取
- 加密算法:将Salt打乱后与加密前密码拼成一个字符串,再进行MD5加密
String str = "" + salt.charAt(0) + salt.charAt(2) + inputPass + salt.charAt(5) + salt.charAt(4);
return md5(str);
- 分布式Session:用户登录成功后,在Redis中缓存用户信息,键为token,值为user;在Response中添加值为token的Cookie;后续可根据Cookie中传来的token去Redis中查询用户信息完成自动登录
- JSR303数据校验:前端的用户id和密码通过LoginVO传递至后端,在LoginVO中通过JSR303先判断输入是否符合条件
三. 商品模块
1. 模块功能
商品模块主要有以下功能:
- 查询商品列表
- 查询商品详情
2. 商品查询流程
商品列表和商品详情查询的流程类似,主要如下:
- 从Redis缓存中尝试获取内容页面
- 缓存中有则直接返回,缓存中没有则从数据库中获取数据
- 根据模版和查询得到的数据利用thymeleafViewResolver手动渲染得到静态html页面
- 将静态页面缓存至Redis中
- 返回静态页面
¥3. 商品模块主要技术点
- 缓存静态页面:提前生成商品列表和商品详情的静态html页面,并缓存至Redis中。用户访问商品页面时,直接从缓存中获取静态页面,一方面避免直接访问数据库,增大数据库压力;另一方面避免了动态渲染页面的开销
四. 订单模块
1.模块功能
订单模块分为以下几个功能:
- 查询订单是否存在
- 查询订单详情
- 下单
2. 下单流程
下单分为创建订单和创建订单详情两步,该两步定义为一个事务(@Transactional)。
¥3. 订单模块主要技术点
- 唯一索引:为了保证订单的唯一性,防止用户重复秒杀,建立(用户id ,商品id)的唯一索引。
- 分布式id:利用SnowFlack雪花算法生成订单id,保证在分布式状况下id的递增和唯一性
- 缓存:前端会轮询是否下单成功,因此将订单和订单详情缓存在Redis中,减少数据库压力
五. 秒杀模块
1. 模块功能
秒杀模块主要分为两个功能:
- 秒杀
- 查询秒杀是否成功
2. 秒杀流程
秒杀流程通过RabbitMQ解耦为前部和后部,前部判断是否能够秒杀,后部进行秒杀订单的生成
前部流程:
- 先查询一级缓存,如果一级缓存中标识商品已秒杀完,直接返回
- 在Redis二级缓存中预减库存,如果库存小于零则去数据库中查询真实库存,获得真实库存后如果发现确实已卖完,则在本地缓存中标记该商品已卖完并返回
- 根据用户id和商品id查询是否重复下单
- 将用户信息和商品id包装成消息发送给消息队列
- 直接返回
后部流程:
- 后端服务从消息队列获取消息
- 判断库存是否小于零
- 判断是否重复下单
- 减库存
- 创建订单
其中减库存和创建订单必须定义为一个事务,且需要额外手段防止超卖
3. 秒杀模块主要技术点
- 三级缓冲:
- 本地缓存:HashMap<Long, Boolean>用于判断某个商品是否已秒杀完
- Redis缓存:Redis缓存缓存某个商品的库存量(与数据库中的真实库存不一定保持一致)
- RabbitMQ异步下单:防止超高流量击垮服务,利用RabbitMQ作流量削峰
通过三级缓冲保护,使得无法秒杀成功的请求尽早返回,减少服务和数据库的压力。其中前两级缓冲的初始化通过Spring Bean的初始化方法完成。
- 分布式锁防止超卖:
为了防止超卖,尝试了各种方法:
- 单机锁(synchronize锁,ReentrantLock),该方法的缺陷是只能在单服务器情况下使用
- 数据库乐观锁,悲观锁
update sk_goods_seckill set stock_count = stock_count - 1 and version = version + 1 where goods_id = #{goodsId} and stock_count > 0 and version = #{version}
但是 stock_count > 0可能会锁住该表的其他行数据,影响整体的并发度
- Redis分布式锁和Zookeeper分布式锁
- AOP切面锁:利用AOP将锁上移至@Transactional外部,在事务提交后再释放锁,防止锁提前释放导致其他事务读取错误信息导致超卖
- 注解+AOP锁:为了锁的外移和代码的复用,定义了一个
@AopLock注解,配合一个AOP切面,该切面会在加了该注解的方法上进行AOP切入式加锁
六. 其他优化与未来优化方向
1. 接口限流
使用Guava的RateLimiter对接口进行限流,如果在规定时间内没拿到令牌,则直接返回
2. IP防刷
- 使用Guava的LoadingCache建立本地缓存,key为IP,value为RateLimiter
- 在访问防刷的接口时,通过Request获取IP地址
- 在LoadingCache会为第一次访问的IP分配一个RateLimiter
- 通过RateLimiter规定该IP每秒能访问的次数达到IP防刷的效果
3. 未来优化方向
- 服务集群化:单台服务器难以承受压力时,需创建服务器集群,一方面提高并发量,一方面保证高可用。对应用服务器需设置负载均衡。
- 微服务化:将上述的各个模块分离成一个个服务。实现服务间的解耦,进一步提升并发量
- MySQL分库分表:当单表的数据量过大时,可考虑分库分表,但是需要涉及到分布式事务
七. 通用模块
1. 异常的设计与处理
- 自定义一个运行时异常GlobalException,构造方法中传入CodeMsg
- CodeMsg为枚举对象,在其中规定了错误码和异常信息
- 通过
@ControllerAdvice和@ExceptionHandler设计全局异常捕获,捕获后根据CodeMsg返回异常原因
2. Redis Key前缀的设计
- 设计一个KeyPrefix基类,主要有前缀名称和过期时间两个属性
- 后续要使用Redis缓存时,为各类缓存项创建一个继承自KeyPrefix的前缀类,在其内部设置常量已供使用
八. 问题
1. Redis使用什么客户端,作了哪些配置?
- 一般操作使用Jedis客户端,分布式锁使用Redisson
- 配置了IP地址,端口号,连接池参数(最大阻塞等待时间,连接池最小空闲连接,连接池最大空闲连接,连接池最大连接数)
2. RabbitMQ作了哪些配置
最小的消费者数量
spring.rabbitmq.listener.simple.concurrency= 1
最大的消费者数量
spring.rabbitmq.listener.simple.max-concurrency= 10
指定一个请求能处理多少个消息,如果有事务的话,必须大于等于transaction数量.
spring.rabbitmq.listener.simple.prefetch= 1
是否启动时自动启动容器
spring.rabbitmq.listener.simple.auto-startup=true
决定被拒绝的消息是否重新入队;默认是true
spring.rabbitmq.listener.simple.default-requeue-rejected= true
发送重试是否可用
spring.rabbitmq.template.retry.enabled=true
第一次和第二次尝试发布或传递消息之间的间隔
spring.rabbitmq.template.retry.initial-interval=1000
最大重试次数
spring.rabbitmq.template.retry.max-attempts=3
最大重试时间间隔
spring.rabbitmq.template.retry.max-interval=10000
应用于上一重试间隔的乘数
spring.rabbitmq.template.retry.multiplier=1.5
2. 一般问题
- 缓存击穿和缓存雪崩以及缓存一致性等问题
- 大量的使用缓存,对于缓存服务器也有很大的压力,如何减少redis的访问(使用本地内存标记)
- 在高并发请求的业务场景,大量请求来不及处理,甚至出现请求堆积的情况(水平拓展,增加消息队列的消费者服务)
- 怎么保证一个用户不能重复下单(唯一索引)
- 怎么解决超卖现象
- 页面静态化的过程及什么是浏览器缓存
3. 假如减了库存但用户没有支付,怎么将库存还原继续进行抢购
- 用户秒杀成功后,给RabbitMQ发送一个TTL消息(TTL为支付规定时间)
- 消息时间到后自动进入死信队列
- 死信队列的消费者获取订单消息,去数据库中查询订单是否已支付,若未支付则还原库存
4. 限流算法
漏桶与令牌桶算法的区别
漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
在某些情况下,漏桶算法不能够有效地使用网络资源。因为漏桶的漏出速率是固定的参数,所以即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使某一个单独的流突发到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。而令牌桶算法则能够满足这些具有突发特性的流量。通常,漏桶算法与令牌桶算法可以结合起来为网络流量提供更大的控制。
两者主要区别在于“漏桶算法”能够强行限制数据的传输速率,而“令牌桶算法”在能够限制数据的平均传输速率外,还允许某种程度的突发传输。在“令牌桶算法”中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的门限,所以它适合于具有突发特性的流量。