秒杀系统简单实现

前言

代码

代码仓库

练习了一下秒杀,网站搭建尽量从简,也算是比较轻车熟路了.

提取秒杀的核心,数据库只建了两张表:(直接从自动生成的脚本拷贝过来的…有些不必要项,大致还是能看明白.)

数据库

CREATE TABLE `product` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL,
  `number` int(11) NOT NULL,
  `start_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `end_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  `info` varchar(500) DEFAULT 'That`s good!',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='秒杀商品表';
CREATE TABLE `user_buy_product` (
  `phone` varchar(15) NOT NULL,
  `id` bigint(20) NOT NULL,
  PRIMARY KEY (`phone`,`id`),
  KEY `fk_product` (`id`),
  CONSTRAINT `fk_product` FOREIGN KEY (`id`) REFERENCES `product` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

PS:上过数据库的课了,建表时还是该考虑考虑设计范式…上面两张表值得一提的点在于外键约束.

秒杀的难点主要在处理并发与并发时的优化.

参考

Github 仓库: Sunybyjava/seckill

Mooc 视频: Java 高并发秒杀

视频讲的很好,虽然项目很简单,但是视频从一定高度讲解了秒杀的场景,收获很大…

文档:

Spring Boot 2.x 中文文档

MyBatis 中文文档 3.4

MyBatis Generator 用户手册

Spring Data Redis- Version 2.1.3.RELEASE

spring-boot-starter-data-redis 翻译官方文档 5.3 - 5.6

技术栈

前后端分离,前端用 vue-cli3 快速搭建,后端用 springboot.

使用 spring boot 与 mybatis 整合,有些繁琐的配置,我弄完后传到了下面的仓库,以后直接拿来用…

使用 spring boot + gradle + mybatisGenerator 实现代码自动生成


后端

核心: spring boot + mybatis

版本控制: gradle

插件: mybatis-generator 工具

前端

核心: vue-cli3

异步请求: axios

cookie 保存状态,操作 cookie 封装了一个类.

界面: Nes.css

思路

开发过程:从交互分析,提取 api,再层层分解,一步一步实现.

按照上面的参考,从 dao 层到 service 层,再到 controller 层,这样自底向上的流程容易让人迷糊…每一步都不知道为了啥.应该从上到下,我缺少什么,然后再去做什么…这样理解起来会比较容易.

开发步骤 .

开发步骤

  1. 搭建后端项目基本框架.放在 MyBatisGenerator-Tool .完成 mybatis 的相关繁琐配置.
  2. 分析项目,设计数据结构,数据库,项目结构.考虑 数据交互,异常处理 等细节.
  3. 分析交互,设计 api.
  4. 对每一个 api 进行实现.
  5. 优化.

讨论一下第三步:

比如,首先用户进入网页,首先肯定会需要一个获取秒杀商品信息的 api.

商品需要在秒杀时段内才能暴露秒杀按钮,同时总不可能让用户不断刷新网页吧,所以前端需要做倒计时,并且动态暴露秒杀按钮.

秒杀操作,实际上还是执行了一个 url (也是 api),这个秒杀 url 不能在倒计时的阶段暴露,只有到了秒杀阶段才能暴露,我们怎么获取它呢?

可以通过写一个获取秒杀 api 的 api.

此时需要考虑到对秒杀 api 加密.(甚至随时间变化,能实现吗?能吧?按时间混淆?)

用户开始秒杀,这里异常情况最多.

此时应该考虑到事务 , 存储过程 …(SQL 怎么实现,框架怎么实现…)

同时这里也是并发点,后面进行优化也主要是围绕这一块.

api 列表

api参数返回值说明
/api/seckill/list秒杀商品列表
/api/seckill/exposerskillId(商品 id)商品的秒杀 url
/api/seckill/execute/{seckillId}/{md5Code}phone(手机号), seckillId(商品 id), md5Code(加密码)返回秒杀结果手机号从 cookie 获得,后两者从 url 获得
/api/time/now获取服务器端时间

api 是根据前端来的,前端要啥,后端写啥…

这一块不是重点,列在这可以和其他块内容参考理解.比如下面的优化块.

RESTful API 设计

参考资料

Github 仓库 : restful-api-design-references

主要这三篇:

  1. 理解 RESTful 架构 - 阮一峰 简单了解什么是 RESTFul
  2. RESTful API 设计指南 - 阮一峰
  3. Restful API 的设计规范 实战经验的总结,具有较强的启发意义

执行情况

各种规范(即便是自己总结的)都自有道理,我在 Restful API 的设计规范 这篇文章看到 不要包装 的建议,所以选择在开发过程中统一利用注解 RestController 直接返回对象(框架帮我们把对象解析为 json 数据).

在我参考的仓库中,封装了一个 dto 的 ajax 交互类.

不包装的话,对异常怎么处理呢?

可以采用 ResponseEntity.

@RequestMapping(value = "/exposer", method = RequestMethod.GET)
  public ResponseEntity<?> exposer(Long skillId) {
    return ResponseEntity.ok("ok");
    // return ResponseEntity.ok(new Object());
    // return ResponseEntity.status(422).body("bad");
  }

如上,ResponseEntity 可以携带各种数据返回,而且可以设置返回的 http 的状态码.

也有人建议通过设置状态码来区分异常.

我采用的方式是:

http 的状态码始终返回 200.

自定义错误信息,返回自定义的状态码与错误信息

先建立:运行时异常类,维护错误信息的枚举类,异常信息交互类(ErrorResult.class).

先看这三个类.

异常:

public class SkillException extends RuntimeException {

  private SkillStatusEnum statusEnum;
  ...
}

真正的错误信息被枚举类维护:

public enum SkillStatusEnum {
  PRODUCT_NOT_EXIST(1000, "this is in not in products."),
  SKILL_DATE_NOT_START(1001, "the skill is not started."),
  SKILL_DATE_END(1002, "the skill is over."),
  SKILL_URL_ERROR(2000, "秒杀地址被篡改。"),
  SKILL_REPEAT(2001, "秒杀成功!无需再次秒杀。"),
  PRODUCT_COUNT_OVER(2002, "库存不足。"),
  SKILL_ERROR(3000, "其他异常");

  private int code;
  private String message;

  SkillStatusEnum() {
  }
  SkillStatusEnum(int state, String info) {
    this.code = state;
    this.message = info;
  }
  public int getCode() {return code;}
  public String getMessage() {return message;}
}

ErrorResult 简单地封装了 code,message 两条属性.

public class ErrorResult {
  // 自定义错误码
  private int code;
  // 错误信息
  private String message;
  ...
}

如果遇到了运行时异常,就把对应的错误信息(在枚举类中)传至异常类,然后抛出.

最后在 controller 层捕获,把异常信息取出,封装至交互类 ErrorResult.class,再通过 ResponseEntity 携带错误信息返回给前端.

回头看上面 controller 层的代码!!我使用了多态(ResponseEntity<?>),这样可以保证我在正常情况下可以向前端传送某些对象,遇到异常时也可以返回 ErrorResult 的实例!


做成这个鬼样也不是我一开始想到的.

比如本来没打算做一个异常类,但是在做 事务 的时候,需要抛出 runtimeexcetpion,才会回滚,为了统一处理异常,就不如自己封装了一下.

PS:有更优雅的方式吗?

这样处理还不如创建个交互类直观…


前端就这样处理:

import axios from 'axios'
axios.get('/api/test')
  .then(function (res) {
    if (res.data.code != undefined) {
        console.log(response.data)
    } else {
        // 由后端抛出的错误
        alert(res.data.message)
    }
  }).catch(function (error) {
       // 由网络或者服务器抛出的错误
     alert(error.toString())
  })

也挺好…

优化

忘了就看看视频,挺短的,也都是干货…

优化是重点.

在这里,优化集中在两个点,看 api 列表,一个是获取商品信息,一个是秒杀.

为什么在这些地方?主要是这里涉及到与数据库的交互,比如获取商品信息,商品信息的变动比较小,就不必每次查询就建立数据库连接.

在视频教程里,分别采用:

使用 redis 缓存商品信息.对于秒杀操作,采用存储过程.

对于前者,redis 的作用就不赘叙.对于后者,什么是存储过程也不多言了.

因为秒杀涉及到修改两个表,那么至少会执行两条 sql,在之前,我们是在 Service 层来执行这两个操作,并且通过 Spring 的声明式事务来管理的.我们可以把这些过程封装为 SQL 层面的一个存储过程,然后只需要在 Service 层调用这个存储过程就行了.

这里优化的是事务行级锁持有的时间.Service 执行的操作,我们放到 SQL 层来做,在更底层执行,效率会高一点.

不过视频教程不建议过多依赖存储过程.

这里的逻辑很简单,可以一用.

查资料,查询,秒杀的操作几乎也都是通过 redis 来优化的.

如果直接在缓存进行秒杀(直接在缓存进行减库存操作)等操作,就又涉及到缓存与数据库的同步…

其他

/api/seckill/list 使用了缓存.

其他同理…

其他优化

进一步的优化,就是分布式之类的了…接下来稍微学习一下.

redis 安装与使用

redis 是啥就不多言了…

安装不提,缺少某些依赖安装即可…

注意

再不进行任何配置的情况下,使用的是默认配置.执行 redis-server 后,该程序会在前台执行.而我们实际对 redis 进行操作需要打开 redis-cli,在这种情况下只能另开一个窗口打开 redis-cli.

如果在当前窗口按 ctrl +zctrl + c 关闭,再执行 redis-cli,是没有效果的.

如果已经整合到 spring,进行数据操作时会报错NOAUTH Authentication required.

另外:执行 redis-server 如果遇到某些错误信息,其实当前窗口已经给出了解决办法.看不明白可以搜索引擎查看.

我们需要后台运行 redis.

修改 /redis/redis.conf 中的 daemonize nodaemonize yes.

进入 /redis/src

执行 ./redis-server ../redis.conf 意思就是以自己的配置(后台执行)来运行 redis-server.

ok.

命令不是死的,灵活变通.比如使用默认值执行了 make install,会在 /usr/local/bin 生成安装目录,可全局执行 redis 相关命令…

ps -ef|grep redis 检查是否后台启动.

springboot2.x 整合 redis

配置文件 /config/RedisConfiguration.java

配置 application.yml

redis 是提供了常见的数据结构的存储你,如 String,List…

在本项目中,redis 使用 jackjson 把对象转换为 json,以 String 来储存,所以只用了 k/v 的方式. json 也是序列化的一种嘛.

redisTemplate.opsForValue()

直接序列化和 json 怎么选型呢?

可以这样:只读取用 json,涉及到修改用普通序列化.

看场景嘛…

前端

数据库存时间类型为 timestamp

前后端数据交换,解析时遇到一些问题.

倒计时的实现

先与服务器交互一次,以服务器的时间为准,然后靠着轮循倒计时…

java实现秒杀系统@Controller @RequestMapping("seckill")//url:/模块/资源/{id}/细分 /seckill/list public class SeckillController { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private SeckillService seckillService; @RequestMapping(value="/list",method = RequestMethod.GET) public String list(Model model){ //获取列表页 List list=seckillService.getSeckillList(); model.addAttribute("list",list); //list.jsp+model = ModelAndView return "list";//WEB-INF/jsp/"list".jsp } @RequestMapping(value = "/{seckillId}/detail",method = RequestMethod.GET) public String detail(@PathVariable("seckillId") Long seckillId, Model model){ if (seckillId == null){ return "redirect:/seckill/list"; } Seckill seckill = seckillService.getById(seckillId); if (seckill == null){ return "forward:/seckill/list"; } model.addAttribute("seckill",seckill); return "detail"; } //ajax json @RequestMapping(value = "/{seckillId}/exposer", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"}) @ResponseBody public SeckillResult exposer(@PathVariable("seckillId") Long seckillId){ SeckillResult result; try { Exposer exposer =seckillService.exportSeckillUrl(seckillId); result = new SeckillResult(true,exposer); } catch (Exception e) { logger.error(e.getMessage(),e); result = new SeckillResult(false,e.getMessage()); } return result; } @RequestMapping(value = "/{seckillId}/{md5}/execution", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"} ) @ResponseBody public SeckillResult execute(@PathVariable("seckillId")Long seckillId,
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值