本文根据动脑学院的一节类似的课程,改编实现。分别使用DB和redis来完成。

隔离的解释

业务隔离:将秒杀业务独立出来,尽量不与其他业务关联,以减少对其他业务的依赖性。譬如秒杀业务只保留用户id,商品id,数量等重要属性,通过中间件发送给业务系统,完成后续的处理。

系统隔离:将秒杀业务单独部署,以减少对其他业务服务器的压力。

数据隔离:由于秒杀对DB的压力很大,将DB单独部署,不与其他业务DB放一起,避免对DB的压力。

本篇讲使用DB完成秒杀系统。下一篇使用redis完成持久层。

一 初始化项目

以Springboot,mysql,jpa为技术方案。

新建Springboot项目,pom如下

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  4. <modelVersion>4.0.0</modelVersion>
  5.  
  6. <groupId>com.tianyalei</groupId>
  7. <artifactId>common</artifactId>
  8. <version>0.0.1-SNAPSHOT</version>
  9. <packaging>jar</packaging>
  10.  
  11. <name>common</name>
  12. <description>Demo project for Spring Boot</description>
  13.  
  14. <parent>
  15. <groupId>org.springframework.boot</groupId>
  16. <artifactId>spring-boot-starter-parent</artifactId>
  17. <version>1.5.2.RELEASE</version>
  18. <relativePath/> <!-- lookup parent from repository -->
  19. </parent>
  20.  
  21. <properties>
  22. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  23. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  24. <java.version>1.8</java.version>
  25. </properties>
  26.  
  27. <dependencies>
  28. <dependency>
  29. <groupId>org.springframework.boot</groupId>
  30. <artifactId>spring-boot-devtools</artifactId>
  31. <optional>true</optional><!-- optional=true,依赖不会传递,该项目依赖devtools;之后依赖myboot项目的项目如果想要使用devtools,需要重新引入 -->
  32. </dependency>
  33. <dependency>
  34. <groupId>org.springframework.boot</groupId>
  35. <artifactId>spring-boot-starter-data-jpa</artifactId>
  36. </dependency>
  37. <dependency>
  38. <groupId>org.springframework.boot</groupId>
  39. <artifactId>spring-boot-starter-data-redis</artifactId>
  40. </dependency>
  41. <dependency>
  42. <groupId>mysql</groupId>
  43. <artifactId>mysql-connector-java</artifactId>
  44. <scope>runtime</scope>
  45. </dependency>
  46. <dependency>
  47. <groupId>com.alibaba</groupId>
  48. <artifactId>druid</artifactId>
  49. <version>1.0.18</version>
  50. </dependency>
  51. <dependency>
  52. <groupId>org.springframework.boot</groupId>
  53. <artifactId>spring-boot-starter-test</artifactId>
  54. <scope>test</scope>
  55. </dependency>
  56. </dependencies>
  57.  
  58. <build>
  59. <plugins>
  60. <plugin>
  61. <groupId>org.springframework.boot</groupId>
  62. <artifactId>spring-boot-maven-plugin</artifactId>
  63. </plugin>
  64. </plugins>
  65. </build>
  66.  
  67. </project>

javaBean

  1. package com.tianyalei.model;
  2.  
  3. import javax.persistence.*;
  4.  
  5. /**
  6. * Created by wuwf on 17/7/5.
  7. */
  8. @Entity
  9. public class GoodInfo {
  10. @Id
  11. @GeneratedValue(strategy = GenerationType.AUTO)
  12. private Integer id;
  13. //数量
  14. private int amount;
  15. //商品编码
  16. @Column(unique = true)
  17. private String code;
  18.  
  19. public Integer getId() {
  20. return id;
  21. }
  22.  
  23. public void setId(Integer id) {
  24. this.id = id;
  25. }
  26.  
  27. public int getAmount() {
  28. return amount;
  29. }
  30.  
  31. public void setAmount(int amount) {
  32. this.amount = amount;
  33. }
  34.  
  35. public String getCode() {
  36. return code;
  37. }
  38.  
  39. public void setCode(String code) {
  40. this.code = code;
  41. }
  42. }

