一、分布式锁与本地锁
在高并发环境下,本地锁只能锁住当前的进程,无法锁住其他进程。
在分布式环境下,本地锁只能锁住当前服务,无法锁住其他服务器上部署的服务,集群环境需要共享一把锁,因此需要使用分布式锁。
例如:
二、分布式锁原理
分布式锁演进-基本原理
分布式锁演进-阶段一
/**
* 测试分布式锁
*/
@Override
public String testLock() throws Exception {
// 1. 占分布式锁:去redis占坑
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1111");
if (lock) {
// 加锁成功,执行业务
// 假设业务执行耗时5s
Thread.sleep(5000);
log.info("业务执行完成!");
// 删除锁
redisTemplate.delete("lock");
return "success";
} else {
// 加锁失败。。。休眠10秒重试
Thread.sleep(1000);
return testLock();
}
}
分布式锁演进-阶段二
/**
* 测试分布式锁
*/
@Override
public String testLock() throws Exception {
// 1. 占分布式锁:去redis占坑
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", UUID.randomUUID().toString());
if (lock) {
// 设置过期时间
redisTemplate.expire("lock", 30, TimeUnit.SECONDS);
// 加锁成功,执行业务 假设业务执行耗时5s
Thread.sleep(5000);
log.info("业务执行完成!");
// 删除锁
redisTemplate.delete("lock");
return "success";
} else {
// 加锁失败。。。休眠10秒重试
Thread.sleep(1000);
return testLock();
}
}
分布式锁演进-阶段三
在阶段2的基础上,占锁的时候,就为锁设置过期时间,保证占有锁和设置过期时间为原子性操作。
/**
* 测试分布式锁
*/
@Override
public String testLock() throws Exception {
// 1. 占分布式锁:去redis占坑
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", UUID.randomUUID().toString(), 30, TimeUnit.SECONDS);
if (lock) {
// 设置过期时间
// redisTemplate.expire("lock", 30, TimeUnit.SECONDS);
// 加锁成功,执行业务 假设业务执行耗时5s
Thread.sleep(5000);
log.info("业务执行完成!");
// 删除锁
redisTemplate.delete("lock");
return "success";
} else {
// 加锁失败。。。休眠10秒重试
Thread.sleep(1000);
return testLock();
}
}
分布式锁演进-阶段四
/**
* 测试分布式锁
*/
@Override
public String testLock() throws Exception {
// 1. 占分布式锁:去redis占坑
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
if (lock) {
// 设置过期时间
// redisTemplate.expire("lock", 30, TimeUnit.SECONDS);
// 加锁成功,执行业务 假设业务执行耗时5s
Thread.sleep(5000);
log.info("业务执行完成!");
if (uuid.equals(getLock())) {
// 删除锁
redisTemplate.delete("lock");
}
return "success";
} else {
// 加锁失败。。。休眠10秒重试
Thread.sleep(1000);
return testLock();
}
}
public String getLock() {
return redisTemplate.opsForValue().get("lock");
}
分布式锁演进-阶段五-最终形态
/**
* 测试分布式锁
*/
@Override
public String testLock() throws Exception {
// 1. 占分布式锁:去redis占坑
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
if (lock) {
// 设置过期时间
// redisTemplate.expire("lock", 30, TimeUnit.SECONDS);
// 加锁成功,执行业务 假设业务执行耗时5s
Thread.sleep(5000);
log.info("业务执行完成!");
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
// if (uuid.equals(getLock())) {
// 删除锁
// redisTemplate.delete("lock");
// }
return "success";
} else {
// 加锁失败。。。休眠10秒重试
Thread.sleep(1000);
return testLock();
}
}
public String getLock() {
return redisTemplate.opsForValue().get("lock");
}
/**
* 从数据库查询并封装数据::分布式锁
* @return
*/
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1、占分布式锁。去redis占坑 设置过期时间必须和加锁是同步的,保证原子性(避免死锁)
String uuid = UUID.randomUUID().toString();
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if (lock) {
System.out.println("获取分布式锁成功...");
Map<String, List<Catelog2Vo>> dataFromDb = null;
try {
//加锁成功...执行业务
dataFromDb = getDataFromDb();
} finally {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//删除锁
stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
//先去redis查询下保证当前的锁是自己的
//获取值对比,对比成功删除=原子性 lua脚本解锁
// String lockValue = stringRedisTemplate.opsForValue().get("lock");
// if (uuid.equals(lockValue)) {
// //删除我自己的锁
// stringRedisTemplate.delete("lock");
// }
return dataFromDb;
} else {
System.out.println("获取分布式锁失败...等待重试...");
//加锁失败...重试机制
//休眠一百毫秒
try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
return getCatalogJsonFromDbWithRedisLock(); //自旋的方式
}
}
三、分布式锁实现
官方介绍:
在来看一下分布式锁的最终形态,主要的问题在于:
- 保证加锁和删除锁是一个原子性的操作。
- 业务执行时间较长,锁的自动续期问题。
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1、占分布式锁。去 redis 占坑
String uuid = UUID.randomUUID().toString();
Boolean lock =
redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);
if(lock){
System.out.println("获取分布式锁成功...");
//加锁成功... 执行业务
//2、设置过期时间,必须和加锁是同步的,原子的
//redisTemplate.expire("lock",30,TimeUnit.SECONDS);
Map<String, List<Catelog2Vo>> dataFromDb;
try{
dataFromDb = getDataFromDb();
}finally {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1]) else return 0 end";
//删除锁
Long lock1 = redisTemplate.execute(new
DefaultRedisScript<Long>(script, Long.class)
, Arrays.asList("lock"), uuid);
}
//获取值对比+对比成功删除=原子操作 lua 脚本解锁
// String lockValue = redisTemplate.opsForValue().get("lock");
// if(uuid.equals(lockValue)){
// //删除我自己的锁
// redisTemplate.delete("lock");//删除锁
// }
return dataFromDb;
}else {
//加锁失败...重试。synchronized ()
//休眠 100ms 重试
System.out.println("获取分布式锁失败...等待重试");
try{
Thread.sleep(200);
}catch (Exception e){
}
return getCatalogJsonFromDbWithRedisLock();//自旋的方式
}
}
上述操作是相同的代码逻辑,可以将其抽取为一个工具类的形式。
四、Redission完成分布式锁
(一) 简介
Redisson 是架设在 Redis 基础上的一个 Java 驻内存数据网格(In-Memory Data Grid)。充分的利用了 Redis 键值数据库提供的一系列优势,基于 Java 实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。
官方文档:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95
(二) 依赖
<!--redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.24.3</version>
</dependency>
(三) 配置
// 默认连接地址 127.0.0.1:6379
RedissonClient redisson = Redisson.create();
Config config = new Config();
//redis://127.0.0.1:7181
//可以用"rediss://"来启用 SSL 连接
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
/**
* redisson配置
*
* @Author: yichangqiao
* @Date: 2024/1/7 15:24
*/
@Configuration
public class RedissonConfig {
/**
* 对所有的Redisson的操作都是使用RedissonClient对象
*
* @return
*/
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() {
Config config = new Config();
// redis:// 为redis地址,如果redis开启SSL链接,需要使用rediss://
config.useSingleServer().setAddress("redis://127.0.0.1:6379")
.setPassword("123456").setDatabase(3);
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
(四) 使用分布式锁
redisson提供了很多种类型的锁,一般是使用的可重入锁。
RLock lock = redisson.getLock("anyLock");// 最常见的使用方法
lock.lock();
// 加锁以后 10 秒钟自动解锁// 无需调用 unlock 方法手动解锁
lock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待 100 秒,上锁以后 10 秒自动解锁 boolean res = lock.tryLock(100,
10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
/**
* 测试redisson分布式锁
*/
@GetMapping("/testRedisson")
public JsonResult testRedisson() {
// 1.获取一把锁,保证获取的是同一把锁就行。
RLock lock = redissonClient.getLock("my-lock");
// 2.加锁 (阻塞式等待,默认加的锁都是30S时间)
lock.lock(30, TimeUnit.SECONDS);
// 锁的自动续期,如果业务超长,运行期间自动给锁续上新的30S,不用担心业务时间过长,锁自动过期被删除。
// 加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30S以后自动删除。
try {
log.info("加锁成功,执行业务:{}", LocalDateTime.now());
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 解锁,假设解锁代码没有运行,redisson会不会出现死锁。
// 如果设置了锁的过期时间不会,否则会出现。
log.info("释放锁:{}", Thread.currentThread().getId());
lock.unlock();
}
return ResultUtil.success("success");
}
(五) 使用其他
可以参照官方文档进行
五、Lock看门狗原理
先看一下以下的代码逻辑:
lock.lock(30, TimeUnit.SECONDS);
// 30秒自动解锁,自动解锁的时间一定要大于业务的执行时间。
// 问题:lock.lock(30, TimeUnit.SECONDS);
// 到了锁的自动到期时间之后,业务代码还没执行完,锁不会自动续期。
上述代码在执行时间到了之后,不会给锁自动续期。手动指定了过期时间后,在底层与看门狗的自动续期走了不同的方法:
看门狗的方法中有一个更新过期时间的定时任务,并且每隔三分之一的看门狗过期时间,就会执行一次,续期30S,看门狗的默认过期时间就是30S。
总结:
- 加锁 (阻塞式等待,默认加的锁都是30S时间)。
- lock.lock(30, TimeUnit.SECONDS); 指定30秒自动解锁,自动解锁时间一定要大于业务的执行时间。
- 如果指定了锁的超时时间,就发送给redis执行脚本进行占锁,默认超时时间为指定时间。
- lock.lock(30, TimeUnit.SECONDS); 在锁到期之后,不会自动续期。
- 锁的自动续期,如果业务超长,运行期间自动给锁续上了新的30S,不用担心业务时间长,锁自动过期被清理掉。
- 如果没有指定锁的超时时间,就使用30 * 1000【LockWatchdogTimeOut】看门狗的默认时间。
- 只要占锁成功,就会自动一个定时任务,重新给锁设置过期时间,新的过期时间就是看门狗的默认时间,每隔10S自动续期。
六、读写锁
(一) 测试
@GetMapping("/writeValue")
public JsonResult writeValue() {
RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");
String s = "";
RLock rLock = lock.writeLock();
try {
// 改数据加写锁,读数据加读锁
rLock.lock();
s = UUID.randomUUID().toString();
Thread.sleep(20000);
redisTemplate.opsForValue().set("writeValue", s);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
rLock.unlock();
}
return ResultUtil.success(s);
}
@GetMapping("/readValue")
public JsonResult readValue() {
RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");
String s = "";
// 加读锁
RLock rLock = lock.readLock();
try {
rLock.lock();
Thread.sleep(20000);
s = redisTemplate.opsForValue().get("writeValue");
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return ResultUtil.success(s);
}
(二) 补充
- 保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁、独享锁)。读锁是一个共享锁。
- 写锁没释放读就必须等待。
- 读 + 读:相当于无锁,并发读,只会在redis中记录号所有当前的读锁,他们都会同时加锁成功。
- 写 + 读:等待写锁释放。
- 写 + 写:阻塞方式。
- 读 + 写:有读锁,写也需要等待。
- 是要有写的存在都必须等待。
七、闭锁
@GetMapping("/lockDoor")
public JsonResult lockDoor() {
RCountDownLatch door = redissonClient.getCountDownLatch("door");
try {
door.trySetCount(5);
door.await(); // 等待锁全部完成
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return ResultUtil.success("放假了。。。。");
}
@GetMapping("/gogogo/{id}")
public JsonResult gogogo(@PathVariable("id") Long id) {
RCountDownLatch door = redissonClient.getCountDownLatch("door");
door.countDown(); // 计数减一
return ResultUtil.success(id + "班的人都走了...");
}
八、信号量
/**
* 信号量测试
*/
@GetMapping("/park")
public JsonResult park() {
RSemaphore park = redissonClient.getSemaphore("park");
// park.acquire(); // 获取一个信号,获取一个值,占一个车位
boolean b = park.tryAcquire();
return ResultUtil.success("ok =>" + b);
}
@GetMapping("/go")
public JsonResult go() {
RSemaphore park = redissonClient.getSemaphore("park");
park.release(); // 释放一个车位
return ResultUtil.success("ok");
}
信号量也可以用作限流:
/**
* 信号量测试
* 也可以用作分布式限流,例如:车库停车
*/
@GetMapping("/park")
public JsonResult park() {
RSemaphore park = redissonClient.getSemaphore("park");
// park.acquire(); // 获取一个信号,获取一个值,占一个车位
boolean b = park.tryAcquire();
if (b) {
// 执行业务
} else {
return ResultUtil.fail("error");
}
return ResultUtil.success("ok =>" + b);
}
@GetMapping("/go")
public JsonResult go() {
RSemaphore park = redissonClient.getSemaphore("park");
park.release(); // 释放一个车位
return ResultUtil.success("ok");
}