需求
现库存某一商品存货量为100,实现每秒10000请求QPS大约为20的秒杀,并能完成正常的库存扣减,防止超卖
前置知识
- 了解
jmeter
的简单使用用于模拟并发; -
redis
在Java
语言下的基本操作;
环境说明
1、redis version 3.2
2、Maven 3.6
3、redis的数据结构 商品编号为key 当前时间+过期时间组成的时间戳为Value
4、Jmeter 5.4.3 项目基于springboot实现
代码结构如下
---com.liang
---controller
GoosController
---entity
GoodsStore
---respository
GoodsStoreRespository
---service
---impl
GoodsStoreServiceImpl
GoodsStoreService
---lock
RedisLockD
POM依赖
<?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>
<groupId>com.liang</groupId>
<artifactId>springbootRedis001</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
</parent>
<!-- 引入各种各样的组件 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- jpa -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 整合redis 虽说是springboot整合redis 但其实为 Spring Data Redis 操作redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 引入连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- 引入lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--fastJson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.56</version>
</dependency>
<!-- 导入thymleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!-- 没有该配置,devtools不生效 -->
<fork>true</fork>
<addResources>true</addResources>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
数据结构
Entity
package com.liang.entity;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
/**
* Created with Intellij IDEA
*
* @Auther: liangjy
* @Date: 2022/01/07/11:14
* @Description: 秒杀demo 连接数据库
*/
@Entity
@Table(name = "goods_store", schema = "seckill")
@Getter
@Setter
@ToString
public class GoodsStore implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private String code;
@Column(name = "store")
private int store;
}
Repository代码
package com.liang.respository;
import com.liang.entity.GoodsStore;
import io.lettuce.core.dynamic.annotation.Param;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import javax.transaction.Transactional;
/**
* Created with Intellij IDEA
*
* @Auther: liangjy
* @Date: 2022/01/07/11:48
* @Description:
*/
public interface GoodsStoreRespository extends JpaRepository<GoodsStore,String> {
/**
* 更新库存
* @param code
* @param store
* @return
*/
@Modifying
@Transactional
@Query("update GoodsStore gs set gs.store=?2 where gs.code=?1")
int updateStore(@Param("code") String code, @Param("store")Integer store);
}
service
package com.liang.service;
import com.liang.entity.GoodsStore;
public interface GoodsStoreService {
/**
* 根据产品编号更新库存
* @param code
* @return
*/
String updateGoodsStore(String code,int count);
/**
* 获取库存对象
* @param code
* @return
*/
GoodsStore getGoodsStore(String code);
}
impl
package com.liang.service.impl;
import com.liang.entity.GoodsStore;
import com.liang.respository.GoodsStoreRespository;
import com.liang.rlock.RedisLockD;
import com.liang.service.GoodsStoreService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Optional;
@Service
public class GoodsStoreServiceImpl implements GoodsStoreService {
@Autowired
private GoodsStoreRespository goodsStoreRespository;
@Autowired
private RedisLockD redisLock;
/**
* 超时时间 5s
*/
private static final int TIMEOUT = 5 * 1000;
/**
* 根据产品编号更新库存
*
* @param code 商品编号
* @param count 前端传过来的数量
* @return
*/
@Override
public String updateGoodsStore(String code, int count) {
//上锁
BigDecimal count1 = new BigDecimal(count);
long time = System.currentTimeMillis() + TIMEOUT;
/**
* 拿不到资源 那么就会等待
*/
if (!redisLock.lock(code, String.valueOf(time))) {
return "排队人数太多,请稍后再试.";
}
try {
GoodsStore goodsStore = getGoodsStore(code);
if (goodsStore != null) {
if (goodsStore.getStore() <= 0) {
return "对不起,卖完了,库存为:" + goodsStore.getStore();
}
if (goodsStore.getStore() < count) {
return "对不起,库存不足,库存为:" + goodsStore.getStore() + " 您的购买数量为:" + count;
}
int store = goodsStore.getStore();
BigDecimal stroe = new BigDecimal(store);
BigDecimal remainstore = stroe.subtract(count1);
goodsStoreRespository.updateStore(code, Integer.parseInt(String.valueOf(remainstore)));
try {
//为了更好的测试多线程同时进行库存扣减,在进行数据更新之后先等1秒,让多个线程同时竞争资源
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "恭喜您,购买成功!";
} else {
return "获取库存失败。";
}
} finally {
//释放锁
redisLock.release(code, String.valueOf(time));
}
}
/**
* 获取库存对象
*
* @param code
* @return
*/
@Override
public GoodsStore getGoodsStore(String code) {
Optional<GoodsStore> optional = goodsStoreRespository.findById(code);
return optional.get();
}
}
lock
package com.liang.rlock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* Created with Intellij IDEA
*
* @Auther: liangjy
* @Date: 2022/01/07/14:36
* @Description:
*/
@Component
public class RedisLockD {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 加锁
*
* @param lockKey 加锁的Key
* @param timeStamp 时间戳:当前时间+超时时间
* @return
*/
public boolean lock(String lockKey, String timeStamp) {
/**
* 在这个场景下商品编码作为key 时间戳来作为value setnx
* 设置成功返回true
*/
if (stringRedisTemplate.opsForValue().setIfAbsent(lockKey, timeStamp)) {
// 对应setnx命令,可以成功设置,也就是key不存在,获得锁成功
return true;
}
//设置失败的原因是因为这个商品已经被操作了,获得锁失败
// 判断锁超时 - 防止原来的操作异常,没有运行解锁操作 ,防止死锁
String currentLock = stringRedisTemplate.opsForValue().get(lockKey);
// 如果锁过期 currentLock不为空且小于当前时间
if (!StringUtils.isEmpty(currentLock) && Long.parseLong(currentLock) < System.currentTimeMillis()) {
//如果lockKey对应的锁已经存在,获取上一次设置的时间戳之后并重置lockKey对应的锁的时间戳
/**
* getAndSet
* 获取原来key键对应的值并重新赋新值。
* 拿到原来的时间戳并复制新的时间戳
* prelock 为原来的时间戳
*/
String preLock = stringRedisTemplate.opsForValue().getAndSet(lockKey, timeStamp);
//假设两个线程同时进来这里,因为key被占用了,而且锁过期了。
//获取的值currentLock=A(get取的旧的值肯定是一样的),两个线程的timeStamp都是B,key都是K.锁时间已经过期了。
//而这里面的getAndSet一次只会一个执行,也就是一个执行之后,上一个的timeStamp已经变成了B。
//只有一个线程获取的上一个值会是A,另一个线程拿到的值是B。
if (!StringUtils.isEmpty(preLock) && preLock.equals(currentLock)) {
return true;
}
}
return false;
}
/**
* 释放锁
*
* @param lockKey
* @param timeStamp
*/
public void release(String lockKey, String timeStamp) {
try {
String currentValue = stringRedisTemplate.opsForValue().get(lockKey);
if (!StringUtils.isEmpty(currentValue) && currentValue.equals(timeStamp)) {
// 删除锁状态
stringRedisTemplate.opsForValue().getOperations().delete(lockKey);
}
} catch (Exception e) {
System.out.println("警报!警报!警报!解锁异常");
}
}
}
controller
package com.liang.controller;
import com.liang.service.GoodsStoreService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
/**
* Created with Intellij IDEA
*
* @Auther: liangjy
* @Date: 2022/01/07/14:42
* @Description:
*/
@Controller
@RequestMapping("/")
public class GoosController {
@Autowired
private GoodsStoreService goodsStoreService;
/**
* 秒杀提交
*
* @param code
* @param num
* @return
*/
@PostMapping("secKill")
@ResponseBody
public String secKill(@RequestParam(value = "code", required = true) String code, @RequestParam(value = "num", required = true) Integer num) {
String reString = goodsStoreService.updateGoodsStore(code, num);
return reString;
}
}
执行测试
Jmeter 建立线程组
建立线程组【一秒设立10000个线程数】
http请求设置
查看结果树
最终效果就是一万个线程数,只有随机11个线程数秒杀成功。相当于实现了非公平锁。但在用户角度来看反而挺公平的
获得锁的时间戳:1645169458647
剩余库存:1000
扣除库存:1
释放锁的时间戳:1645169458647
获得锁的时间戳:1645169459801
剩余库存:999
扣除库存:1
释放锁的时间戳:1645169459801
获得锁的时间戳:1645169460897
剩余库存:998
扣除库存:1
获得锁的时间戳:1645169461948
释放锁的时间戳:1645169460897
剩余库存:997
扣除库存:1
释放锁的时间戳:1645169461948
获得锁的时间戳:1645169463224
剩余库存:996
扣除库存:1
获得锁的时间戳:1645169464540
释放锁的时间戳:1645169463224
剩余库存:995
扣除库存:1
释放锁的时间戳:1645169464540
获得锁的时间戳:1645169465695
剩余库存:994
扣除库存:1
释放锁的时间戳:1645169465695
获得锁的时间戳:1645169466746
剩余库存:993
扣除库存:1
获得锁的时间戳:1645169467859
释放锁的时间戳:1645169466746
剩余库存:992
扣除库存:1
释放锁的时间戳:1645169467859
获得锁的时间戳:1645169468955
剩余库存:991
扣除库存:1
释放锁的时间戳:1645169468955
获得锁的时间戳:1645169469966
剩余库存:990
扣除库存:1
释放锁的时间戳:1645169469966
查看数据库是否出现超卖
数据显示正常,简单实现了开篇需求