Redis的分布式锁
集群下的线程并发问题
我们在使用synchronized进行加锁时,由于每一个JVM有一个锁监视器,这就能够保证每次只有一个线程能获取这把锁。但是若在集群环境下(假设两台),那就会有两个JVM,也就会有两个锁监视器,就会有多个线程获取到锁,这样就没办法实现多JVM之间的互斥,就会产生线程并发问题
分布式锁
实现一个在多个JVM外部的同一个锁监视器
分布式锁:满足分布式系统下或集群模式下多进程可见并且互斥的锁
基于Redis的分布式锁
获取锁:
互斥:确保只能有一个线程获取锁
非阻塞:尝试一次,成功返回true,失败返回false
设置锁和设置超时时间必须保证原子性
1
| SET lock thread1 NX EX 10
|
释放锁:
手动释放
超时释放:获取锁时添加一个超时时间
简易版
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public interface ILock {
boolean tryLock(long timeoutSec);
void unlock(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| public class SimpleRedisLock implements ILock {
private String name; private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) { this.name = name; this.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<>(); UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua")); UNLOCK_SCRIPT.setResultType(Long.class); }
@Override 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); } @Override public void unlock() { String threadId = ID_PREFIX + Thread.currentThread().getId(); String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name); if(threadId.equals(id)) { stringRedisTemplate.delete(KEY_PREFIX + name); } } }
|
此时所存在的问题:
Redis的Lua脚本
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。
业务的lua脚本:
1 2 3 4 5 6
| if(redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]) end return 0
|
基于Redis的分布式锁实现思路:
1、利用set nx ex 获取锁,并设置过期时间,保存线程标示
2、释放锁时先判断线程标示是否与自己一致,一致则删除锁
特性:
1、利用set nx 满足互斥性
2、利用set ex 保证故障时锁依然能释放,避免死锁,提高安全性
3、利用Redis集群保证高可用和高并发
Redisson
基于setnx实现的分布式锁存在下面问题
Redisson是一个在Redis的基础上实现的java驻内存数据网络。他不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
官网:https://redisson.org
入门:
导入依赖
1 2 3 4 5
| <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.13.6</version> </dependency>
|
配置客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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;
@Configuration public class RedissonConfig {
@Bean public RedissonClient redissonClient(){ Config config = new Config(); config.useSingleServer().setAddress("redis://192.168.150.101:6379").setPassword("123321"); return Redisson.create(config); } }
|
使用分布式锁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource; import java.util.concurrent.TimeUnit;
@Slf4j @SpringBootTest class RedissonTest {
@Resource private RedissonClient redissonClient;
private RLock lock;
@BeforeEach void setUp() { lock = redissonClient.getLock("order"); }
@Test void method1() throws InterruptedException { boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS); if (!isLock) { log.error("获取锁失败 .... 1"); return; } try { log.info("获取锁成功 .... 1"); method2(); log.info("开始执行业务 ... 1"); } finally { log.warn("准备释放锁 .... 1"); lock.unlock(); } } void method2() { boolean isLock = lock.tryLock(); if (!isLock) { log.error("获取锁失败 .... 2"); return; } try { log.info("获取锁成功 .... 2"); log.info("开始执行业务 ... 2"); } finally { log.warn("准备释放锁 .... 2"); lock.unlock(); } } }
|
Redisson分布式锁原理:
可重入:利用hash结构记录线程id和重入次数
可重试:利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制
超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间
总结:
1、不可重入Redis分布式锁:
原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标示
缺陷:不可重入、无法重试、锁超时失效
2、可重入的Redis分布式锁:
原理:利用hash结构,记录线程标示和重入次数;利用watchDog延续锁时间;利用信号量控制锁重试等待
缺陷:redis宕机引起锁失效问题
3、Redisson的multiLock:
原理:多个独立的Redis节点,必须在所有节点都获取重入锁才算获取锁成功
缺陷:运维成本高、实现复杂