这篇博客是笔者学习慕课网若鱼老师的《Java 秒杀系统方案优化 高性能高并发实战》课程的学习笔记. 若鱼老师授课循循善诱, 讲解由浅入深, 欢迎大家支持.
本文记录课程中的注意点, 方便以后 code review. 此外, 本文将注意点相关的优质讲解链接在了一起, 方便初学者系统学习.
本文并非单纯介绍秒杀系统特有的技术点, 不适合高手. 进阶学习的话, 极客时间有个不错的小专栏 -- 如何设计一个秒杀系统, 阿里高级技术专家讲解秒杀系统的设计要点, 那个课程挺干货的.
设计秒杀系统的技术要点
1. 登录的密码传输:
用户的数据库表设计, 需要增加一字段保存密码的 Salt 值
两次 MD5 操作(敏感数据一定要使用 https 协议传输):
客户端: 将明文 password 和客户端硬编码的 Salt 值进行拼接, 然后进行 MD5 操作.
不用盐的话, MD5 字符串有可能会被彩虹表或者社工库破解
服务端: 将客户端传过来的 MD5 字符串和数据库用户对应的 Salt 字段进行拼接. 然后进行 MD5 操作.
这次加盐 MD5, 可以有效防止内部员工泄露或者数据库被拖库后, 明文密码泄露
2. 自定义 JSR303 的校验器
可以参照 javax.validation.constraints.NotNull 注解, 自定义自己的校验器, 将校验代码与业务代码分离. 不过由于校验失败会输出 BindException 异常, 所以最好配合全局捕获异常进行友好的输出.
自定义校验器很简单, 只需要定义一个注解和对应的校验类
3. 自定义全局异常捕获
使用 @ControllerAdvice 注解, 定义全局的异常捕获, 并从异常中获取异常信息解析出来, 发送给前端
可以自定义一个 GlobalException 异常, 利用全局异常捕获, 将所有服务器处理异常集中处理.(Service 层处理异常后不设置状态码, 而是直接抛 GlobalException 全局异常)
不返回状态码的好处是 Controller 层不需要再繁琐的判断 Service 层的返回值, 代码更简洁
4. 数据库表设计
通过将订单建立唯一索引来保证用户只能创建一个秒杀订单
商品金额最好以分为单位, 比较安全
商品 ID 最好不要使用自增, 会暴露商品总数等信息. 可以使用 UUID, 但范围查找时会有性能损耗. 所以一般采用 SnowFlake 算法生成 ID
另外, 自增 ID 的缺点也就是无法在多个表中, 或者多个数据库中保持 ID 主键唯一不重复, 所以若是使用分布式数据库以及数据合并的情况下时不能使用自增 ID 的.
5. 代码规范
更新字段越多, 产生的数据库 Binlog 就越多. 所以只更新数据库部分字段的时候, 最好新建一个对象, 只赋值要更新的字段, 然后调用 mybatis 的 @Update, 这样不做全量更新可以提高性能
前端回包使用 Result 包装类封装, 对报错信息使用 CodeMsg 包装类封装, 保持代码风格统一
Service 只注入跟自己同名的 dao, 如果需要别的 dao, 请注入对应的 Service
Service 的 API 相比 dao 会多一些防御代码(例如, 直接修改了别的模块 dao 数据, 但缓存未清理), 更加安全
6. 事务
秒杀有两个事务:
减库存 ->创建秒杀订单
创建秒杀订单
秒杀中涉及到上述两个事务, 为了保障数据安全, 可以使用声明式事务(Spring 的 @Transactional)
PROPAGATION_REQUIRED 是 Spring 默认的传播机制, 如果外层有事务, 则当前事务加入到外层事务, 一块提交, 一块回滚. 本工程的场景使用默认事务传播机制即可
有关 Spring 事务传播机制可以查看这篇博客 https://www.jianshu.com/p/fab34943c03c
7. 压测
在生产环境中, 秒杀系统要独立运行与其他业务系统, 实现资源隔离, 避免业务系统相互影响稳定性
请求入口可以使用 nginx,LVS,F5 等不同的负载均衡器
Jmeter 随机生成用户数据, 然后使用 Jmeter 模拟用户压测. 压测运行环境最好与被测服务器环境隔离.
接口测试可以还使用 Postman 和 ApacheBench
8. 页面优化技术
页面 / URL 缓存. 用于数据变化不频繁的页面或者热点网页. 如果数据较多需要分页的数据, 类似商品详情数据, 一般可以考虑只缓存前两页(根据访问量作取舍)
缓存方法: 将渲染好的 html 文件存放到 Redis. 在访问 Url 时, 首先检测 Redis 是否有 HTML 缓存. 有缓存的话则直接返回缓存; 没有缓存的话则渲染后存入 Redis, 并返回给前端. 页面缓存过期时间具体根据业务场景判断.
页面局部缓存. 热点数据缓存, 当 Ajax 请求信息更新, 涉及的可能是需要保存在数据库的操作, 例如表格信息等时, 可以采用 Redis 缓存, 方法同页面缓存一样, 定义好可以区分业务的 Key 即可
静态资源优化
JS/CSS 压缩, 减少流量(可通过升级 HTTP2 来解决)
多个 JS/CSS 组合, 减少连接数(例如: tengine)
CDN 就近访问
如果需要采用 JS/CSS 压缩或者减少连接数等方法, 可以使用 HTTP2 来提升性能 https://www.jianshu.com/p/e664fe534ef9
对象缓存. 例如使用 Redis 保存 Session 对象. 对象缓存涉及到一个双写一致性问题, 有关双写一致性问可以查看这篇博客 https://www.jianshu.com/p/a0d8a1dd9bc7
9. 秒杀的逻辑优化
顺序:
系统初始化, 把商品库存数量加载到 Redis
收到请求, Redis 原子操作预减库存, 库存不足, 直接返回, 否则进入 3
请求入队, 立即返回前端 "排队中"
请求出队, 生成订单, 减少库存(服务端)
客户端轮询, 是否秒杀成功 (客户端) 和 4 同步, 得到结果刷新结果显示
优化:
在第二步预减库存时, 可以在内存里加一个 map,ID 为商品 ID,value 为是否有库存, 这样当库存没有之后, 直接通过内存中的值判断是否还有库存, 减少对 Redis 的访问.
购买请求加入消息队列, 异步下单(前端显示排队中), 增强用户体验
前端要尽量减少重复请求
10. 安全优化
10.1 秒杀接口地址隐藏
每次点击秒杀按钮, 先从服务器获取动态拼接而成的秒杀地址.
Redis 以缓存用户 ID 和商品 ID 为 Key, 秒杀地址为 Value 缓存秒杀地址
用户请求秒杀商品的时候, 要带上秒杀地址进行校验
10.2 数学公式验证码
防止恶意脚本抢购
使请求时间分散
10.3 接口限流防刷
使用计数法, 在拦截器做限制请求频率. 利用 Redis 缓存的有效期(以用户 ID 拼接 Url 作为 key, 以访问次数为 value), 指定缓存有效期为 1 秒, 访问接口每次将 value+1, 到达阈值跳转全局异常.
优化: 使用拦截器 + 自定义注解, 减少对业务代码的侵入. 有关拦截器可以查看这篇博客 https://www.jianshu.com/p/d0719c1ebfd9
另外对于接口限流也可以考虑使用令牌桶, 控制对 MySQL 的访问.
最后, 限于笔者经验水平有限, 欢迎读者就文中的观点提出宝贵的建议和意见. 如果想获得更多的学习资源或者想和更多的技术爱好者一起交流, 可以关注我的公众号『全菜工程师小辉』后台回复关键词领取学习资料, 进入前后端技术交流群和程序员副业群. 同时也可以加入程序员副业群 Q 群: 735764906 一起交流.
来源: https://www.cnblogs.com/mseddl/p/11595633.html