1. 程式人生 > 資料庫 >SpringBoot學習記錄(十四)-------- Spring Boot整合Redis開發:模擬高併發秒殺活動

SpringBoot學習記錄(十四)-------- Spring Boot整合Redis開發:模擬高併發秒殺活動

秒殺業務流程

使用者點選商品列表頁中的商品,進入商品詳情頁,如果秒殺成功,則跳轉訂單詳情頁。瞬間的併發非常大,系統很可能出現問題,瓶頸在資料庫(加快取,非同步化來減輕資料庫壓力,防止直接穿透到資料庫)

秒殺架構設計理念

限流:鑑於只有少部分使用者能夠秒殺成功,所以要限制大部分流量,只允許少部分流量進入服務後端。
削峰:對於秒殺系統瞬時會有大量使用者湧入,所以在搶購一開始會有很高的瞬間峰值。高峰值流量是壓垮系統很重要的原因,所以如何把瞬間的高流量變成一段時間平穩的流量也是設計秒殺系統很重要的思路。實現削峰的常用的方法有利用快取和訊息中介軟體等技術。
非同步處理:秒殺系統是一個高併發系統,採用非同步處理模式可以極大地提高系統併發量,其實非同步處理就是削峰的一種實現方式。

記憶體快取:秒殺系統最大的瓶頸一般都是資料庫讀寫,由於資料庫讀寫屬於磁碟IO,效能很低,如果能夠把部分資料或業務邏輯轉移到記憶體快取,效率會有極大地提升。
可拓展:當然如果我們想支援更多使用者,更大的併發,最好就將系統設計成彈性可拓展的,如果流量來了,拓展機器就好了。像淘寶、京東等雙十一活動時會增加大量機器應對交易高峰。

秒殺系統架構設計思路

將請求攔截在系統上游,降低下游壓力:秒殺系統特點是併發量極大,但實際秒殺成功的請求數量卻很少,所以如果不在前端攔截很可能造成資料庫讀寫鎖衝突,最終請求超時。
利用快取:利用快取可極大提高系統讀寫速度。
訊息佇列:訊息佇列可以削峰,將攔截大量併發請求,這也是一個非同步處理過程,後臺業務根據自己的處理能力,從訊息佇列中主動的拉取請求訊息進行業務處理。

測試教程

第一步:

建立Redis測試應用springboot-redis,引入Redis相關依賴,pom.xml配置資訊如下:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson</artifactId>
        <version>3.4.3</version>
    </dependency>
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.9.0</version>
    </dependency>
</dependencies>

因為這裡需要介紹三種方式來解決秒殺超賣的問題,所以引入了三個Redis依賴。

第二步:

修改專案的application.yml配置檔案,主要配置應用資訊和redis服務資訊,配置如下:

server:
  port: 8908

spring:
  application:
    name: springboot-redis
  jackson:
    # 指定時間格式
    date-format: 'yyyy-MM-dd HH:mm:ss'
    # 排除結果中屬性值是 null 的屬性
    default-property-inclusion: non_null
  redis:
    database: 0 #Redis資料庫索引(預設為0)
    host: 127.0.0.1 #Redis伺服器地址
    port: 6379 #Redis伺服器連線埠
    password: # Redis伺服器連線密碼(預設為空)
    timeout: 5000 #連線超時時間(毫秒)
    jedis:
      pool:
        max-active: 8 #連線池最大連線數(使用負值表示沒有限制)
        max-wait: -1 #連線池最大阻塞等待時間(使用負值表示沒有限制)
        max-idle: 8 #連線池中的最大空閒連線
        min-idle: 0 #連線池中的最小空閒連線

第三步:

建立Redisson配置類,程式碼如下:

package com.test.redis.config;

import io.netty.channel.nio.NioEventLoopGroup;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.Codec;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ClassUtils;

@Configuration
public class RedissonConfig {

    private String address = "redis://127.0.0.1:6379";
    private int connectionMinimumIdleSize = 10;
    private int idleConnectionTimeout = 10000;
    private int pingTimeout = 1000;
    private int connectTimeout = 10000;
    private int timeout = 5000;
    private int retryAttempts = 3;
    private int retryInterval = 1500;
    private int reconnectionTimeout = 3000;
    private int failedAttempts = 3;
    private String password = null;
    private int subscriptionsPerConnection = 5;
    private String clientName = null;
    private int subscriptionConnectionMinimumIdleSize = 1;
    private int subscriptionConnectionPoolSize = 50;
    private int connectionPoolSize = 64;
    private int database = 1;
    private boolean dnsMonitoring = false;
    private int dnsMonitoringInterval = 5000;

    private int thread; //當前處理核數量 * 2

    private String codec = "org.redisson.codec.JsonJacksonCodec";

    @Bean(destroyMethod = "shutdown")
    RedissonClient redisson() throws Exception {
        Config config = new Config();
        config.useSingleServer().setAddress(address)
                .setConnectionMinimumIdleSize(connectionMinimumIdleSize)
                .setConnectionPoolSize(connectionPoolSize)
                .setDatabase(database)
                .setDnsMonitoring(dnsMonitoring)
                .setDnsMonitoringInterval(dnsMonitoringInterval)
                .setSubscriptionConnectionMinimumIdleSize(subscriptionConnectionMinimumIdleSize)
                .setSubscriptionConnectionPoolSize(subscriptionConnectionPoolSize)
                .setSubscriptionsPerConnection(subscriptionsPerConnection)
                .setClientName(clientName)
                .setFailedAttempts(failedAttempts)
                .setRetryAttempts(retryAttempts)
                .setRetryInterval(retryInterval)
                .setReconnectionTimeout(reconnectionTimeout)
                .setTimeout(timeout)
                .setConnectTimeout(connectTimeout)
                .setIdleConnectionTimeout(idleConnectionTimeout)
                .setPingTimeout(pingTimeout)
                .setPassword(password);
        Codec codec = (Codec) ClassUtils.forName("org.redisson.codec.JsonJacksonCodec",ClassUtils.getDefaultClassLoader()).newInstance();
        config.setCodec(codec);
        config.setThreads(thread);
        config.setEventLoopGroup(new NioEventLoopGroup());
        config.setUseLinuxNativeEpoll(false);
        return Redisson.create(config);
    }
}

