SpringBoot+Redis實現實現Java高併發之秒殺系統

基於SpringBoot

  1. 實現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配置而已。

測試代碼的頁面效果

進入首頁
在這裏插入圖片描述

點擊下面的立即搶購
在這裏插入圖片描述

在這裏插入圖片描述
在這裏插入圖片描述
碼雲源碼

發佈了4 篇原創文章 · 獲贊 6 · 訪問量 422
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章