Springboot+redis+Vue實現秒殺的項目實踐

1、Redis簡介

Redis是一個開源的key-value存儲系統。

Redis的五種基本類型:String(字符串),list(鏈表),set(集合),zset(有序集合),hash,stream(Redis5.0後的新數據結構)

這些數據類型都支持push/pop、add/remove及取交集並集和差集及更豐富的操作,而且這些操作都是原子性的。

Redis的應用場景為配合關系型數據庫做高速緩存,降低數據庫IO

需要註意的是,Redis是單線程的,如果一次批量處理命令過多,會造成Redis阻塞或網絡擁塞(傳輸數據量大)

2、實現代碼

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>org.example</groupId>
    <artifactId>seckill</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>2.2.1.RELEASE</version>
            <scope>test</scope>
        </dependency>

        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- spring2.X集成redis所需common-pool2-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.6.0</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.24</version>
        </dependency>

    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <fork>true</fork>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

application.properties.xml

#Redis服務器地址
spring.redis.host=192.168.1.2
#Redis服務器連接端口
spring.redis.port=6379
#Redis數據庫索引(默認為0)
spring.redis.database=0
#連接超時時間(毫秒)
spring.redis.timeout=1800000
#連接池最大連接數(使用負值表示沒有限制)
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待時間(負數表示沒限制)
spring.redis.lettuce.pool.max-wait=-1
#連接池中的最大空閑連接
spring.redis.lettuce.pool.max-idle=5
#連接池中的最小空閑連接
spring.redis.lettuce.pool.min-idle=0
# 關閉超時時間
#pring.redis.lettuce.shutdown-timeout=100
#配置spring啟動端口號
server.port=8080

前端界面

seckillpage.html

<!DOCTYPE html>
<html lang="en" xmlns:v-on="http://www.w3.org/1999/xhtml" xmlns:v-bind="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>redis秒殺</title>
</head>
<body>
<!-- 開發環境版本,包含瞭有幫助的命令行警告 -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- 官網提供的 axios 在線地址 -->
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.20.0-0/axios.min.js"></script>
<div id="app">
    <h1>商品1元秒殺</h1>
    <!-- 左箭頭 -->
    <input type="button" value="<" v-on:click="prov" v-show="this.index>-1"/>
    <img v-bind:src="imgArr[index]" width="200px" />
    <!-- 右箭頭 -->
    <input type="button" value=">" v-on:click="next" v-show="this.index<2"/><br>
    <input type="button" v-on:click="seckill" value="秒 殺"/>
</div>
<script>
    var app = new Vue({
        el: "#app",
        data: {
            productId: "01234",
            imgArr:[
                "/image/phone1.png",
                "/image/phone2.png",
                "/image/phone3.png",
            ],
            index:0
        },
        methods: {
            seckill: function () {
                let param = new URLSearchParams()
                param.append('productId', this.productId)
                param.append('index', this.index)
                axios({
                    method: 'post',
                    url: 'http://192.168.1.6:8080/index/doSeckill',
                    data: param
                }).then(function (response) {
               
                    if (response.data == true) {
                        alert("秒殺成功");
                    } else {
                        alert("搶光瞭");
                    }
                 
                },
                    function(error){
                    alert("發生錯誤");
                });

            },

            prov:function(){
                this.index--;
            },
            next:function(){
                this.index++;
            }
        }
    });

</script>
</body>
</html>

相關配置類

Redis配置類

RedisConfig.java

package com.springboot_redis_seckill.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

/**
 * @author WuL2
 * @create 2021-05-27 14:26
 * @desc
 **/
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setConnectionFactory(factory);
        template.setKeySerializer(redisSerializer); //key序列化方式
        template.setValueSerializer(jackson2JsonRedisSerializer); //value序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer); //value hashmap序列化
        return template;
    }

    @Bean(name = "cacheManager")
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //解決查詢緩存轉換異常的問題
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化(解決亂碼的問題),過期時間600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}

配置Vue獲取後端接口數據時出現的跨域請求問題。

CorsConfig.java

package com.springboot_redis_seckill.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;


/**
 * @author: wu linchun
 * @time: 2021/5/28 22:22
 * @description: 解決跨域問題(接口是http,而axios一般請求的是https。從https到http是跨域,因此要配置跨域請求)
 */

@Configuration
public class CorsConfig {
    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*"); //允許任何域名
        corsConfiguration.addAllowedHeader("*"); //允許任何頭
        corsConfiguration.addAllowedMethod("*"); //允許任何方法
        return corsConfiguration;
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", buildConfig()); //註冊
        return new CorsFilter(source);
    }

}

服務層

SecKillService.java

package com.springboot_redis_seckill.service;

public interface SecKillService {
    public boolean doSecKill(String uid,String productId);
}

SecKillServiceImpl.java

package com.springboot_redis_seckill.service.impl;

import com.springboot_redis_seckill.service.SecKillService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

/**
 * @author WuL2
 * @create 2021-05-27 14:53
 * @desc
 **/
@Service
public class SecKillServiceImpl implements SecKillService {
    @Autowired
    @Qualifier("redisTemplate")
    private RedisTemplate redisTemplate;

