網站首頁 編程語言 正文
一般電商商品秒殺活動會面臨大量用戶同時下單的情況,不僅要面臨高并發的問題,還要保證下單數量不超過商品數量和用戶對同一商品不能重復下單(保證商品不被同一個用戶搶購完,也就是防黃牛)。
面對這些問題,可以采用Redis分布鎖來解決,通過Redis中setnx命令來保證同一時間只有一個線程能夠正常下單,待訂單創建成功后解鎖,其余線程再來搶鎖。
首先模擬一下未采用Redis加鎖的代碼實現,創建了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數據庫-->
<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默認配置 -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- spring boot 2.3版本后,如果需要使用校驗,需手動導入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實現類:
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("不允許重復下單");
return HttpResult.fail("不允許重復下單");
}
// 扣減庫存
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("庫存不足");
}
// 創建訂單
save(order);
return HttpResult.success();
}
}
啟動項目,打開jmeter模擬200個用戶(同一個)同時搶
?查看訂單表:
?商品表:
?雖然避免了下單數量多于商品庫存數量,但還是出現重復下單的情況。
接下來就輪到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實現類方法:
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);
// 創建鎖對象
String lockKey = LOCK_ORDER_KEY_PREFIX + order.getUserId();
RLock redisLock = redissonClient.getLock(lockKey);
boolean isLocked = redisLock.tryLock();
if (!isLocked) {
log.error("不允許重復下單");
return HttpResult.fail("不允許重復下單");
}
try {
// 查詢訂單
Integer count = query().eq("user_id", order.getUserId()).eq("product_id", order.getProductId()).count();
if (count > 0) {
log.error("不允許重復下單");
return HttpResult.fail("不允許重復下單");
}
//扣減庫存
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("庫存不足");
}
// 插入數據庫
save(order);
} finally {
redisLock.unlock();
}
return HttpResult.success();
}
}
啟動之前,刪除訂單表數據,恢復商品表庫存為100:
重新模擬200個用戶(同一用戶)同時下單:
?
?訂單表:
商品表:
?
?可以看到,利用Redisson加鎖后,有效避免了用戶重復下單的問題,需要注意的是,加鎖的方法tryLock()這里用的是Redisson默認的參數,其實還可以自己指定參數。比如lock.tryLock(10,10, TimeUnit.SECONDS)就是嘗試加鎖,最多等待10秒,上鎖以后10秒自動解鎖,這個可以根據自己的實際情況來做調整。
原以為代碼這樣寫就萬無一失了,于是又測試了一下500個請求秒殺,結果發現竟然有漏網之魚
?訂單表
?商品表
雖然200個請求沒出問題,但稍微一加大并發量就暴露了問題,針對這種情況,如果是單機部署可以考慮使用?ConcurrentHashMap來存放秒殺成功的用戶,每次加鎖成功后判斷集合里是否有該id的用戶,如果有,則說明該用戶已經下單成功。如果是集群部署的話,那就可以考慮采用redis來緩存秒殺成功的用戶,這樣每臺機器上的服務都能訪問redis來判斷該用戶是否已經下單成功。
代碼只需稍微改動一下,增加一個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實現類稍作修改:
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);
// 創建鎖對象
String lockKey = LOCK_ORDER_KEY_PREFIX + order.getUserId();
RLock redisLock = redissonClient.getLock(lockKey);
boolean isLocked = redisLock.tryLock();
if (!isLocked) {
log.error("不允許重復下單");
return HttpResult.fail("不允許重復下單");
}
String key = CACHE_ORDER_KEY_PREFIX + order.getUserId();
if (redisUtils.isExistKey(key)) {
log.error("不允許重復下單");
return HttpResult.fail("不允許重復下單");
}
try {
// 查詢訂單
Integer count = query().eq("user_id", order.getUserId()).eq("product_id", order.getProductId()).count();
if (count > 0) {
log.error("不允許重復下單");
return HttpResult.fail("不允許重復下單");
}
//扣減庫存
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);
// 插入數據庫
save(order);
} finally {
redisLock.unlock();
}
return HttpResult.success();
}
}
最后為了測試的嚴謹,在本機復制這個項目,端口改為8081,同時開啟2個服務
?然后用nginx代理這2個端口
?依然用jmeter測試500個并發請求
?測試完的用戶表
?商品表
?redis緩存的下單成功的用戶
?到此,簡單的商品秒殺就實現了。
?
原文鏈接:https://blog.csdn.net/wl_Honest/article/details/124006944
相關推薦
- 2022-09-01 C++?OpenCV實戰之形狀識別_C 語言
- 2022-05-03 C#設計模式之簡單工廠模式_C#教程
- 2022-12-04 Dart?異步編程生成器及自定義類型用法詳解_Dart
- 2022-05-12 van-checkbox 全選,解決單個點擊后會取消全部的問題
- 2022-06-02 Apache教程Hudi與Hive集成手冊_服務器其它
- 2022-04-07 Python編程基礎之運算符重載詳解_python
- 2021-11-03 linux下shell常用腳本命令及有關知識_Linux
- 2022-07-16 python中文文本切詞Kmeans聚類_python
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支