分布式锁主要用于解决分布式项目不同系统并发操作同一资源的问题
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 https://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.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.docker</groupId>
<artifactId>example</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>example</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<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>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
controller:
/**
* 秒杀提交
* @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;
}
service:
public interface GoodsStoreFacade {
/**
* 根据产品编号更新库存
* @param code
* @return
*/
String updateGoodsStore(String code,int count);
/**
* 获取库存对象
* @param code
* @return
*/
GoodsStore getGoodsStore(String code);
}
impl:
/**
* 库存管理服务
* @author user
*
*/
@Service
public class GoodsStoreService implements GoodsStoreFacade {
@Autowired
private GoodsStoreRespository goodsStoreRespository;
@Autowired
private RedisLock redisLock;
/**
* 超时时间 5s
*/
private static final int TIMEOUT = 5*1000;
/**
* 根据产品编号更新库存
* @param code
* @return
*/
@Override
public String updateGoodsStore(String code,int count) {
//上锁
long time = System.currentTimeMillis() + TIMEOUT;
if(!redisLock.lock(code, String.valueOf(time))){
return "请稍候再试,现在抢购人数过多!";
}
System.out.println("获得锁的时间戳:"+String.valueOf(time));
try {
GoodsStore goodsStore = getGoodsStore(code);
if(goodsStore != null){
if(goodsStore.getStore() <= 0){
return "对不起,卖完了,库存为:"+goodsStore.getStore();
}
if(goodsStore.getStore() < count){
return "对不起,库存不足,库存为:"+goodsStore.getStore()+" 您的购买数量为:"+count;
}
System.out.println("剩余库存:"+goodsStore.getStore());
System.out.println("扣除库存:"+count);
goodsStoreRespository.updateStore(code, count);
try{
//为了更好的测试多线程同时进行库存扣减,在进行数据更新之后先等5秒,让多个线程同时竞争资源
Thread.sleep(5000);
}catch (InterruptedException e){
e.printStackTrace();
}
return "恭喜您,购买成功!";
}else{
return "获取库存失败。";
}
} finally {
//释放锁
redisLock.release(code, String.valueOf(time));
System.out.println("释放锁的时间戳:"+String.valueOf(time));
}
}
/**
* 获取库存对象
* @param code
* @return
*/
@Override
public GoodsStore getGoodsStore(String code){
Optional<GoodsStore> optional = goodsStoreRespository.findById(code);
return optional.get();
}
}
dao:
/**
* 库存Respository
* @author user
*
*/
public interface GoodsStoreRespository extends JpaRepository<GoodsStore,String> {
/**
* 更新库存
* @param code
* @param store
* @return
*/
@Modifying
@Transactional
@Query("update GoodsStore gs set gs.store=gs.store-?2 where gs.code=?1")
int updateStore(@Param("code") String code,@Param("store")Integer store);
}
entity:
@Entity
@Table(name="goods_store")
public class GoodsStore implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
@Id
private String code;
@Column(name="store")
private int store;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public int getStore() {
return store;
}
public void setStore(int store) {
this.store = store;
}
}
redislock:
@Component
public class RedisLock {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 加锁
* @param lockKey 加锁的Key
* @param timeStamp 时间戳:当前时间+超时时间
* @return
*/
public boolean lock(String lockKey,String timeStamp){
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对应的锁的时间戳
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("警报!警报!警报!解锁异常");
}
}
}
测试:
代码中设置库存更新之后休眠5s,所以可以简单地在postman中测试,开三个窗口,连续请求秒杀接口,得到的效果如下:
只有拿到锁的那个线程抢购成功:
另外两个线程均显示:请稍候再试,现在抢购人数过多!
若不加锁,则所有的线程均显示请购成功,
不加锁的代码如下:
改动:注释掉redislock,postman发送三个请求,在减库存之前加个sleep,模拟并发访问
/**
* 库存管理服务
* @author user
*
*/
@Service
public class GoodsStoreService implements GoodsStoreFacade {
@Autowired
private GoodsStoreRespository goodsStoreRespository;
@Autowired
private RedisLock redisLock;
/**
* 超时时间 5s
*/
private static final int TIMEOUT = 5*1000;
/**
* 根据产品编号更新库存
* @param code
* @return
*/
@Override
public String updateGoodsStore(String code,int count) {
//上锁
long time = System.currentTimeMillis() + TIMEOUT;
/* if(!redisLock.lock(code, String.valueOf(time))){
return "请稍候再试,现在抢购人数过多!";
}*/
System.out.println("获得锁的时间戳:"+String.valueOf(time));
try {
GoodsStore goodsStore = getGoodsStore(code);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(goodsStore != null){
if(goodsStore.getStore() <= 0){
return "对不起,卖完了,库存为:"+goodsStore.getStore();
}
if(goodsStore.getStore() < count){
return "对不起,库存不足,库存为:"+goodsStore.getStore()+" 您的购买数量为:"+count;
}
System.out.println("剩余库存:"+goodsStore.getStore());
System.out.println("扣除库存:"+count);
goodsStoreRespository.updateStore(code, count);
try{
//为了更好的测试多线程同时进行库存扣减,在进行数据更新之后先等5秒,让多个线程同时竞争资源
Thread.sleep(5000);
}catch (InterruptedException e){
e.printStackTrace();
}
return "恭喜您,购买成功!";
}else{
return "获取库存失败。";
}
} finally {
//释放锁
/*redisLock.release(code, String.valueOf(time));*/
System.out.println("释放锁的时间戳:"+String.valueOf(time));
}
}
/**
* 获取库存对象
* @param code
* @return
*/
@Override
public GoodsStore getGoodsStore(String code){
Optional<GoodsStore> optional = goodsStoreRespository.findById(code);
return optional.get();
}
}