dao层,注意一下sql语句,where条件中的amount - count >= 0是关键,该语句能严格保证不超卖。

  1. package com.tianyalei.repository;
  2.  
  3. import com.tianyalei.model.GoodInfo;
  4. import org.springframework.data.jpa.repository.Modifying;
  5. import org.springframework.data.jpa.repository.Query;
  6. import org.springframework.data.repository.CrudRepository;
  7.  
  8. /**
  9. * Created by admin on 17/7/5.
  10. */
  11. public interface GoodInfoRepository extends CrudRepository<GoodInfo, Integer> {
  12. @Query("update GoodInfo set amount = amount - ?2 where code = ?1 and amount - ?2 >= 0")
  13. @Modifying
  14. int updateAmount(String code, int count);
  15. }

service接口

  1. package com.tianyalei.service;
  2.  
  3. import com.tianyalei.model.GoodInfo;
  4.  
  5. /**
  6. * Created by wuwf on 17/7/5.
  7. */
  8. public interface GoodInfoService {
  9. void add(GoodInfo goodInfo);
  10.  
  11. void delete(GoodInfo goodInfo);
  12.  
  13. int update(String code, int count);
  14. }

Service实现类

  1. package com.tianyalei.service;
  2.  
  3. import com.tianyalei.model.GoodInfo;
  4. import com.tianyalei.repository.GoodInfoRepository;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.stereotype.Service;
  7. import org.springframework.transaction.annotation.Transactional;
  8.  
  9. /**
  10. * Created by wuwf on 17/7/5.
  11. */
  12. @Service("db")
  13. public class GoodInfoDbService implements GoodInfoService {
  14.  
  15. @Autowired
  16. private GoodInfoRepository goodInfoRepository;
  17.  
  18. @Transactional
  19. public int update(String code, int count) {
  20. return goodInfoRepository.updateAmount(code, count);
  21. }
  22.  
  23. public void add(GoodInfo goodInfo) {
  24. goodInfoRepository.save(goodInfo);
  25. }
  26.  
  27. public void delete(GoodInfo goodInfo) {
  28. goodInfoRepository.deleteAll();
  29. }
  30.  
  31. }

yml配置文件

  1. spring:
  2. jpa:
  3. database: mysql
  4. show-sql: true
  5. hibernate:
  6. ddl-auto: update
  7. datasource:
  8. type: com.alibaba.druid.pool.DruidDataSource
  9. driver-class-name: com.mysql.jdbc.Driver
  10. url: jdbc:mysql://localhost:3306/test
  11. username: root
  12. password:
  13. redis:
  14. host: localhost
  15. port: 6379
  16. password:
  17. pool:
  18. max-active: 8
  19. max-idle: 8
  20. min-idle: 0
  21. max-wait: 10000
  22. profiles:
  23. active: dev
  24. server:
  25. port: 8080

以上即是基本配置。

二 模拟并发访问抢购

