嚼一嚼秒杀系统(一)基于数据库乐观锁
1、背景
最近突然对秒杀系统比较感兴趣,所以突发奇想来研究一下。其实在面试过程中也经常会遇到此类问题:如果你有一千个商品,怎么在最快的时间内卖出去,且不多卖不少卖。
下面我就基于这个常见的问题,逐步完成一个高性能的秒杀系统。
github地址:https://github.com/351524388/flash-sale
2、架构
本篇文章介绍demo1,使用到的技术如下:
- SpringBoot-V2.3.1.RELEASE
- JPA
- @Version:数据版本号,实现乐观锁
- @UpdateTimestamp:自动更新修改时间
- Tomcat
- server.tomcat.threads.max:200(默认情况)
- JPA
- MySQL-V5.7.20
- nginx:反向代理,主要是通过80节点访问多个后台服务
- jmeter:模拟高并发场景,聚合报告查看结果
3、代码实现
demo1实现的功能,是在高并发的情况下,正确的扣减库存。代码比较简单,分为以下几层:
-
entity:数据库模型,核心代码如下:
@Entity @Table(name = "good_count_info") public class GoodCountInfo { /** * 自增主键 */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; /** * 商品总数 */ private Long goodCount; /** * 数据版本号 */ @Version private Long versionNo; /** * 修改时间 */ @UpdateTimestamp private LocalDateTime modifyTime; // 省略 Setter、Getter 方法 ... }
-
repository:操作数据库,代码如下:
@Repository public interface GoodCountRepository extends JpaRepository<GoodCountInfo, Integer> { }
-
service:业务操作,代码如下:
@Service public class GoodCountServiceImpl implements IGoodCountService { @Value("${flash.sale.table.row.id:1}") private int rowId; @Autowired private GoodCountRepository goodCountRepository; @Override public Long getGoodCount() { return goodCountRepository.findAll().get(0).getGoodCount(); } @Override @Transactional(rollbackFor = Throwable.class) public boolean subGoodCount() { GoodCountInfo goodCountInfo = goodCountRepository.getOne(rowId); if (goodCountInfo.getGoodCount() > 0) { goodCountInfo.setGoodCount(goodCountInfo.getGoodCount() - 1); goodCountRepository.save(goodCountInfo); return true; } else { return false; } } }
-
controller:接口层,代码如下:
@RestController @RequestMapping("/flash-sale/goodCount") public class FlashSaleController { @Autowired private IGoodCountService goodCountService; @GetMapping public Object getGoodCountInfo() { return goodCountService.getGoodCount(); } @PostMapping public Object subGoodCountInfo() { try { return goodCountService.subGoodCount(); } catch (Exception e) { System.out.println("减库存失败"); return false; } } }
这里再贴一下application.properties配置文件的内容:
spring.datasource.url=jdbc:mysql://localhost:3306/flash-sale?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=validate
#先启动节点1,修改这里的id,然后启动节点2
flash.sale.table.row.id=1
详细代码参见github:https://github.com/351524388/flash-sale
4、测试结果
这只是一个很简单的demo,但是再简单也需要能正确运行才行,于是我采用jmeter进行压测,通过聚合报告来分析结果,如果不太会用jmeter的读者,可以自行百度(因为我也是刚百度学会的)。
最终压测的结果如下:
- 单节点:吞吐量100/s,异常率0%
- 双节点:
- 单行数据吞吐量200/s,异常率4%。多运行几次,可以达到300/s。
- 两行数据吞吐量200/s,异常率2%。多运行几次,可以达到500/s。
根据结果可以看出,增加节点可以提高并发效率,但是异常率也会增加。如果将一千条数据分到不同的行中,我这里使用了两行,每行五百条,分开后可以有效提高并发效率,异常率也会降低。
另外有一点,就是服务启动以后,运行次数增加,性能会越来越好。所以秒杀之前对系统进行预热,对并发效率也会有所提升。毕竟Java号称是越跑越快。
5、总结
这是一个很简单的demo,后期会在这个基础上逐步完善,争取将单个节点的性能提升到极致。
不足之处,欢迎评论指出。
问题列表
- 安装MySQL5.7.20的时候会报错,提示需要安装Microsoft Visual C++ 2013 Redistributable (x86),这时候只需要将这个插件的64位和32位都安装上就好了
- Idea多一个服务启动多个实例:https://www.cnblogs.com/yg_zhang/p/12651584.html
- Jmeter压测:https://blog.csdn.net/weixin_42118716/article/details/106498171