分布式锁
注意:项目代码在github上,github地址为https://github.com/nocoders/java-everything.git
工作中项目是部署在多台服务器上的,所以经常会面临解决分布式场景下数据一致性问题,这个时候可以使用分布式锁来解决。
Redis实现分布式锁有两种方案,一种是直接执行lua脚本,另一种是使用Redisson中的RedissonLock
。lua脚本只是简单的实现了分布式锁,Redisson则是实现了可重入锁以及对锁的时长自动延时。
LUA脚本
使用lua脚本实现分布式锁的原子性删除操作。创建锁的时候,使用Redis的SETNX命令进行创建,设置key-value。如果创建失败,说明锁已经被占用了,否则创建成功。当加锁代码运行完成时,使用lua脚本对锁进行删除操作。
创建锁
在SpringBoot项目中,我们可以使用StringRedisTemplate
的setIfAbsent(K key, V value, long timeout, TimeUnit unit)
方法创建锁。
-
引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
锁创建
String lockValue = UUID.randomUUID().toString().replace("-", ""); Boolean lockRes = stringRedisTemplate.opsForValue().setIfAbsent("lockKey", lockValue, 100L, TimeUnit.SECONDS);
删除锁
锁使用完成后,需要使用lua脚本删除锁,脚本中会先根据key获取到value,并根据value同传入的value值是否相等判断是否为同一线程设置的锁,满足条件后会将其进行删除。
-
lua脚本
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
-
使用
DefaultRedisScript
获取文件中的lua脚本DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript(); defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("deleteKey.lua"))); defaultRedisScript.setResultType(Long.class);
-
执行lua脚本,返回结果0代表执行失败,1代表执行成功
Long result = stringRedisTemplate.execute(defaultRedisScript, Collections.singletonList("lockKey"),lockValue);
完整代码如下:
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* setNx 加锁
* lua脚本删除分布式锁
*
* @author linmeng
* @date 2023/2/24 23:48
*/
@SpringBootTest
@RunWith(SpringRunner.class)
public class RedisLockLua {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void redisLockUnlock() {
String lockValue = UUID.randomUUID().toString().replace("-", "");
Boolean lockRes = stringRedisTemplate.opsForValue().setIfAbsent("lockKey", lockValue, 100L, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(lockRes)) {
System.out.println("加锁成功");
DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript();
defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("deleteKey.lua")));
defaultRedisScript.setResultType(Long.class);
Long result = stringRedisTemplate.execute(defaultRedisScript, Collections.singletonList("lockKey"),lockValue);
System.out.println("解锁是否成功:"+ (1 == result));
}
}
}
Redisson
Redisson底层使用的还是lua脚本,只不过在原有基础上添加了可重入以及使用定时任务自动延时的功能。可重入原理可查看这篇博客 Redis分布式锁—Redisson+RLock可重入锁实现篇, 自动续期原理可查看下面这篇博客 Redisson分布式锁自动续期源码分析。
-
引入依赖
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.19.3</version> </dependency>
-
bean注入
import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * Redisson 配置类 * @author linmeng * @date 2023/2/25 00:14 */ @Configuration public class RedissonConfig { @Bean public RedissonClient redissonClient() { Config config = new Config(); config.useSingleServer() .setAddress("redis://1.117.171.139:6379").setDatabase(1); return Redisson.create(config); } }
-
使用
import org.junit.Test; import org.junit.runner.RunWith; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; /** * Redisson 分布式锁实现 * * @author linmeng * @date 2023/2/24 23:48 */ @SpringBootTest @RunWith(SpringRunner.class) public class RedisLockRedisson { @Autowired private RedissonClient redissonClient; @Test public void redisLockUnlock() { Lock lock = redissonClient.getLock("lock"); try { lock.lock(); System.out.println("加锁成功"); TimeUnit.SECONDS.sleep(5); lock.lock(); System.out.println("加锁成功2"); TimeUnit.SECONDS.sleep(5); } catch (Exception e) { } finally { lock.unlock(); System.out.println("解锁成功:"); lock.unlock(); System.out.println("解锁成功2:"); } } }