分布式锁介绍
在java的开发中,我们一般在需要并发访问的资源上使用加锁Lock或者synchronized来同步访问,但是只能针对单个jvm内的加锁,当系统需要在多个系统之间访问同一个受保护的资源时,就需要用到分布式锁的机制了,比如在电商网站网站的高并发情况下,大量请求需要扣减库存,而扣减库存的操作需要受保护的。常见的实现分布式锁的方案由通过zookeeper的临时有序节点,数据库的自增主键和今天我们要讲的redis实现。
redis实现分布式锁原理
在redis中实现分布式锁加锁是set lock_key random_value nx px millisecond来实现加锁操作,nx代表当lock_key不存在设置成功,否则表示设置失败,px 代表键的过期时间,防止由于线程长时间持有锁不释放而导致的死锁。random-value表示随机值,每个客户端的都不一样。
解锁操作需要保证原子性,先获取到键key对应的值,判断是否和当前节点设置的值一致,如果不一致则不能解锁(其他客户端不能解锁当前客户端锁定的值),否则可以解锁。因此需要保证原子性。
可以通过lua脚本来执行。
// redis.call中的第一个参数表示要执行的命令,Lua脚本中的下表从1开始,KEYS是要获取的key, ARGV传入的参数值
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end
redis单机版的分布式锁实现
今天我们讲的是通过单机版实现的分布式锁,即多个线程并发获取同一把锁。
1. 创建springboot工程,引入redis依赖
<?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.4.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>redislock</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>screw</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-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. 在application.properties中配置redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=13
3. 创建redis配置类
@Configuration
public class RedisConfiguration {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private Integer port;
public JedisPoolConfig jedisPoolConfig() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 此处的配置可以写在application.properties中,也可以不加
// jedisPoolConfig.setMaxIdle(100);
// jedisPoolConfig.setMaxWaitMillis(200);
// jedisPoolConfig.setMaxTotal(100);
// jedisPoolConfig.setMinIdle(50);
return jedisPoolConfig;
}
@Bean
public JedisPool jedisPool() {
JedisPoolConfig jedisPoolConfig = jedisPoolConfig();
return new JedisPool(jedisPoolConfig,host,port);
}
}
4. 创建RedisLock类
@Service
@Slf4j
public class RedisLock {
private String lock_key = "redis_lock"; // 锁键
protected long internalLockReleaseTime = 30000; //锁过期时间
private long timeout = 999999; // 获取锁的超时时间
//SET的参数
SetParams params = SetParams.setParams().nx().px(internalLockReleaseTime);
@Autowired
private JedisPool jedisPool;
/**
* 加锁
* @param id
* @return
*/
public boolean lock(String id) {
Jedis jedis = jedisPool.getResource();
Long start = System.currentTimeMillis();
try{
for(;;) {
//SET 命令返回OK,则证明获取锁成功
String lock = jedis.set(lock_key,id,params);
if("OK".equals(lock)) {
System.out.println("加锁成功: " + id);
return true;
}
System.out.println("加锁失败等待: " + id);
//否则循环等待,在timeout时间内仍未获取到锁,则获取失败
long l = System.currentTimeMillis() - start;
if(l >= timeout) {
return false;
}
try{
Thread.sleep(100);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}finally {
jedis.close();
}
}
/**
* 解锁
* @param id
* @return
*/
public boolean unlock(String id) {
Jedis jedis = jedisPool.getResource();
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then" +
" return redis.call('del',KEYS[1]) " +
"else" +
" return 0 " +
"end";
try{
Object result = jedis.eval(script, Collections.singletonList(lock_key),Collections.singletonList(id));
if("1".equals(result.toString())) {
System.out.println("解锁成功: " + id);
return true;
}
System.out.println("解锁失败: " + id);
return false;
}finally {
jedis.close();
}
}
}
5. 创建访问控制器类
唯一性id可以使用UUID也可以使用Snowflake雪花算法来生成。
创建1000个线程来执行并发访问。
@Controller
@Slf4j
public class IndexController {
@Autowired
private RedisLock redisLock;
@Autowired
private SnowFlakeIdWorker snowFlakeIdWorker;
int count = 0;
@RequestMapping("/index")
@ResponseBody
public String index() throws InterruptedException {
int clientcount = 1000;
CountDownLatch countDownLatch = new CountDownLatch(clientcount);
ExecutorService executorService = Executors.newFixedThreadPool(clientcount);
long start = System.currentTimeMillis();
for(int i = 0;i < clientcount;i++) {
executorService.execute(() -> {
//通过UUID获取唯一的ID字符串
String id = UUID.randomUUID().toString();
try{
redisLock.lock(id);
count++;
}finally {
redisLock.unlock(id);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
long end = System.currentTimeMillis();
log.info("执行线程数:{},总耗时:{},count数为:{}",clientcount,end-start,count);
return "success";
}
}
6. 浏览器访问
http://localhost:8080/index 并验证最终的count的是1000次

image.png