網(wǎng)站首頁 編程語言 正文
一般電商商品秒殺活動會面臨大量用戶同時(shí)下單的情況,不僅要面臨高并發(fā)的問題,還要保證下單數(shù)量不超過商品數(shù)量和用戶對同一商品不能重復(fù)下單(保證商品不被同一個用戶搶購?fù)辏簿褪欠傈S牛)。
面對這些問題,可以采用Redis分布鎖來解決,通過Redis中setnx命令來保證同一時(shí)間只有一個線程能夠正常下單,待訂單創(chuàng)建成功后解鎖,其余線程再來搶鎖。
首先模擬一下未采用Redis加鎖的代碼實(shí)現(xiàn),創(chuàng)建了3張表:用戶表、商品表和訂單表
?
?
maven依賴:
<dependencies>
<!--單元測試-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--集成mysql數(shù)據(jù)庫-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<!--springboot中的redis依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- lettuce pool 緩存連接池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<!--處理JSON格式-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions><!-- 去掉springboot默認(rèn)配置 -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- spring boot 2.3版本后,如果需要使用校驗(yàn),需手動導(dǎo)入validation包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency> <!-- 引入log4j2依賴 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<!-- redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
</dependencies>
Controller層:
package com.wl.demo.controller;
import cn.hutool.core.util.StrUtil;
import com.wl.demo.common.result.HttpResult;
import com.wl.demo.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author wl
* @date 2022/4/6
*/
@RestController
@RequestMapping("/order")
public class OrderController {
private final OrderService orderService;
@Autowired
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping("/add")
public HttpResult addOrder(@RequestBody String body) {
if (StrUtil.isBlank(body)) {
return HttpResult.fail("請求體不能為空");
}
return orderService.addOrder(body);
}
}
Service實(shí)現(xiàn)類:
package com.wl.demo.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wl.demo.common.result.HttpResult;
import com.wl.demo.entity.Order;
import com.wl.demo.mapper.OrderMapper;
import com.wl.demo.service.OrderService;
import com.wl.demo.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author wl
* @date 2022/4/6
*/
@Service
@Slf4j
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
private final ProductService productService;
@Autowired
public OrderServiceImpl(ProductService productService) {
this.productService = productService;
}
@Override
@Transactional(rollbackFor = Exception.class)
public HttpResult addOrder(String body) {
Order order = JSONObject.toJavaObject(JSONObject.parseObject(body), Order.class);
// 查詢訂單
Integer count = query().eq("user_id", order.getUserId()).eq("product_id", order.getProductId()).count();
if (count > 0) {
log.error("不允許重復(fù)下單");
return HttpResult.fail("不允許重復(fù)下單");
}
// 扣減庫存
boolean success = productService.update()
.setSql("stock = stock - 1")
.eq("product_id", order.getProductId()).gt("stock", 0) // where id = ? and stock > 0
.update();
if (!success) {
log.error("庫存不足");
return HttpResult.fail("庫存不足");
}
// 創(chuàng)建訂單
save(order);
return HttpResult.success();
}
}
啟動項(xiàng)目,打開jmeter模擬200個用戶(同一個)同時(shí)搶
?查看訂單表:
?商品表:
?雖然避免了下單數(shù)量多于商品庫存數(shù)量,但還是出現(xiàn)重復(fù)下單的情況。
接下來就輪到Redisson出場
首先定義Redisson配置類:
package com.wl.demo.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author wl
* @date 2022/4/6
*/
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redisClient() {
Config config = new Config();
//這里ip和port改成自己的redis地址和端口
config.useSingleServer().setAddress("redis://ip:port");
return Redisson.create(config);
}
}
然后修改剛剛的Service實(shí)現(xiàn)類方法:
package com.wl.demo.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wl.demo.common.result.HttpResult;
import com.wl.demo.entity.Order;
import com.wl.demo.mapper.OrderMapper;
import com.wl.demo.service.OrderService;
import com.wl.demo.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author wl
* @date 2022/4/6
*/
@Service
@Slf4j
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
private final static String LOCK_ORDER_KEY_PREFIX = "lock:order:";
private final RedissonClient redissonClient;
private final ProductService productService;
@Autowired
public OrderServiceImpl(RedissonClient redissonClient, ProductService productService) {
this.redissonClient = redissonClient;
this.productService = productService;
}
@Override
@Transactional(rollbackFor = Exception.class)
public HttpResult addOrder(String body) {
Order order = JSONObject.toJavaObject(JSONObject.parseObject(body), Order.class);
// 創(chuàng)建鎖對象
String lockKey = LOCK_ORDER_KEY_PREFIX + order.getUserId();
RLock redisLock = redissonClient.getLock(lockKey);
boolean isLocked = redisLock.tryLock();
if (!isLocked) {
log.error("不允許重復(fù)下單");
return HttpResult.fail("不允許重復(fù)下單");
}
try {
// 查詢訂單
Integer count = query().eq("user_id", order.getUserId()).eq("product_id", order.getProductId()).count();
if (count > 0) {
log.error("不允許重復(fù)下單");
return HttpResult.fail("不允許重復(fù)下單");
}
//扣減庫存
boolean success = productService.update()
.setSql("stock = stock - 1")
.eq("product_id", order.getProductId()).gt("stock", 0) // where id = ? and stock > 0
.update();
if (!success) {
log.error("庫存不足");
return HttpResult.fail("庫存不足");
}
// 插入數(shù)據(jù)庫
save(order);
} finally {
redisLock.unlock();
}
return HttpResult.success();
}
}
啟動之前,刪除訂單表數(shù)據(jù),恢復(fù)商品表庫存為100:
重新模擬200個用戶(同一用戶)同時(shí)下單:
?
?訂單表:
商品表:
?
?可以看到,利用Redisson加鎖后,有效避免了用戶重復(fù)下單的問題,需要注意的是,加鎖的方法tryLock()這里用的是Redisson默認(rèn)的參數(shù),其實(shí)還可以自己指定參數(shù)。比如lock.tryLock(10,10, TimeUnit.SECONDS)就是嘗試加鎖,最多等待10秒,上鎖以后10秒自動解鎖,這個可以根據(jù)自己的實(shí)際情況來做調(diào)整。
原以為代碼這樣寫就萬無一失了,于是又測試了一下500個請求秒殺,結(jié)果發(fā)現(xiàn)竟然有漏網(wǎng)之魚
?訂單表
?商品表
雖然200個請求沒出問題,但稍微一加大并發(fā)量就暴露了問題,針對這種情況,如果是單機(jī)部署可以考慮使用?ConcurrentHashMap來存放秒殺成功的用戶,每次加鎖成功后判斷集合里是否有該id的用戶,如果有,則說明該用戶已經(jīng)下單成功。如果是集群部署的話,那就可以考慮采用redis來緩存秒殺成功的用戶,這樣每臺機(jī)器上的服務(wù)都能訪問redis來判斷該用戶是否已經(jīng)下單成功。
代碼只需稍微改動一下,增加一個RedisUtils類
package com.wl.demo.util;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* @author wl
* @date 2022/4/6
*/
@Component
@Slf4j
public class RedisUtils {
private final RedisTemplate<String, String> stringRedisTemplate;
@Autowired
public RedisUtils(RedisTemplate<String, String> stringRedisTemplate, RedissonClient redissonClient) {
this.stringRedisTemplate = stringRedisTemplate;
}
public String getString(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
public void setValue(String key, String value) {
stringRedisTemplate.opsForValue().set(key, value);
}
public void setValue(String key, String value, Long expireTime, TimeUnit timeUnit) {
stringRedisTemplate.opsForValue().set(key, value, expireTime, timeUnit);
}
public Boolean isExistKey(String key) {
return stringRedisTemplate.hasKey(key);
}
}
Service實(shí)現(xiàn)類稍作修改:
package com.wl.demo.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wl.demo.common.result.HttpResult;
import com.wl.demo.entity.Order;
import com.wl.demo.mapper.OrderMapper;
import com.wl.demo.service.OrderService;
import com.wl.demo.service.ProductService;
import com.wl.demo.util.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.TimeUnit;
/**
* @author wl
* @date 2022/4/6
*/
@Service
@Slf4j
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
private final static String LOCK_ORDER_KEY_PREFIX = "lock:order:";
private final static String CACHE_ORDER_KEY_PREFIX = "cache:order:";
private final RedisUtils redisUtils;
private final RedissonClient redissonClient;
private final ProductService productService;
@Autowired
public OrderServiceImpl(RedisUtils redisUtils, RedissonClient redissonClient, ProductService productService) {
this.redisUtils = redisUtils;
this.redissonClient = redissonClient;
this.productService = productService;
}
@Override
@Transactional(rollbackFor = Exception.class)
public HttpResult addOrder(String body) {
Order order = JSONObject.toJavaObject(JSONObject.parseObject(body), Order.class);
// 創(chuàng)建鎖對象
String lockKey = LOCK_ORDER_KEY_PREFIX + order.getUserId();
RLock redisLock = redissonClient.getLock(lockKey);
boolean isLocked = redisLock.tryLock();
if (!isLocked) {
log.error("不允許重復(fù)下單");
return HttpResult.fail("不允許重復(fù)下單");
}
String key = CACHE_ORDER_KEY_PREFIX + order.getUserId();
if (redisUtils.isExistKey(key)) {
log.error("不允許重復(fù)下單");
return HttpResult.fail("不允許重復(fù)下單");
}
try {
// 查詢訂單
Integer count = query().eq("user_id", order.getUserId()).eq("product_id", order.getProductId()).count();
if (count > 0) {
log.error("不允許重復(fù)下單");
return HttpResult.fail("不允許重復(fù)下單");
}
//扣減庫存
boolean success = productService.update()
.setSql("stock = stock - 1")
.eq("product_id", order.getProductId()).gt("stock", 0) // where id = ? and stock > 0
.update();
if (!success) {
log.error("庫存不足");
return HttpResult.fail("庫存不足");
}
//存入秒殺成功的用戶
redisUtils.setValue(key, JSONObject.toJSONString(order), 60L, TimeUnit.SECONDS);
// 插入數(shù)據(jù)庫
save(order);
} finally {
redisLock.unlock();
}
return HttpResult.success();
}
}
最后為了測試的嚴(yán)謹(jǐn),在本機(jī)復(fù)制這個項(xiàng)目,端口改為8081,同時(shí)開啟2個服務(wù)
?然后用nginx代理這2個端口
?依然用jmeter測試500個并發(fā)請求
?測試完的用戶表
?商品表
?redis緩存的下單成功的用戶
?到此,簡單的商品秒殺就實(shí)現(xiàn)了。
?
原文鏈接:https://blog.csdn.net/wl_Honest/article/details/124006944
相關(guān)推薦
- 2022-09-17 詳解python中靜態(tài)方法staticmethod用法_python
- 2022-04-20 Flutter如何輕松實(shí)現(xiàn)動態(tài)更新ListView淺析_Android
- 2024-03-25 Intellij IDEA 啟動tomcat報(bào)錯
- 2022-11-06 Go+Redis實(shí)現(xiàn)延遲隊(duì)列實(shí)操_Golang
- 2022-10-22 React拖拽調(diào)整大小的組件_React
- 2022-09-05 Hbase 之KeyValue結(jié)構(gòu)詳解
- 2022-10-06 如何利用Redis作為Mybatis的二級緩存_Redis
- 2022-06-28 python神經(jīng)網(wǎng)絡(luò)使用tensorflow實(shí)現(xiàn)自編碼Autoencoder_python
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支