1.设计原理:在分布式环境下,会存在多个线程竞争系统资源,最典型的是秒杀场景,现在我专门为秒杀场景设计了一个分布式锁,以及使用Jmeter模拟秒杀场景对分布式锁进行秒杀测试。
2.分布式锁基本逻辑:
1)判断当前线程获取的锁是否存在,如果存在就直接返回true,不做任何操作。
2)如果不存在锁,就直接创建锁,然后获取当前锁的值,用当前时间+超时时间作为锁的值,以UUID作为键。遇到锁存在,但是锁过期的情况下,就自动为锁进行续命,重新加锁。
3)并且判断锁旧的锁值是否为空,为空就说明被重新续命了,只能保证一个线程拿到锁,所以使用
if (StringUtils.isEmpty(oldValue) && oldValue.equals(currValue)){ return true; } 给锁续命
3.添加依赖
<?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.6.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>distributelock</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>distributelock</name>
<description>distributelock</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-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<builder>paketobuildpacks/builder-jammy-base:latest</builder>
</image>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.配置文件
server:
port: 8636
spring:
redis:
port: 6379
password:
host: localhost
5.分布式锁组件
package com.example.distributelock.utils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;
/**
* @className:@DistributeRock
* @Description:分布式锁组件
* @Author:
* @Version:1.0
**/
@Component
public class DistributeRock {
//创建日志打印对象
private Logger logger = LoggerFactory.getLogger(DistributeRock.class);
@Autowired
private RedisTemplate redisTemplate;
public boolean locked(String key,String value){
//判断所加的锁是否存在,存在返回true,不存在就加锁
if (redisTemplate.opsForValue().setIfAbsent(key,value)){
return true;
}
String currValue = redisTemplate.opsForValue().get(key).toString();
logger.info("long =={}",Long.parseLong(currValue));
//判断锁是否为空且锁是不是过期了,因为是以当前时间+锁的超时时间定义值的
if (!StringUtils.isBlank(currValue)&&Long.parseLong(currValue)<System.currentTimeMillis()){
//获取并且重新设置值 ,相当于锁的续命,
String oldValue = redisTemplate.opsForValue().getAndSet(key,value).toString();
//只让一个线程拿到锁,另外的线程进来的时候currValue值已经被修改
if (StringUtils.isEmpty(oldValue) && oldValue.equals(currValue)){
return true;
}
}
return false;
}
public void unlock(String key,String value){
try {
String currValue = redisTemplate.opsForValue().get(key).toString();
logger.info("currValue============={}",currValue);
logger.info("value============={}",value);
if (!StringUtils.isEmpty(currValue) && currValue.equals(value)){
redisTemplate.opsForValue().getOperations().delete(key);
}
}catch (Exception e){
e.printStackTrace();
logger.info("解锁异常!");
}
}
/**
* 提前预设库存
*/
@PostConstruct
public void initStock(){
Object stockObj =redisTemplate.opsForValue().get("stock");
if (null !=stockObj){
logger.info("存在key");
redisTemplate.delete("stock");
}
redisTemplate.opsForValue().setIfAbsent("stock","10000",99999999999L, TimeUnit.HOURS);
logger.info("库存初始化成功!");
}
}
6.redisTemplate配置
package com.example.distributelock.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @className:@RedisConfig
* @Description:TODO
* @Author:
* @Version:1.0
**/
@Configuration
public class RedisConfig {
/**
* 创建一个redis操作模板bean
* @param factory
* @return
*/
@Bean
public RedisTemplate<String,Object>redisTemplate(RedisConnectionFactory factory){
//创建一个实体
RedisTemplate<String,Object> template = new RedisTemplate<String,Object>();
template.setConnectionFactory(factory);
GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer(Object.class);
template.setValueSerializer(genericToStringSerializer);
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
7.controller相关代码
package com.example.distributelock.controller;
/**
* @className:@StockController
* @Description:TODO
* @Author:
* @Version:1.0
**/
import com.example.distributelock.utils.DistributeRock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 库存压力测试
*/
@RestController
@RequestMapping("/testlock")
public class StockController {
//设置过期时间10s
private static final Integer EXPIRE_TIME =10 ;
//日志
private Logger logger = LoggerFactory.getLogger(StockController.class);
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@Autowired
private DistributeRock distributeRock;
@GetMapping("/pressTest")
public Map<String,Object>pressTest(){
Map<String,Object> map = new HashMap<>();
long lockTime = System.currentTimeMillis() +10*1000;
String uuid = UUID.randomUUID().toString();
boolean isLock = distributeRock.locked(uuid,String.valueOf(lockTime));
if (!isLock){
map.put("data","");
map.put("code",500);
map.put("msg","系统正忙....");
return map;
}
try {
if (Integer.parseInt(redisTemplate.opsForValue().get("stock").toString())>0){
redisTemplate.opsForValue().decrement("stock");
map.put("data","库存剩余:"+Integer.parseInt(redisTemplate.opsForValue().get("stock").toString()));
logger.info("uid==={}",uuid);
map.put("code",200);
map.put("msg","库存扣减成功");
logger.info("库存扣减成功,剩余"+Integer.parseInt(redisTemplate.opsForValue().get("stock").toString()));
return map;
}else {
map.put("data","库存剩余:"+Integer.parseInt(redisTemplate.opsForValue().get("stock").toString()));
logger.info("uid==={}",uuid);
map.put("code",500);
map.put("msg","库存扣失败!");
logger.error("库存扣减失败!!!!,剩余"+Integer.parseInt(redisTemplate.opsForValue().get("stock").toString()));
}
}catch (Exception e){
logger.info("库存扣减失败!");
}finally {
distributeRock.unlock(uuid,String.valueOf(lockTime));
logger.info("释放锁!!");
}
return map;
}
}
8.我在本地的IDEA启动了4个不同端口的SpringBoot项目进行测试。
9.下面附上nginx的配置。
10.Jemeter的配置也一起放上来吧,希给大家借鉴一下。
总结:分布式锁三部曲,
1.创建唯一标识的id去识别锁。
2.redis的原子性操作,即 redisTemplate.opsForValue().setIfAbsent 和redisTemplate.opsForValue().getAndSet方法的应用。
3.在锁未被释放的时候,进行锁的续命,保证当前获取锁的线程执行完之后,才能让其他线程获取锁。