新建junit测试类
  1. package com.tianyalei;
  2.  
  3. import com.tianyalei.model.GoodInfo;
  4. import com.tianyalei.service.GoodInfoService;
  5. import org.junit.After;
  6. import org.junit.Before;
  7. import org.junit.Test;
  8. import org.junit.runner.RunWith;
  9. import org.springframework.boot.test.context.SpringBootTest;
  10. import org.springframework.test.context.junit4.SpringRunner;
  11.  
  12. import javax.annotation.Resource;
  13. import java.util.ArrayList;
  14. import java.util.List;
  15. import java.util.concurrent.CountDownLatch;
  16.  
  17. /**
  18. * Created by wuwf on 17/7/5.
  19. */
  20. @RunWith(SpringRunner.class)
  21. @SpringBootTest
  22. public class MyTest {
  23.  
  24. @Resource(name = "db")
  25. private GoodInfoService service;
  26.  
  27. private String goodCode = "iphone7";
  28. /**
  29. * 机器总数量
  30. */
  31. private int goodAmount = 100;
  32. /**
  33. * 并发量
  34. */
  35. private int threadNum = 200;
  36.  
  37. //销售量
  38. private int goodSale = 0;
  39.  
  40. //买成功的数量
  41. private int accountNum = 0;
  42. //买成功的人的ID集合
  43. private List<Integer> successUsers = new ArrayList<>();
  44.  
  45. private GoodInfo goodInfo;
  46.  
  47. /*当创建 CountDownLatch 对象时,对象使用构造函数的参数来初始化内部计数器。每次调用 countDown() 方法,
  48. CountDownLatch 对象内部计数器减一。当内部计数器达到0时, CountDownLatch 对象唤醒全部使用 await() 方法睡眠的线程们。*/
  49. private CountDownLatch countDownLatch = new CountDownLatch(threadNum);
  50.  
  51. @Test
  52. public void contextLoads() {
  53. for (int i = 0; i < threadNum; i++) {
  54. new Thread(new UserRequest(goodCode, 7, i)).start();
  55. countDownLatch.countDown();
  56. }
  57.  
  58. //让主线程等待200个线程执行完,休息2秒,不休息的话200条线程还没执行完,就打印了
  59. try {
  60. Thread.sleep(2000);
  61. } catch (InterruptedException e) {
  62. e.printStackTrace();
  63. }
  64. System.out.println("-----------购买成功的用户数量----------为" + accountNum);
  65. System.out.println("-----------销售量--------------------为" + goodSale);
  66. System.out.println("-----------剩余数量------------------为" + (goodAmount - goodSale));
  67. System.out.println(successUsers);
  68. }
  69.  
  70. private class UserRequest implements Runnable {
  71.  
  72. private String code;
  73. private int buyCount;
  74. private int userId;
  75.  
  76. public UserRequest(String code, int buyCount, int userId) {
  77. this.code = code;
  78. this.buyCount = buyCount;
  79. this.userId = userId;
  80. }
  81.  
  82. @Override
  83. public void run() {
  84. try {
  85. //让线程等待,等200个线程创建完一起执行
  86. countDownLatch.await();
  87. } catch (InterruptedException e) {
  88. e.printStackTrace();
  89. }
  90. //如果更新数据库成功,也就代表购买成功了
  91. if (service.update(code, buyCount) > 0) {
  92. //对service加锁,因为很多线程在访问同一个service对象,不加锁将导致购买成功的人数少于预期,且数量不对,可自行测试
  93. synchronized (service) {
  94. //销售量
  95. goodSale += buyCount;
  96. accountNum++;
  97. //收录购买成功的人
  98. successUsers.add(userId);
  99. }
  100. }
  101. }
  102. }
  103.  
  104. @Before
  105. public void add() {
  106. goodInfo = new GoodInfo();
  107. goodInfo.setCode(goodCode);
  108. goodInfo.setAmount(goodAmount);
  109. service.add(goodInfo);
  110. }
  111.  
  112. @After
  113. public void delete() {
  114. service.delete(goodInfo);
  115. }
  116.  
  117. }

注意,由于是模拟并发,需要保证200个线程同时启动去访问数据库,所以使用了CountDownLatch类,在调用UserRequest线程的start方法后,会先进入await状态,等待200个线程创建完毕后,一起执行。

注意,由于是多线程操作service,必然导致数据不同步,所以需要对service加synchronize锁,来保证service的update方法能够正确执行。如果不加,可以自行测试,会导致少卖。

运行该测试类,看打印的结果。





可以多次运行,并修改每个人的购买数量、总商品数量、线程数,看看结果是否正确。

如修改为每人购买8个

mysql支持的并发访问量有限,倘若并发量较小,可以采用上面的update的sql就能控制住,倘若量大,可以考虑使用nosql。

下一篇讲一下redis模拟的方式。

