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