在单机环境,我们使用最多的是juc包里的单机锁,但是随着微服务分布式项目的普及,juc里的锁是不能控制分布锁环境的线程安全的,因为单机锁只能控制同个进程里的线程安全,不能控制多节点的线程安全,所以就需要使用分布式锁
分布式锁的原理就不说太多了,主要讲讲基于redis的分布式锁的用法。
一、redis分布式锁原理
实际上不管在java中怎么实现redis锁,其实底层都是使用 redis的setnx
和expire 命令 来实现的。命令行操作如下:
setnx命令:
SETNX是SET if not exists的简写,设置key的值,如果key值不存在,则可以设置,否则不可以设置,这个有点像juc中cas锁的原理
# setnx命令,相当于set和nx命令一起用
setnx tkey aaa
EX : 设置指定的到期时间(以秒为单位)。
PX : 设置指定的到期时间(以毫秒为单
NX : 仅在键不存在时设置键。
XX : 只有在键已存在时才设置。
expire命令:
如果只使用setnx
不加上过期时间,手动释放锁时候出现异常,就会导致一直解不了锁,所以还是要加上expire
命令来设置过期时间。
- 保证原子性
但是又有一个问题,设置过期时间时候报错了,也同样会导致锁释放不了,所以为了保证原子性,需要这两个命令一起执行
# set tkey过期时间10秒,nx:如果键不存在时设置
set tkey aaa ex 10 nx
二、Jedis 实现分布式锁
1)常用命令:
如果不存在就获取锁 ,获取成功返回 1 : jedis.setnx("lock", "1")
设置锁的过期时间: jedis.expire("lock", 10);
删除锁:jedis.del("lock");
也可以使用set方法:jedis.set(String key, String value, String nxxx, String expx, int time)
例如:jedis.set(key, val, "NX", "PX", expireTime); 成功返回 OK 。
jedis高版本得依赖 使用这个api::set(String key, String value, SetParams params)
效果是一样的,使用方式: jedis.set("lock", "1", new SetParams().nx().ex(10L)))
一般建议使用下面得方式。
2)简单实现:
public static void main(String[] args) {
for(int i = 0;i < 10 ;i++ ){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Boolean b = true;
while (b) {
//连接本地的 Redis 服务
Jedis jedis = new Jedis("",6379,0);
// 如果 Redis 服务设置了密码,需要用下面这行代码输入密码
jedis.auth("");
// System.out.println("连接成功");
// //查看服务是否运行
// System.out.println("服务正在运行: "+jedis.ping());
if (jedis.setnx("lock", "1") == 1) {
// 获取锁成功
jedis.expire("lock", 10); // 设置锁的过期时间
System.out.println("2222222222222");
b= false;
jedis.del("lock");
}
}
}
});
thread.start();
}
有很多封装好的工具类,网上可以自行搜索一下。
三、基于RedisTemplate 实现分布式锁
1)常用命令:
获取锁以及设置锁过期时间:
redisTemplate.opsForValue().setIfAbsent("lockKey", "lockValue", 10, TimeUnit.SECONDS)
删除锁:
redisTemplate.opsForValue().getOperations().delete("lockKey");
2) 简单使用:
redis配置:
@Configuration
@Slf4j
public class RedisConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration("localhost", 6379);
redisStandaloneConfiguration.setPassword(RedisPassword.of("123456"));
LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
.clientOptions(ClientOptions.builder().timeoutOptions(TimeoutOptions.enabled()).build())
.build();
return new LettuceConnectionFactory(redisStandaloneConfiguration, clientConfiguration);
}
/**
* 通用redisTemplate采用GenericJackson2JsonRedisSerializer序列化value
* @param lettuceConnectionFactory
* @return
*/
@Bean
@Resource
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
// 设置key的序列化方式,采用StringRedisSerializer
GenericToStringSerializer<String> keySerializer = new GenericToStringSerializer<>(String.class);
redisTemplate.setKeySerializer(keySerializer);
redisTemplate.setHashKeySerializer(keySerializer);
// 设置value的序列化方式,采用Jackson2JsonRedisSerializer
GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();
redisTemplate.setValueSerializer(valueSerializer);
redisTemplate.setHashValueSerializer(valueSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
简单调用方法:
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(RedisConfig.class);
// 获取 RedisTemplate Bean
RedisTemplate<String, String> redisTemplate = context.getBean(RedisTemplate.class);
for(int i = 0;i < 10 ;i++ ){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Boolean b = true;
while (b) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(RedisConfig.class);
// 获取 RedisTemplate Bean
RedisTemplate<String, String> redisTemplate = context.getBean(RedisTemplate.class);
if (redisTemplate.opsForValue().setIfAbsent("aaa", "1", 10, TimeUnit.SECONDS)) {
System.out.println("2222222222222");
b= false;
}
redisTemplate.opsForValue().getOperations().delete("aaa");
// 关闭应用上下文
context.close();
}
}
});
thread.start();
}
}
简单使用封装类:
@Slf4j
@Service
@RequiredArgsConstructor
public class RedisLockService {
private final StringRedisTemplate stringRedisTemplate;
//获取尝试时间
private final long EXPIRE_TIME = 5 * 1000;
/**
* 加锁
* @param key String key = "dec_store_lock_" + "eth";
* @param value 当前时间+超时时间 long time = System.currentTimeMillis() + TIMOUT;
* @return
*/
public boolean tryLock(String key, String value) {
if (stringRedisTemplate.opsForValue().setIfAbsent(key, value, EXPIRE_TIME, TimeUnit.SECONDS)) {
return true;
}
//currentValue=A 这两个线程的value都是B 其中一个线程拿到锁
String currentValue = stringRedisTemplate.opsForValue().get(key);
//如果锁过期
if (!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) {
//获取上一个锁的时间 如果高并发的情况可能会出现已经被修改的问题 所以多一次判断保证线程的安全
String oldValue = stringRedisTemplate.opsForValue().getAndSet(key, value);
if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {
return true;
}
}
return false;
}
/**
* 加锁
* @param key String key = "dec_store_lock_" + "eth";
* @param value 当前时间+超时时间 long time = System.currentTimeMillis() + TIMOUT;
* @return
*/
public boolean tryLock(String key, String value,long expireTime,long waitTime) {
long nowTime = System.currentTimeMillis();
while ((System.currentTimeMillis()) - nowTime < waitTime){
if (Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, value, expireTime, TimeUnit.MILLISECONDS))) {
return true;
}
}
return false;
}
/**
* 解锁
* @param key
* @param value
* @return
*/
public void unlock(String key, String value) {
String currentVaule = stringRedisTemplate.opsForValue().get(key);
try {
if (!StringUtils.isEmpty(currentVaule) && currentVaule.equals(value)) {
stringRedisTemplate.opsForValue().getOperations().delete(key);
}
} catch (Exception e) {
log.error("解锁失败 ",e);
}
}
}
四、基于lua脚本实现分布式锁
在使用分布式锁时,需要注意的情况就是不同的线程删除的了相同的锁,在多线程情况下,就会造成逻辑上的混乱,所以我们解决这种情况的方法就是,不同的线程,使用不同的key。当然有些业务就是要使用相同的key。所以在删除锁的时候我们最好能使用lua脚本的方式,这样在删除的时候是原子性的,就能避免这种情况。
Lua脚本(unlock.lua)
if(redis.call('get', KEYS[1]) == ARGV[1]) then
return redis.call('del', KEYS[1])
end
return 0
调用代码:
import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock {
private String name;
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
// 加载脚本
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
// 加载工程resourcecs下的unlock.lua文件
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
public boolean tryLock(long timeoutSec) {
// 获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
public void unlock() {
// 调用Lua脚本
stringRedisTemplate.execute(UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}
}
当前还存在的问题:
- 不可重入:同一个线程无法多次获取同一把锁(线程1在执行方法1的时候调用了方法2,而方法2也需要锁)
- 不可重试:获取锁只尝试一次就返回false,没有重试机制;当前直接返回结果。
- 超时释放:锁超时释放虽然可以避免死锁,但是如果业务执行耗时较长,也会导致锁释放,存在安全隐患。
- 主从一致性:如果redis提供了主从集群,主存同步存在延迟。当主结点宕机时,从节点尚未同步主结点锁数据,则会造成锁失效。
写的不是特别详细,需要的话可以找找比较详细的资料。平时工作中用到的次数不多,只做了解。
五、基于 Redisson 实现分布式锁
1、依赖:
<!-- 原生,本章使用-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
<!-- 另一种Spring集成starter,本章未使用 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.13.6</version>
</dependency>
2、配置类:
@Configuration
public class RedissonConfig {
/**
* redis地址
*/
@Value("${spring.redis.host}")
private String redisHost;
/**
* redis端口号
*/
@Value("${spring.redis.port}")
private String redisPort;
/**
* redis密码
*/
@Value("${spring.redis.password}")
private String redisPassword;
/**
* redis的数据库编号
*/
@Value("${spring.redis.database}")
private Integer redisDatabase;
@Bean
public RedissonClient redissonClient() {
// 创建 Redisson 客户端连接
Config config = new Config();
config.useSingleServer()//单机模式
.setAddress("redis://"+redisHost+":"+redisPort)//redis服务器地址
.setDatabase(redisDatabase)//指定数据库编号
.setPassword(redisPassword)//redis密码
.setConnectionMinimumIdleSize(10)//连接车最小空闲连接数
.setConnectionPoolSize(50)//连接池最大线程数
.setIdleConnectionTimeout(60000)//线程超时时间
.setConnectTimeout(10000)//客户端程序获取redis链接的超时时间
.setTimeout(10000);//响应超时时间
return Redisson.create(config);
}
}
3、简单使用:
常用api:
1) RLock.lock() 直接加锁,解锁
RLock lock1 = redissonClient.getLock(KEY_LOCKED);
log.error("lock1 clas: {}", lock1.getClass());
lock1.lock();//加锁
// 处理业务逻辑
// ........
lock1.unlock();//解锁
2)RLock.lock(long var1, TimeUnit var3); 加锁时增加过期时间,到时自动释放锁。
RLock lock1 = redissonClient.getLock(KEY_LOCKED);
// 500s 后自动释放锁
lock1.lock(500, TimeUnit.SECONDS);
try {
Thread.sleep(TIME_LOCKED);
} catch (InterruptedException ignore) {
// ignore
}
3) RLock.tryLock(long time, TimeUnit unit) 尝试一定时间去获取锁,返回Boolean值,获取成功返回 true ,获取失败返回false。 一般用这个,不会直接加锁。
RLock lock1 = redissonClient.getLock(KEY_LOCKED);
boolean b = lock1.tryLock(7, TimeUnit.SECONDS);
if (!b) {
log.info(Thread.currentThread().getName() + " \t 获取锁失败");
return;
}
log.info(Thread.currentThread().getName() + " \t 获取锁");
lock1.unlock();
4) RLock.tryLock(long var1, long var3, TimeUnit var5) 接收3个参数,第一个指定最长等待时间waitTime,第二个指定最长持有锁的时间 holdTime, 第三个是时间单位
RLock lock1 = redissonClient.getLock(KEY_LOCKED);
// 尝试获取锁7s, 最多占有锁2s,超过后自动释放,调用unlock可以提前释放。
boolean b = lock1.tryLock(7, 2, TimeUnit.SECONDS);
if (!b) {
log.info(Thread.currentThread().getName() + " \t 获取锁失败");
return;
}
// 如果是当前线程持有锁,手动释放
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
}
公平锁:
我们使用 RLock lock1 = redissonClient.getLock(KEY_LOCKED); 直接获取的锁,默认的是非公平锁。
如果我们要使用公平锁,也就是说先到先得的形式,可以使用下面的方式获取:
RLock lock1 = redissonClient.getFairLock(KEY_LOCKED);
其他操作一样。
读写锁:
- 多个线程可以同时读取共享资源,读取操作不会阻塞其他读取操作。
- 写操作会阻塞所有读取操作和写操作,只有当没有线程在读取或写入时,写操作才能执行。
- 读写锁适用于读操作远远多于写操作的场景,可以提高并发性能。
也就是说读写锁分为 读 和 写,当多个线程都在读的时候,没有问题,都可以正常读,但是当有线程写的时候,就会锁住 读 和 写的所有操作。就是说你要是读随便读,但是如果一旦有写,那么其他的读和写都会被锁住。确保只有一个写在进行。
1)获取一个读写锁实例:
RWLockReadWriteLock rwLock = redisson.getReadWriteLock("myReadWriteLock");
2)获取读锁和写锁
使用读写锁实例,可以分别获取读锁和写锁:
RLock readLock = rwLock.readLock();
RLock writeLock = rwLock.writeLock();
3)示例代码
private static void lock4() {
for (int i = 0; i < 3; i++) {
try {
Thread.sleep(1 * 1000);
} catch (InterruptedException e) {
}
new Thread(new Runnable() {
@Override
public void run() {
log.info(Thread.currentThread().getName() + " \t 运行");
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(KEY_LOCKED);
readWriteLock.readLock().lock();
log.info(Thread.currentThread().getName() + " \t 获取读锁");
try {
// 模拟处理逻辑用时5s
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
}
readWriteLock.readLock().unlock();
log.info(Thread.currentThread().getName() + " \t 释放读锁");
readWriteLock.writeLock().lock();
log.info(Thread.currentThread().getName() + " \t 获取写锁");
try {
// 模拟处理逻辑用时5s
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
}
readWriteLock.writeLock().unlock();
log.info(Thread.currentThread().getName() + " \t 释放写锁");
}
}).start();
}
}
会发现读锁可以同时获取,但是写锁只能有一个!
删除锁:
一般我们不会直接去进行解锁操作 lock.unlock(); 会在解锁条件前先进行判断一下 Objects.nonNull(lock)
确保lock
不是null
。lock.isLocked()
检查锁是否被任何线程持有,而lock.isHeldByCurrentThread()
检查锁是否被请求它的当前线程持有。
if (Objects.nonNull(lock) && lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
六、Redisson 原理
redisson 官网中文地址:
目录 · redisson/redisson Wiki · GitHub