第四步:

建立秒殺服務介面,包含秒殺處理方法,程式碼如下:

package com.test.redis.controller;

import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

@Slf4j
@RestController
@RequestMapping("/api")
public class SeckillController {

    @Resource(name = "stringRedisTemplate")
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private RedissonClient redissonClient;
    /**
     * 購買成功數量
     */
    private AtomicInteger sellCount = new AtomicInteger(0);

    /**
     * 初始化商品庫存數量
     * @return
     */
    @GetMapping("/initcount")
    public String initcount() {
        stringRedisTemplate.opsForValue().set("product_count","5");
        sellCount.set(0);
        return "初始化庫存成功";
    }

    /**
     * 加入事務的減少庫存方式
     * @return
     */
    @GetMapping("/sell1")
    public String sell1() {
        stringRedisTemplate.setEnableTransactionSupport(true);
        List<Object> results = stringRedisTemplate.execute(new SessionCallback<List<Object>>() {
            @Override
            public List<Object> execute(RedisOperations operations) throws DataAccessException {
                operations.watch("product_count");
                String product_count = (String) operations.opsForValue().get("product_count");
                operations.multi();
                operations.opsForValue().get("product_count");
                Integer productCount = Integer.parseInt(product_count);
                productCount = productCount - 1;
                if (productCount < 0) {
                    return null;
                }
                operations.opsForValue().set("product_count",productCount.toString());
                return operations.exec();
            }
        });

        if (results != null && results.size() > 0) {
            return "減少庫存成功,共減少" + sellCount.incrementAndGet();
        }
        return "庫存不足";
    }


    /**
     * 直接用jredis加入事務的減少庫存方式
     * @return
     */
    @GetMapping("/sell2")
    public String reduceSku3() {
        Jedis jedis = new Jedis("127.0.0.1",6379);
        List<Object> result;
        Transaction transaction = null;
        try {
            jedis.watch("product_count");
            int product_count = Integer.parseInt(jedis.get("product_count"));
            if (product_count > 0) {
                transaction = jedis.multi();
                transaction.set("product_count",String.valueOf(product_count - 1));
                result = transaction.exec();
                if (result == null || result.isEmpty()) {
                    log.error("Transaction error...");  //可能是watch-key被外部修改,或者是資料操作被駁回
                    //transaction.discard();  //watch-key被外部修改時,discard操作會被自動觸發
                    return "Transaction error...";
                }
            } else {
                return "庫存不足";
            }
            return "減少庫存成功,共減少" + sellCount.incrementAndGet();
        } catch (Exception e) {
            log.error(e.getMessage());
            transaction.discard();
            return "fail";
        }
    }

    /**
     * 通過加鎖方式減少庫存方式
     * @return
     */
    @GetMapping("/sell3")
    public String sell3() {
        RLock rLock = redissonClient.getLock("product_count");
        try {
            rLock.lock();
            Integer product_count = Integer.parseInt(stringRedisTemplate.opsForValue().get("product_count"));
            product_count = product_count - 1;
            if (product_count < 0) {
                return "庫存不足";
            }
            stringRedisTemplate.opsForValue().set("product_count",product_count.toString());
            return "減少庫存成功,共減少" + sellCount.incrementAndGet();
        } finally {
            rLock.unlock();
        }
    }

    /**
     * 銷售成功的數量
     * @return
     */
    @GetMapping("/sellcount")
    public String sellcount() {
        return "成功搶到的商品數量:" + sellCount.get();
    }

}

程式碼解析:

先初始化商品庫存數量

通過三種防止超賣的方法銷售商品

檢視商品銷售成功的數量

使用spring的redisTemplate執行事務,需要在開啟事務後執行一個redis的查詢操作(非真實值),原因是:

spring對redis事務的exec()方法返回結果做了處理(把返回值的 OK結果刪掉)。

導致在事務中只有set等更新操作時,事務執行失敗與成功返回的結果一樣。

事務過程中查詢redis的值只會在事務執行成功後才放回。而在事務執行過程中只會返回null

第五步:

接下來啟動應用與Redis服務,應用啟動成功後開啟瀏覽器,訪問服務介面順序如下:

商品初始化介面:http://127.0.0.1:8908/api/initcount

減少庫存數量(Redis事務):http://127.0.0.1:8908/api/sell1

減少庫存數量(Jredis加事務):http://127.0.0.1:8908/api/sell2

減少庫存數量(Redisson鎖):http://127.0.0.1:8908/api/sell3

商品銷售成功數量:http://127.0.0.1:8908/api/sellcount

介面訪問返回資料順序如下圖:

在這裡插入圖片描述在這裡插入圖片描述在這裡插入圖片描述在這裡插入圖片描述
在這裡插入圖片描述
開啟Redis桌面管理軟體,檢視商品庫存資訊,可以看到商品庫存正常,如下圖:
在這裡插入圖片描述

以上就是今天為大家講解的高併發秒殺活動測試,通過三種方法來解決商品超賣問題,通過Jmeter測試軟體來模擬1000個請求,最後測試結果商品庫存數量也是為0,成功銷售數量為5,可以見三種防止超賣的方法都可以。
轉載地址: https://mp.weixin.qq.com/s/o_C7JHi78qmKRZuYjdImew