基於SpringBoot
- 實現Java高併發之秒殺系統
1、技術棧
後端: SpringBoot + Redis
前端: Bootstrap + Jquery
2、測試環境
IDEA + Maven+ Tomcat8.5 + JDK8
3、下載redis
Redis下載地址
4、基本流程圖
Spring的聲明式事務通過:傳播行爲、隔離級別、只讀提示、事務超時、回滾規則來進行定義。
配置pom.xml文件
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- alibaba的druid數據庫連接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency>
<!-- redis客戶端 小白用的是服務器上的-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
配置application.yml文件
server:
port: 8080
spring:
datasource:
name: springboot
type: com.alibaba.druid.pool.DruidDataSource
#druid相關配置
druid:
#監控統計攔截的filters
filter: stat
#mysql驅動
driver-class-name: com.mysql.jdbc.Driver
#基本屬性
url: jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&?zeroDateTimeBehavior=convertToNull
username: root
password: sasa
#配置初始化大小/最小/最大
initial-size: 1
min-idle: 1
max-active: 20
#獲取連接等待超時時間
max-wait: 60000
#間隔多久進行一次檢測,檢測需要關閉的空閒連接
time-between-eviction-runs-millis: 60000
thymeleaf:
prefix: classpath:/templates/
check-template-location: true
suffix: .html
encoding: UTF-8
mode: LEGACYHTML5
cache: false
#文件上傳相關設置
servlet:
multipart:
max-file-size: 10Mb
max-request-size: 100Mb
#devtools插件
devtools:
livereload:
enabled: true #是否支持livereload
port: 35729
restart:
enabled: true #是否支持熱部署
#redis緩存
redis:
#redis數據庫索引,默認是0
database: 0
#redis服務器地址
host: 39.105.174.56
# Redis服務器連接密碼(默認爲空)
password:
#redis服務器連接端口,默認是6379
port: 6379
# 連接超時時間(毫秒)
timeout: 1000
jedis:
pool:
# 連接池最大連接數(使用負值表示沒有限制)
max-active: 8
# 連接池最大阻塞等待時間(使用負值表示沒有限制
max-wait: -1
# 連接池中的最大空閒連接
max-idle: 8
# 連接池中的最小空閒連接
min-idle: 0
#mybatis配置
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: cn.tycoding.entity
configuration:
# 使用jdbc的getGeneratedKeys 可以獲取數據庫自增主鍵值
use-generated-keys: true
# 使用列別名替換列名,默認true。
use-column-label: true
# 開啓駝峯命名轉換
map-underscore-to-camel-case: true
# 打印sql
logging:
level:
cn.tycoding.mapper: DEBUG
創建數據庫seckill.sql文件
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for seckill
-- ----------------------------
DROP TABLE IF EXISTS `seckill`;
CREATE TABLE `seckill` (
`seckill_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`title` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品標題',
`image` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品圖片',
`price` decimal(10, 2) NULL DEFAULT NULL COMMENT '商品原價格',
`cost_price` decimal(10, 2) NULL DEFAULT NULL COMMENT '商品秒殺價格',
`stock_count` bigint(20) NULL DEFAULT NULL COMMENT '剩餘庫存數量',
`start_time` timestamp(0) NOT NULL DEFAULT '1970-02-01 00:00:01' COMMENT '秒殺開始時間',
`end_time` timestamp(0) NOT NULL DEFAULT '1970-02-01 00:00:01' COMMENT '秒殺結束時間',
`create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
PRIMARY KEY (`seckill_id`) USING BTREE,
INDEX `idx_start_time`(`start_time`) USING BTREE,
INDEX `idx_end_time`(`end_time`) USING BTREE,
INDEX `idx_create_time`(`end_time`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '秒殺商品表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of seckill
-- ----------------------------
INSERT INTO `seckill` VALUES (1, 'Apple/蘋果 iPhone 6s Plus 國行原裝蘋果6sp 5.5寸全網通4G手機', 'https://g-search3.alicdn.com/img/bao/uploaded/i4/i3/2249262840/O1CN011WqlHkrSuPEiHxd_!!2249262840.jpg_230x230.jpg', 2600.00, 1100.00, 9, '2019-12-22 16:30:00', '2019-12-22 23:30:00', '2019-12-22 21:12:46');
INSERT INTO `seckill` VALUES (2, 'ins新款連帽毛領棉襖寬鬆棉衣女冬外套學生棉服', 'https://gw.alicdn.com/bao/uploaded/i3/2007932029/TB1vdlyaVzqK1RjSZFzXXXjrpXa_!!0-item_pic.jpg_180x180xz.jpg', 200.00, 150.00, 10, '2019-12-22 16:30:00', '2019-12-22 23:30:00', '2019-12-22 21:12:46');
INSERT INTO `seckill` VALUES (3, '可愛超萌兔子毛絨玩具垂耳兔公仔布娃娃睡覺抱女孩玩偶大號女生 ', 'https://g-search3.alicdn.com/img/bao/uploaded/i4/i2/3828650009/TB22CvKkeOSBuNjy0FdXXbDnVXa_!!3828650009.jpg_230x230.jpg', 160.00, 130.00, 20, '2019-12-22 16:30:00', '2019-12-22 23:30:00', '2019-12-22 21:12:46');
-- ----------------------------
-- Table structure for seckill_order
-- ----------------------------
DROP TABLE IF EXISTS `seckill_order`;
CREATE TABLE `seckill_order` (
`seckill_id` bigint(20) NOT NULL COMMENT '秒殺商品ID',
`money` decimal(10, 2) NULL DEFAULT NULL COMMENT '支付金額',
`user_phone` bigint(20) NOT NULL COMMENT '用戶手機號',
`create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '創建時間',
`state` tinyint(4) NOT NULL DEFAULT -1 COMMENT '狀態:-1無效 0成功 1已付款',
PRIMARY KEY (`seckill_id`, `user_phone`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '秒殺訂單表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of seckill_order
-- ----------------------------
INSERT INTO `seckill_order` VALUES (1, 1100.00, 15173117830, '2019-12-22 22:00:40', -1);
SET FOREIGN_KEY_CHECKS = 1;
整個項目結構
隔離級別
聲明式事務的第二個維度就是隔離級別。隔離級別定義了一個事務可能受其他併發事務影響的程度。多個事務併發運行,經常會操作相同的數據來完成各自的任務,但是可以回導致以下問題:
更新丟失:當多個事務選擇同一行操作,並且都是基於最初的選定的值,由於每個事務都不知道其他事務的存在,就會發生更新覆蓋的問題。
髒讀:事務A讀取了事務B已經修改但爲提交的數據。若事務B回滾數據,事務A的數據存在不一致的問題。
不可重複讀:書屋A第一次讀取最初數據,第二次讀取事務B已經提交的修改或刪除的數據。導致兩次數據讀取不一致。不符合事務的隔離性。
幻讀:事務A根據相同條件第二次查詢到的事務B提交的新增數據,兩次數據結果不一致,不符合事務的隔離性。
理想情況下,事務之間是完全隔離的,從而可以防止這些問題的發生。但是完全的隔離會導致性能問題,因爲它通常會涉及鎖定數據庫中的記錄。侵佔性的鎖定會阻礙併發性,要求事務互相等待以完成各自的工作。
因此爲了實現在事務隔離上有一定的靈活性。因此,就會有多重隔離級別:
隔離級別 | 含義 |
---|---|
ISOLATION_DEFAULT | 使用後端數據庫默認的隔離級別 |
SIOLATION_READ_UNCOMMITTED | 允許讀取尚未提交的數據變更。可能會導致髒讀、幻讀或不可重複讀 |
ISOLATION_READ_COMMITTED | 允許讀取併發事務提交的數據。可以阻止髒讀,但是幻讀或不可重複讀仍可能發生 |
ISOLATION_REPEATABLE_READ | 對同一字段的多次讀取結果是一致的,除非數據是被本事務自己所修改,可以阻止髒讀和不可重複讀,但幻讀仍可能發生 |
ISOLATION_SERIALIZABLE | 完全服從ACID的事務隔離級別,確保阻止髒讀、不可重複讀、幻讀。這是最慢的事務隔離級別,因爲它通常是通過完全鎖定事務相關的數據庫來實現的 |
回滾規則
pring的事務管理器默認是針對unchecked exception回滾,也就是默認對Error異常和RuntimeException異常以及其子類進行事務回滾。
也就是說事務只有在遇到運行期異常纔會回滾,而在遇到檢查型異常時不會回滾。
這也就是我們之前設計Service業務層邏輯的時候一再強調捕獲try catch異常,且將編譯期異常轉換爲運行期異常。
Redis緩存優化
配置JedisConfig序列化
package cn.tycoding.redis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
@Configuration
public class JedisConfig {
private Logger logger = LoggerFactory.getLogger(JedisConfig.class);
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.jedis.pool.max-active}")
private int maxActive;
@Value("${spring.redis.jedis.pool.max-idle}")
private int maxIdle;
@Value("${spring.redis.jedis.pool.min-idle}")
private int minIdle;
@Value("${spring.redis.jedis.pool.max-wait}")
private long maxWaitMillis;
@Bean
public JedisPool redisPoolFactory(){
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
jedisPoolConfig.setMaxTotal(maxActive);
jedisPoolConfig.setMinIdle(minIdle);
JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, null);
logger.info("JedisPool注入成功");
logger.info("redis地址:" + host + ":" + port);
return jedisPool;
}
}
這裏是爲了將我們在application.yml中配置的參數注入到JedisPool中,使用Spring的@Value註解能讀取到Spring配置文件中已經配置的參數的值
package cn.tycoding.redis;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
@Configuration
public class RedisTemplateConfig {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
logger.info("RedisTemplate序列化配置,轉化方式:" + jackson2JsonRedisSerializer.getClass().getName());
return redisTemplate;
}
}
注意
實現序列化目前而言不是必須的,因爲我們使用了Spring-data-redis提供的高度封裝的RedisTemplate模板類。
SpringBoot2.x實現Redis的序列化仍是由很多方案,但是我這裏使用了Spring-data-redis提供的一種jackson2JsonRedisSerializer的序列化方式。
如果不實現Redis的序列化,可以往Redis中存入數據,但是存入的key都是亂碼的,想要避免這一點就必須實現序列化。
這個步驟和我們之前整合SSM+Redis+Shiro+Solr框架中已經講到了用XML實現序列化配置,這裏僅是換成了Java配置而已。
測試代碼的頁面效果
進入首頁
點擊下面的立即搶購