    @Override
    public synchronized boolean doSecKill(String uid, String productId) {
        //1、uid和productId非空判斷
        if (uid == null || productId == null) {
            return false;
        }

        //2、拼接key
        String kcKey = "sk:" + productId + ":qt";   //庫存
        String userKey = "sk:" + productId + ":user";  //秒殺成功的用戶

        //3、獲取庫存
        String kc = String.valueOf(redisTemplate.opsForValue().get(kcKey)) ;
        if (kc == null) {
            System.out.println("秒殺還沒有開始,請等待");
            return false;
        }

        //4、判斷用戶是否已經秒殺成功過瞭
        if (redisTemplate.opsForSet().isMember(userKey, uid)) {
            System.out.println("已秒殺成功,不能重復秒殺");
            return false;
        }
        //5、如果庫存數量小於1,秒殺結束
        if (Integer.parseInt(kc) <=0) {
            System.out.println("秒殺結束");
            return false;
        }

        //6、秒殺過程
        redisTemplate.opsForValue().decrement(kcKey);  //庫存數量減1
        redisTemplate.opsForSet().add(userKey, uid);
        System.out.println("秒殺成功。。。");
        return true;
    }
}

控制層

package com.springboot_redis_seckill.controller;

import com.alibaba.fastjson.JSONObject;
import com.springboot_redis_seckill.service.SecKillService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Random;

/**
 * @author WuL2
 * @create 2021-05-27 14:28
 * @desc
 **/
@Controller
@RequestMapping("/index")
public class MyController {
    @Autowired
    @Qualifier("secKillServiceImpl")
    private SecKillService secKillService;

    @RequestMapping(value = {"/seckillpage"}, method = RequestMethod.GET)
    public String seckillpage() {
        return "/html/seckillpage.html";
    }


    @RequestMapping(value = {"/doSeckill"}, method = RequestMethod.POST)
    @ResponseBody  //自動返回json格式的數據
    public Object doSeckill(HttpServletRequest request, HttpServletResponse response) {
        System.out.println("doSeckill");
        String productId = request.getParameter("productId");  
        String index = request.getParameter("index");
        System.out.println(productId+index);  //拼接成為商品ID
        int id = new Random().nextInt(50000);  //使用隨機數生成用戶ID
        String uid = String.valueOf(id) + " ";
        boolean flag = secKillService.doSecKill(uid, productId+index);
        System.out.println(flag);
        return flag;
    }
}

啟動類

RunApplication.java

package com.springboot_redis_seckill;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author WuL2
 * @create 2021-05-27 14:32
 * @desc
 **/
@SpringBootApplication
public class RunApplication {
    public static void main(String[] args) {
        SpringApplication.run(RunApplication.class, args);
    }
}

3、啟動步驟

因為一共有三件商品要秒殺,所以在redis裡面設置三個商品的庫存數量。這裡數量都設置為10。

127.0.0.1:6379> set sk:012340:qt 10
OK
127.0.0.1:6379> set sk:012341:qt 10
OK
127.0.0.1:6379> set sk:012342:qt 10
OK

要確保redis能夠被訪問,要確保關閉linux的防火墻,以及關閉redis的保護模式。

vim redis.conf   --打開redis配置
 
service iptables stop   --關閉防火墻
 
 
//關閉redis保護模式
redis-cli    --進入redis客戶端
config set protected-mode "no"   --配置裡面關閉redis保護模式(隻是進入redis.conf把protected-mode變為no是不行的,還要加一句config set protected-mode "no"

啟動springboot項目

秒殺成功後,該商品在redis中的數量就減1。

當數量減為0時,則提示“搶光瞭”。

4、使用ab進行並發測試

如果是centOS 6版本的linux都是默認按照瞭ab工具的。

如果沒有安裝ab工具,可在linux終端用命令聯網下載安裝。

yum install httpd-tools

安裝完成後,就可以使用ab工具進行並發測試瞭。

在linux終端輸入如下命令:

ab -n 2000  -c 200 -p '/root/Desktop/post.txt' -T 'application/x-www-form-urlencoded' 'http://192.168.1.6:8080/index/doSeckill/'

012341這個商品庫存變為0瞭

5、線程安全

為瞭防止出現“超買”的現象,需要讓操作redis的方法是線程安全的(即在方法上加上一個“悲觀鎖”synchronized)。

如果不加synchronized就會出現“超買”現象,即redis庫存會出現負數

之所以產生這種現象是由於並發導致多個用戶同時調用瞭doSecKill()方法,多個用戶同時修改瞭redis中的sk:012342:qt的值,但暫時都沒有提交存入到redis中去。等到後來一起提交,導致瞭sk:012342:qt的值被修改瞭多次,因此會出現負數。

因此在doSecKill()方法加上悲觀鎖,用戶調用該方法就對該方法加鎖,修改瞭sk:012342:qt的值後並提交存入redis中之後,才會釋放鎖。其他用戶才能得到鎖並操作該方法。

6、總結

redis作為一種Nosql的非關系型數據庫,因為其單實例,簡單高效的特性通常會被作為其他關系型數據庫的高速緩存。尤其是在秒殺這樣的高並發操作。先將要秒殺的商品信息從數據庫讀入到redis中,秒殺的過程實際上是在與redis進行交互。等秒殺完成後再將秒殺的結果存入數據庫。可以有效降低降低數據庫IO,防止數據庫宕機。

7、參考資料

https://www.bilibili.com/video/BV1Rv41177Af?p=27

https://www.cnblogs.com/taiyonghai/p/5810150.html

到此這篇關於Springboot+redis+Vue實現秒殺的項目實踐的文章就介紹到這瞭,更多相關Springboot+redis+Vue 秒殺內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: