Springboot+redis+Vue實(shí)現(xiàn)秒殺的項(xiàng)目實(shí)踐
1、Redis簡(jiǎn)介
Redis是一個(gè)開(kāi)源的key-value存儲(chǔ)系統(tǒng)。
Redis的五種基本類型:String(字符串),list(鏈表),set(集合),zset(有序集合),hash,stream(Redis5.0后的新數(shù)據(jù)結(jié)構(gòu))
這些數(shù)據(jù)類型都支持push/pop、add/remove及取交集并集和差集及更豐富的操作,而且這些操作都是原子性的。
Redis的應(yīng)用場(chǎng)景為配合關(guān)系型數(shù)據(jù)庫(kù)做高速緩存,降低數(shù)據(jù)庫(kù)IO
需要注意的是,Redis是單線程的,如果一次批量處理命令過(guò)多,會(huì)造成Redis阻塞或網(wǎng)絡(luò)擁塞(傳輸數(shù)據(jù)量大)
2、實(shí)現(xiàn)代碼
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服務(wù)器地址 spring.redis.host=192.168.1.2 #Redis服務(wù)器連接端口 spring.redis.port=6379 #Redis數(shù)據(jù)庫(kù)索引(默認(rèn)為0) spring.redis.database=0 #連接超時(shí)時(shí)間(毫秒) spring.redis.timeout=1800000 #連接池最大連接數(shù)(使用負(fù)值表示沒(méi)有限制) spring.redis.lettuce.pool.max-active=20 #最大阻塞等待時(shí)間(負(fù)數(shù)表示沒(méi)限制) spring.redis.lettuce.pool.max-wait=-1 #連接池中的最大空閑連接 spring.redis.lettuce.pool.max-idle=5 #連接池中的最小空閑連接 spring.redis.lettuce.pool.min-idle=0 # 關(guān)閉超時(shí)時(shí)間 #pring.redis.lettuce.shutdown-timeout=100 #配置spring啟動(dòng)端口號(hào) 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> <!-- 開(kāi)發(fā)環(huán)境版本,包含了有幫助的命令行警告 --> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <!-- 官網(wǎng)提供的 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("發(fā)生錯(cuò)誤"); }); }, prov:function(){ this.index--; }, next:function(){ this.index++; } } }); </script> </body> </html>
相關(guān)配置類
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); //解決查詢緩存轉(zhuǎn)換異常的問(wèn)題 ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); // 配置序列化(解決亂碼的問(wèn)題),過(guò)期時(shí)間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獲取后端接口數(shù)據(jù)時(shí)出現(xiàn)的跨域請(qǐng)求問(wèn)題。
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: 解決跨域問(wèn)題(接口是http,而axios一般請(qǐng)求的是https。從https到http是跨域,因此要配置跨域請(qǐng)求) */ @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()); //注冊(cè) return new CorsFilter(source); } }
服務(wù)層
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"; //庫(kù)存 String userKey = "sk:" + productId + ":user"; //秒殺成功的用戶 //3、獲取庫(kù)存 String kc = String.valueOf(redisTemplate.opsForValue().get(kcKey)) ; if (kc == null) { System.out.println("秒殺還沒(méi)有開(kāi)始,請(qǐng)等待"); return false; } //4、判斷用戶是否已經(jīng)秒殺成功過(guò)了 if (redisTemplate.opsForSet().isMember(userKey, uid)) { System.out.println("已秒殺成功,不能重復(fù)秒殺"); return false; } //5、如果庫(kù)存數(shù)量小于1,秒殺結(jié)束 if (Integer.parseInt(kc) <=0) { System.out.println("秒殺結(jié)束"); return false; } //6、秒殺過(guò)程 redisTemplate.opsForValue().decrement(kcKey); //庫(kù)存數(shù)量減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 //自動(dòng)返回json格式的數(shù)據(jù) 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); //使用隨機(jī)數(shù)生成用戶ID String uid = String.valueOf(id) + " "; boolean flag = secKillService.doSecKill(uid, productId+index); System.out.println(flag); return flag; } }
啟動(dòng)類
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、啟動(dòng)步驟
因?yàn)橐还灿腥唐芬霘ⅲ栽趓edis里面設(shè)置三個(gè)商品的庫(kù)存數(shù)量。這里數(shù)量都設(shè)置為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能夠被訪問(wèn),要確保關(guān)閉linux的防火墻,以及關(guān)閉redis的保護(hù)模式。
vim redis.conf --打開(kāi)redis配置 service iptables stop? --關(guān)閉防火墻 //關(guān)閉redis保護(hù)模式 redis-cli --進(jìn)入redis客戶端 config set protected-mode "no" --配置里面關(guān)閉redis保護(hù)模式(只是進(jìn)入redis.conf把protected-mode變?yōu)閚o是不行的,還要加一句config set protected-mode "no"
啟動(dòng)springboot項(xiàng)目
秒殺成功后,該商品在redis中的數(shù)量就減1。
當(dāng)數(shù)量減為0時(shí),則提示“搶光了”。
4、使用ab進(jìn)行并發(fā)測(cè)試
如果是centOS 6版本的linux都是默認(rèn)按照了ab工具的。
如果沒(méi)有安裝ab工具,可在linux終端用命令聯(lián)網(wǎng)下載安裝。
yum install httpd-tools
安裝完成后,就可以使用ab工具進(jìn)行并發(fā)測(cè)試了。
在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這個(gè)商品庫(kù)存變?yōu)?了
5、線程安全
為了防止出現(xiàn)“超買”的現(xiàn)象,需要讓操作redis的方法是線程安全的(即在方法上加上一個(gè)“悲觀鎖”synchronized)。
如果不加synchronized就會(huì)出現(xiàn)“超買”現(xiàn)象,即redis庫(kù)存會(huì)出現(xiàn)負(fù)數(shù)
之所以產(chǎn)生這種現(xiàn)象是由于并發(fā)導(dǎo)致多個(gè)用戶同時(shí)調(diào)用了doSecKill()方法,多個(gè)用戶同時(shí)修改了redis中的sk:012342:qt的值,但暫時(shí)都沒(méi)有提交存入到redis中去。等到后來(lái)一起提交,導(dǎo)致了sk:012342:qt的值被修改了多次,因此會(huì)出現(xiàn)負(fù)數(shù)。
因此在doSecKill()方法加上悲觀鎖,用戶調(diào)用該方法就對(duì)該方法加鎖,修改了sk:012342:qt的值后并提交存入redis中之后,才會(huì)釋放鎖。其他用戶才能得到鎖并操作該方法。
6、總結(jié)
redis作為一種Nosql的非關(guān)系型數(shù)據(jù)庫(kù),因?yàn)槠鋯螌?shí)例,簡(jiǎn)單高效的特性通常會(huì)被作為其他關(guān)系型數(shù)據(jù)庫(kù)的高速緩存。尤其是在秒殺這樣的高并發(fā)操作。先將要秒殺的商品信息從數(shù)據(jù)庫(kù)讀入到redis中,秒殺的過(guò)程實(shí)際上是在與redis進(jìn)行交互。等秒殺完成后再將秒殺的結(jié)果存入數(shù)據(jù)庫(kù)??梢杂行Ы档徒档蛿?shù)據(jù)庫(kù)IO,防止數(shù)據(jù)庫(kù)宕機(jī)。
7、參考資料
https://www.bilibili.com/video/BV1Rv41177Af?p=27
https://www.cnblogs.com/taiyonghai/p/5810150.html
到此這篇關(guān)于Springboot+redis+Vue實(shí)現(xiàn)秒殺的項(xiàng)目實(shí)踐的文章就介紹到這了,更多相關(guān)Springboot+redis+Vue 秒殺內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java如何獲取數(shù)組和字符串的長(zhǎng)度(length還是length())
這篇文章主要介紹了Java如何獲取數(shù)組和字符串的長(zhǎng)度(length還是length()),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12springboot 異步調(diào)用的實(shí)現(xiàn)方法
這篇文章主要介紹了springboot 異步調(diào)用的實(shí)現(xiàn)方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-04-04將應(yīng)用程序進(jìn)行Spring6遷移的最佳使用方式
這篇文章主要介紹了將應(yīng)用程序進(jìn)行Spring6遷移的最佳方式,以及如何充分利用此升級(jí),需要的朋友可以參考下,如有錯(cuò)誤的地方還請(qǐng)指正2023-03-03SpringBoot對(duì)Druid配置SQL監(jiān)控功能失效問(wèn)題及解決方法
這篇文章主要介紹了SpringBoot對(duì)Druid配置SQL監(jiān)控功能失效問(wèn)題的解決方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-05-05工作中禁止使用Executors快捷創(chuàng)建線程池原理詳解
這篇文章主要為大家介紹了工作中禁止使用Executors快捷創(chuàng)建線程池原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11Java 時(shí)間格式轉(zhuǎn)換之impleDateFormat與Data API解析與使用
想必大家對(duì) SimpleDateFormat 并不陌生。SimpleDateFormat 是 Java 中一個(gè)非常常用的類,他是以區(qū)域敏感的方式格式化和解析日期的具體類。 它允許格式化 (date -> text)、語(yǔ)法分析 (text -> date)和標(biāo)準(zhǔn)化2021-11-11