📒博客首页:崇尚学技术的科班人
🍣今天给大家带来的文章是《多线程 -- 超卖问题(redis分布式锁)》
🍣
🍣希望各位小伙伴们能够耐心的读完这篇文章🍣
🙏博主也在学习阶段,如若发现问题,请告知,非常感谢🙏
💗同时也非常感谢各位小伙伴们的支持💗
下面抽取的是一个SpringBoot
项目中的秒杀案例的一部分,synchronized
可以解决该超卖问题,但是由于此项目是分布式情景下的,所以最终我们用redis
分布式锁解决了超卖问题。
引入的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- springBoot整合redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
yaml
server:
servlet-path: /sell # 访问路径前缀
KeyUtil
import java.util.Random;
public class KeyUtil {
public static String getUniqueKey(){
Random random = new Random();
Integer a = random.nextInt(900000) + 100000;
return String.valueOf(a) + System.currentTimeMillis();
}
}
SecKillService
/**
* @author :小肖
* @date :Created in 2022/2/10 10:42
*/
public interface SecKillService {
/**
* 查询秒杀活动特价商品的信息
* @param productId
* @return
*/
String querySecKillProductInfo(String productId);
/**
* 模拟不同用户秒杀同一商品的请求
* @param productId
* @return
*/
void orderProductMockDiffUser(String productId);
}
SecKillServiceImpl
import com.xiao.exception.SellException;
import com.xiao.service.SecKillService;
import com.xiao.utils.KeyUtil;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* @author :小肖
* @date :Created in 2022/2/10 10:43
*/
@Service
public class SecKillServiceImpl implements SecKillService {
/**
* 国庆活动,皮蛋粥特价,限量100000份
*/
static Map<String,Integer> products;
static Map<String,Integer> stock;
static Map<String,String> orders;
static
{
/**
* 模拟多个表,商品信息表,库存表,秒杀成功订单表
*/
products = new HashMap<>();
stock = new HashMap<>();
orders = new HashMap<>();
products.put("123456", 100000);
stock.put("123456", 100000);
}
private String queryMap(String productId)
{
return "国庆活动,皮蛋粥特价,限量份"
+ products.get(productId)
+" 还剩:" + stock.get(productId)+" 份"
+" 该商品成功下单用户数目:"
+ orders.size() +" 人" ;
}
@Override
public String querySecKillProductInfo(String productId)
{
return this.queryMap(productId);
}
@Override
public synchronized void orderProductMockDiffUser(String productId)
{
//1.查询该商品库存,为0则活动结束。
int stockNum = stock.get(productId);
if(stockNum == 0) {
throw new SellException(100,"活动结束");
}else {
//2.下单(模拟不同用户openid不同)
orders.put(KeyUtil.getUniqueKey(),productId);
//3.减库存
stockNum =stockNum-1;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stock.put(productId,stockNum);
}
}
}
SecKillController
import com.xiao.service.SecKillService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author :小肖
* @date :Created in 2022/2/10 10:46
*/
@RestController
@RequestMapping("/skill")
@Slf4j
public class SecKillController {
@Autowired
private SecKillService secKillService;
/**
* 查询秒杀活动特价商品的信息
* @param productId
* @return
*/
@GetMapping("/query/{productId}")
public String query(@PathVariable String productId)throws Exception
{
return secKillService.querySecKillProductInfo(productId);
}
/**
* 秒杀,没有抢到获得"哎呦喂,xxxxx",抢到了会返回剩余的库存量
* @param productId
* @return
* @throws Exception
*/
@GetMapping("/order/{productId}")
public String skill(@PathVariable String productId)throws Exception
{
log.info("@skill request, productId:" + productId);
secKillService.orderProductMockDiffUser(productId);
return secKillService.querySecKillProductInfo(productId);
}
}
以上是我们测试的时候所需要使用到的代码,请学习的小伙伴保持一致。
(1)、并发测试
postman
来模拟高并发进行测试,以上表示的是我们的请求总数是2000
,并发量是20
。(2)、测试结果
12
份postman
并发测试并不明显,但超卖现象已经反映出来了。(3)、简要分析
@Override
public synchronized void orderProductMockDiffUser(String productId)
{
//1.查询该商品库存,为0则活动结束。
int stockNum = stock.get(productId);
if(stockNum == 0) {
throw new SellException(100,"活动结束");
}else {
//2.下单(模拟不同用户openid不同)
orders.put(KeyUtil.getUniqueKey(),productId);
//3.减库存
stockNum =stockNum-1;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stock.put(productId,stockNum);
}
}
(1)、并发测试
postman
来模拟高并发进行测试,以上表示的是我们的请求总数是500
,并发量是20
。(2)、测试结果
(3)、简要分析
synchronized
解决了超卖问题,但是 synchronized
是重量级锁,每次访问秒杀方法的时候都是单线程的形式,系统的性能变得更低了。但是数据更加安全了。(1)、修改后的代码
yaml
spring:
redis:
host: 192.168.123.135
port: 6379 # 由于没有设置密码,所以不需要进行设置
server:
servlet-path: /sell # 访问路径前缀
RedisLock
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* redis分布式锁
* @author :小肖
* @date :Created in 2022/2/10 13:28
*/
@Component
@Slf4j
public class RedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 加锁
* @param key
* @param value
* @return
*/
public boolean lock(String key,String value){
if(redisTemplate.opsForValue().setIfAbsent(key,value)){
// 加锁成功
return true;
}
// 如果锁过期
String currentValue = redisTemplate.opsForValue().get(key);
if(!StringUtils.isEmpty(currentValue) &&
Long.parseLong(currentValue) < System.currentTimeMillis()){
String oldValue = redisTemplate.opsForValue().getAndSet(key,value);
if(!StringUtils.isEmpty(oldValue) &&
oldValue.equals(oldValue)){
return true;
}
}
// 锁已经被其它线程获取
return false;
}
/**
* 解锁
* @param key
* @param value
*/
public void unlock(String key,String value){
try{
String currentValue = redisTemplate.opsForValue().get(key);
if(!StringUtils.isEmpty(currentValue) &&
currentValue.equals(value)){
redisTemplate.opsForValue().getOperations().delete(key);
}
}catch (Exception e){
log.error("【redis 分布式锁】 解锁异常,{}",e);
}
}
}
SecKillServiceImpl
import com.xiao.exception.SellException;
import com.xiao.service.RedisLock;
import com.xiao.service.SecKillService;
import com.xiao.utils.KeyUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* @author :小肖
* @date :Created in 2022/2/10 10:43
*/
@Service
public class SecKillServiceImpl implements SecKillService {
private static final int TIMEOUT = 10 * 1000;
@Autowired
private RedisLock redisLock;
/**
* 国庆活动,皮蛋粥特价,限量100000份
*/
static Map<String,Integer> products;
static Map<String,Integer> stock;
static Map<String,String> orders;
static
{
/**
* 模拟多个表,商品信息表,库存表,秒杀成功订单表
*/
products = new HashMap<>();
stock = new HashMap<>();
orders = new HashMap<>();
products.put("123456", 100000);
stock.put("123456", 100000);
}
private String queryMap(String productId)
{
return "国庆活动,皮蛋粥特价,限量份"
+ products.get(productId)
+" 还剩:" + stock.get(productId)+" 份"
+" 该商品成功下单用户数目:"
+ orders.size() +" 人" ;
}
@Override
public String querySecKillProductInfo(String productId)
{
return this.queryMap(productId);
}
@Override
public void orderProductMockDiffUser(String productId)
{
// 加锁
long time = System.currentTimeMillis() + TIMEOUT;
if(!redisLock.lock(productId,String.valueOf(time))){
throw new SellException(101,"哎呦喂,人也太多了吧,请耐心排队等待~~");
}
//1.查询该商品库存,为0则活动结束。
int stockNum = stock.get(productId);
if(stockNum == 0) {
throw new SellException(100,"活动结束");
}else {
//2.下单(模拟不同用户openid不同)
orders.put(KeyUtil.getUniqueKey(),productId);
//3.减库存
stockNum =stockNum-1;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stock.put(productId,stockNum);
}
// 解锁
redisLock.unlock(productId,String.valueOf(time));
}
}
(2)、测试结果
(3)、简要分析
RedisLock
这个类了。redis
中的setnx
方法,从而我们以商品Id
作为key
,以时间作为value
,如果没有设置该key
的话,那么我们就可以调用此方法从而实现加锁。如果该锁已被其它线程占用的话,那么我们就会抛出异常。同时还有一个过期时间的判断,如果没有这个判断的话当我们处理其中的业务逻辑抛出异常之后会导致系统死锁。所以这个过期时间的判断是十分必要的。redis
中的key
删除即可。版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/weixin_56727438/article/details/122857141
内容来源于网络,如有侵权,请联系作者删除!