一、业务流程

下订单、扣库存、支付订单

风险控制;库存锁定;生成订单;短信通知;更新统计数据。
image.png

架构设计

秒杀架构模型设计理解 - 图2


二、问题及解决思路

2.1 问题

1.1:高并发

秒杀具有时间短、并发量大的特点。
短时间内会有大量请求涌进来,后端如何防止并发过高造成缓存击穿或者失效,击垮数据库都是需要考虑的问题。

1.2:超卖问题

分析秒杀的业务场景,最重要的有一点就是超卖问题,假如备货只有100个,但是最终超卖了200,一般来讲秒杀系统的价格都比较低,如果超卖将严重影响公司的财产利益,因此首当其冲的就是解决商品的超卖问题。

1.3:接口防刷

现在的秒杀大多都会出来针对秒杀对应的软件,这类软件会模拟不断向后台服务器发起请求,一秒几百次都是很常见的,如何防止这类软件的重复无效请求,防止不断发起的请求也是需要我们针对性考虑的

1.4:秒杀url

对于普通用户来讲,看到的只是一个比较简单的秒杀页面,在未达到规定时间,秒杀按钮是灰色的,一旦到达规定时间,灰色按钮变成可点击状态。这部分是针对小白用户的,如果是稍微有点电脑功底的用户,会通过F12看浏览器的network看到秒杀的url,通过特定软件去请求也可以实现秒杀。或者提前知道秒杀url的人,一请求就直接实现秒杀了。这个问题我们需要考虑解决

1.5:数据库设计

秒杀有把我们服务器击垮的风险,如果让它与我们的其他业务使用在同一个数据库中,耦合在一起,就很有可能牵连和影响其他的业务。如何防止这类问题发生,就算秒杀发生了宕机、服务器卡死问题,也应该让他尽量不影响线上正常进行的业务

1.6: 宕机

考虑宕机后果
秒杀是营销活动中的一种,如果和其他营销活动应用部署在同一服务器上,肯定会对现有其他活动造成冲击,极端情况下可能导致整个电商系统服务宕机


2.2 客户端 优化

1.3.6 秒杀页面静态化

存在CDN中

CDN, 内容分发网络,加速用户获取数据的系统 部署在用户最近的网络节点上 命中CND不需要访问后端服务器

将商品的描述、参数、成交记录、图像、评价等全部写入到一个静态页面,用户请求不需要通过访问后端服务器,直接在前台客户端生成,这样可以最大可能的减少服务器的压力。具体的方法可以使用freemarker模板技术,建立网页模板,填充数据,然后渲染网页

1.1.1 [接口限流]-前端限流

首先第一步就是通过前端限流,用户在秒杀按钮点击以后发起请求,那么在接下来的5秒是无法点击(通过设置按钮为disable)。这一小举措开发起来成本很小,但是很有效。

2.3 服务端 优化

1.3.1 增加网络带宽


1.3.2 使用nginx

nginx是一个高性能web服务器,它的并发能力可以达到几万,而tomcat只有几百。通过nginx映射客户端请求,再分发到后台tomcat服务器集群中可以大大提升并发能力。

1.3.3 [削峰]-单体redis升级为集群redis

秒杀是一个读多写少的场景,使用redis做缓存再合适不过。
不过考虑到缓存击穿问题,我们应该构建redis集群,采用哨兵模式,可以提升redis的性能和可用性。

1.3.4 [削峰]-使用消息队列

1.3.5 [异步处理]-

1.1.2 [接口限流]-后端限流

同一个用户xx秒内重复请求直接拒绝,redis.setexpire(userId,value,10).value可以是任意值,一般放业务属性比较好,这个是设置以userId为key,10秒的过期时间(10秒后,key对应的值自动为null)

1.4.1 秒杀url的设计

  1. 为了避免有程序访问经验的人通过下单页面url直接访问后台接口来秒杀货品,我们需要将秒杀的url实现动态化,即使是开发整个系统的人都无法在秒杀开始前知道秒杀的url。具体的做法就是通过md5加密一串随机字符作为秒杀的url,然后前端访问后台获取具体的url,后台校验通过之后才可以继续秒杀。
  2. 秒杀开启时输出秒杀接口地址和MD5串,否则输出系统时间和秒杀时间

1.5.1 SQL语句精简