1 秒杀系统模拟基础实现,使用DB实现的更多相关文章

  1. 2 秒杀系统模拟基础实现,使用Redis实现

    这一篇,我们来使用redis进行数据存储. 新建一个redis的service实现类 package com.tianyalei.service; import com.tianyalei.model ...

  2. 谈一下关于CQRS架构如何实现高性能

    CQRS架构简介 前不久,看到博客园一位园友写了一篇文章,其中的观点是,要想高性能,需要尽量:避开网络开销(IO),避开海量数据,避开资源争夺.对于这3点,我觉得很有道理.所以也想谈一下,CQRS架构 ...

  3. CQRS架构如何实现高性能

    CQRS架构如何实现高性能 CQRS架构简介 前不久,看到博客园一位园友写了一篇文章,其中的观点是,要想高性能,需要尽量:避开网络开销(IO),避开海量数据,避开资源争夺.对于这3点,我觉得很有道理. ...

  4. MongoDB(NoSQL) 非关系型数据库

    目录 简单了解 mongoDB 简单使用mongoDB 简单了解 mongoDB # NoSQL 泛指非关系型的数据库 NoSQL(NoSQL = Not Only SQL ),意即"不仅仅 ...

  5. mongodb 性能篇

    一.  索引及其优化 索引的概述 数据库的索引好比是一本书前面的目录,能加快数据查询的速度. 适当的地方增加索引,不合理的地方删除次优索引,能优化性能较差的应用. 索引的操作 基础索引:db.ken. ...

  6. MongoDB常用命令

    本文整理了一年多以来我常用的MongoDB操作,涉及mongo-shell.pymongo,既有运维层面也有应用层面,内容有浅有深,这也就是我从零到熟练的历程. MongoDB的使用之前也分享过一篇, ...

  7. MongoDB使用小结:一些常用操作分享

    本文整理了一年多以来我常用的MongoDB操作,涉及mongo-shell.pymongo,既有运维层面也有应用层面,内容有浅有深,这也就是我从零到熟练的历程. MongoDB的使用之前也分享过一篇, ...

  8. VC++ ADO相关

    <VC对ADO的操作> ADO概述: ADO是Microsoft为最新和最强大的数据访问范例 OLE DB 而设计的,是一个便于使用的应用程序层接口. ADO 使您能够编写应用程序以通过 ...

  9. [MongoDB]Mongodb攻略

    -------------------------------------------------------------------------------------------- [基础] 1. ...

随机推荐

  1. Delphi 正则表达式语法(10): 选项

    Delphi 正则表达式语法(10): 选项 // preCaseLess: 不区分大小写, 相当于其他语言中的 i var   reg: TPerlRegEx; begin   reg := TPe ...

  2. Php DOMDocument 中的 formatOutput

    Nicely formats output with indentation and extra space 是否处理 缩进和多余的空白符

  3. 本地连不上远程mysql数据库(2)

    Host is not allowed to connect to this MySQL server解决方法 今天在ubuntu上面装完MySQL,却发现在本地登录可以,但是远程登录却报错Host ...

  4. 《Spring Boot 实战》随记

    第一部分 Spring 4.x 1. Spring基础 略过 2. Spring常用配置 2.1 Bean的scope 使用@Scope注解配置scope.默认signleton,原型模式protot ...

  5. Java Web架构总结

    转载至:http://www.cnblogs.com/wuxl360/p/7489763.html 初始搭建 开始的开始,就是各种框架一搭,然后扔到Tomcat容器中跑就是了,这时候我们的文件,数据库 ...

  6. python selenium firefox使用

    演示的版本信息如下: Python 3.6.0 Selenium  3.5.0 Firefox 55.0.3 geckodriver v1.0.18.0 win64 1.前提准备 1.1 安装pyth ...

  7. alias指令别名和 sshpass命令简化ssh登陆

     在之前的一篇博文中 ubuntu下关于profile和bashrc中环境变量的理解 提到过可以编辑bashrc文件,vim ~/.bashrc,来编写自己的小指令,就是给长指令取个简单的别名.比如b ...

  8. Jconsle

    1. jconsole 远程连接: JConsole很好用,可以解决很多疑难杂症.但远程连接需要设置一下Java opt才可以使用.以下是步骤: 1). 在java opt下添加如下内容: 如果是无须 ...

  9. Linux下SSH中配置说明

    SSH 协议:安全外壳协议.为 Secure Shell 的缩写.SSH 为建立在应用层和传输层基础上的安全协议. sshd服务使用SSH协议可以用来进行远程控制,或在计算机之间传送文件.而实现此功能 ...

  10. Text Justification,文本对齐

    问题描述:把一个集合的单词按照每行L个字符放,每行要两端对齐,如果空格不能均匀分布在所有间隔中,那么左边的空格要多于右边的空格,最后一行靠左对齐. words: ["This", ...