传统的做法是先查询库存,再去update。这样的话需要两个sql,而实际上一个sql我们就可以完成的。可以用这样的做法:

  1. update miaosha_goods set stock =stock-1
  2. where goos_id ={#goods_id} and version = #{version} and sock>0

1.6.1 熔断降级

Hystrix进行服务熔断和降级,可以开发一个备用服务,假如服务器真的宕机了,直接给用户一个友好的提示返回,而不是直接卡死,服务器错误等生硬的反馈。

1.6.2 防止导致整个系统宕机

三、实战

数据库设计

  1. 创建秒杀库存表
  2. 秒杀成功明细表— 用户登录认证相关的信息

    秒杀1:

  1. @Override
  2. @Transactional
  3. /**
  4. * 使用注解控制事务方法的优点: 1.开发团队达成一致约定,明确标注事务方法的编程风格
  5. * 2.保证事务方法的执行时间尽可能短,不要穿插其他网络操作,RPC/HTTP请求或者剥离到事务方法外部
  6. * 3.不是所有的方法都需要事务,如只有一条修改操作,只读操作不需要事务控制
  7. */
  8. public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
  9. throws SeckillException, RepeatKillException, SeckillCloseException {
  10. if (md5 == null || !md5.equals(getMD5(seckillId))) {
  11. throw new SeckillException("seckill data rewrite");
  12. }
  13. // 执行秒杀逻辑:减库存 + 记录购买行为
  14. Date now = new Date();
  15. try {
  16. // 记录购买行为
  17. int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
  18. // 唯一:seckillId,userPhone
  19. if (insertCount <= 0) {
  20. // 重复秒杀
  21. throw new RepeatKillException("seckill repeated");
  22. } else {
  23. // 减库存,热点商品竞争
  24. int updateCount = seckillDao.reduceNumber(seckillId, now);
  25. if (updateCount <= 0) {
  26. // 没有更新到记录 rollback
  27. throw new SeckillCloseException("seckill is closed");
  28. } else {
  29. // 秒杀成功 commit
  30. SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
  31. return new SeckillExecution(seckillId, SeckillStateEnum.SUCCESS, successKilled);
  32. }
  33. }
  34. } catch (SeckillCloseException | RepeatKillException e1) {
  35. throw e1;
  36. } catch (Exception e) {
  37. logger.error(e.getMessage(), e);
  38. // 所有编译期异常转换为运行期异常
  39. throw new SeckillException("seckill inner error:" + e.getMessage());
  40. }
  41. }
  1. <!-- 目的:为dao接口方法提供sql语句配置 -->
  2. <update id="reduceNumber">
  3. <!-- 具体的sql -->
  4. UPDATE seckill
  5. SET number = number - 1
  6. WHERE seckill_id = #{seckillId}
  7. AND start_time <![CDATA[ <= ]]> #{killTime}
  8. AND end_time >= #{killTime}
  9. AND number > 0
  10. </update>

秒杀2:

  1. // 执行秒杀操作by存储过程
  2. @Override
  3. public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) {
  4. if (md5 == null || !md5.equals(getMD5(seckillId))) {
  5. return new SeckillExecution(seckillId, SeckillStateEnum.DATA_REWRITE);
  6. }
  7. Date killTime = new Date();
  8. Map<String, Object> map = new HashMap<String, Object>();
  9. map.put("seckillId", seckillId);
  10. map.put("phone", userPhone);
  11. map.put("killTime", killTime);
  12. map.put("result", null);
  13. // 执行存储过程,result被赋值
  14. try {
  15. seckillDao.killByProcedure(map);
  16. // 获取result
  17. int result = MapUtils.getInteger(map, "result", -2);
  18. if (result == 1) {
  19. SuccessKilled sk = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
  20. return new SeckillExecution(seckillId, SeckillStateEnum.SUCCESS, sk);
  21. } else {
  22. return new SeckillExecution(seckillId, SeckillStateEnum.stateOf(result));
  23. }
  24. } catch (Exception e) {
  25. logger.error(e.getMessage(), e);
  26. return new SeckillExecution(seckillId, SeckillStateEnum.INNER_ERROR);
  27. }
  28. }
  1. <select id="killByProcedure" statementType="CALLABLE">
  2. call execute_seckill (
  3. #{seckillId, jdbcType = BIGINT, mode = IN },
  4. #{phone, jdbcType = BIGINT, mode = IN },
  5. #{killTime, jdbcType = TIMESTAMP, mode = IN },
  6. #{result, jdbcType = INTEGER, mode = OUT }
  7. )
  8. </select>
  1. -- 秒杀执行存储过程
  2. DELIMITER $$ -- onsole ; 转换为 $$
  3. -- 定义存储过程
  4. -- 参数:in 输入参数; out 输出参数
  5. -- row_count():返回上一条修改类型sql(delete,insert,upodate)的影响行数
  6. -- row_count: 0:未修改数据; >0:表示修改的行数; <0:sql错误/未执行修改sql
  7. CREATE PROCEDURE `seckill`.`execute_seckill`
  8. (IN v_seckill_id bigint, IN v_phone BIGINT, IN v_kill_time TIMESTAMP, OUT r_result INT)
  9. BEGIN
  10. DECLARE insert_count INT DEFAULT 0;
  11. START TRANSACTION;
  12. INSERT ignore INTO success_killed (seckill_id, user_phone, create_time)
  13. VALUES(v_seckill_id, v_phone, v_kill_time);
  14. SELECT ROW_COUNT() INTO insert_count;
  15. IF (insert_count = 0) THEN
  16. ROLLBACK;
  17. SET r_result = -1;
  18. ELSEIF (insert_count < 0) THEN
  19. ROLLBACK ;
  20. SET r_result = -2;
  21. ELSE
  22. UPDATE seckill SET number = number - 1
  23. WHERE seckill_id = v_seckill_id AND end_time > v_kill_time
  24. AND start_time < v_kill_time AND number > 0;
  25. SELECT ROW_COUNT() INTO insert_count;
  26. IF (insert_count = 0) THEN
  27. ROLLBACK;
  28. SET r_result = 0;
  29. ELSEIF (insert_count < 0) THEN
  30. ROLLBACK;
  31. SET r_result = -2;
  32. ELSE
  33. COMMIT;
  34. SET r_result = 1;
  35. END IF;
  36. END IF;
  37. END;
  38. $$
  39. -- 代表存储过程定义结束
  40. DELIMITER ;
  41. SET @r_result = -3;
  42. -- 执行存储过程
  43. call execute_seckill(1001, 13631231234, now(), @r_result);
  44. -- 获取结果
  45. SELECT @r_result;
  46. -- 存储过程
  47. -- 1.存储过程优化:事务行级锁持有的时间
  48. -- 2.不要过度依赖存储过程
  49. -- 3.简单的逻辑可以应用存储过程
  50. -- 4.QPS:一个秒杀单6000/qps

参考文章:https://blog.51cto.com/13527416/2085258?cid=700792