二.分布式锁 -前言:
1.分布式锁 原理 与 使用:
1)分布式锁 原理:
a:我们可以 让 多个服务,同时去一个地方 “占锁”,如果占到,就执行逻辑,否则就必须等待,直到 释放锁。
b:“占锁”,可以去 redis ,可以去 数据库,要去到 大家都能访问到的 地方,等待可以 自旋 的方式。
2.分布式锁 演进 :使用 redis 存储,占锁:
1):问题:
a:setnx 占好了位置,代码异常 / 程序在页面过程中 宕机。没有执行删除锁逻辑,这就造成了死锁。
2):解决:
a:设置 锁 的 自动过期,即使没有删除,会自动删除。
b:注意:redis 加锁保证原子性,解锁保证原子性。
c:如果 业务为执行完毕,锁过期,就要 锁自动延期。
3):代码逻辑:
@Override
public List<CategoryVo> listWithTree() {
//占用 分布式锁,去 redis 占坑,没有key 的时候,才能设置上。存在key,就需要等待。
//避免 删除 别人的锁
String s = UUID.randomUUID().toString();
//给锁设置 过期时间 30 毫秒
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
if (lock) {
//占到锁,执行 业务,
...
//查询玩之后要解锁。(删除 自己的锁)
String lock2 = (String) redisTemplate.opsForValue().get("lock");
if (uuid.equals(lock2)) {
Boolean lock1 = redisTemplate.delete("lock");
}
return getDataFromDb();
} else {
//没占到锁,休眠 100s 后重试
// 自旋 的方式,自己调用 自己的方法。
return listWithTree();
}
}
二.分布式锁 - Redisson 框架
官网地址:(https://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95)
1.分布式锁 原理 与 使用:
1)引入 依赖:
<!--以后 使用 redisson ,作为所有 分布式锁,分布式对象 等功能框架-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.5</version>
</dependency>
2):redisson 配置方法:(单Redis节点模式)(文档)
@Configuration
public class MyRedissonConfig {
@Bean(destroyMethod = "shutdown")
public RedissonClient redisson() throws IOException {
Config config = new Config();
config.useSingleServer().setAddress("redis://114.215.173.88:6379");
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
2):redisson 简单使用方法:(文档)
@Autowired
private RedissonClient redissonClient;
@RequestMapping("/hello")
public R hello() {
RLock lock = redissonClient.getLock("my-lock");
//加锁 1 //阻塞式等待:拿不到锁一直等待。
// 1).锁的 自动续期,如果业务超长,运行期间,会自动给锁续上新的 30s。不用担心业务时间长,锁自动过期被删掉。
// 2).加锁的业务,只要运行完成,就不会给当前锁续期,即使不手动解锁,锁 默认在 30s 以后,自动删除。
// lock.lock();
//加锁 2 // 10秒,自动落锁,自动解锁时间,一定要大于业务执行时间。
// lock.lock(20, TimeUnit.SECONDS);
//问题:10秒 锁的时间到期以后,不会自动续期
// 1).如果我们传递了 锁的超时时间,就发送给 redis 执行脚本,进行占锁,默认超时就是我们指定的时间。
// 2).如果我们 未指定锁的超时时间,就使用 30 * 1000【 看门狗 默认时间 】 ;
// 只要 占锁成功,就会启动一个定时任务【 重新给锁 设置过期时间,新的过期时间,就是看门狗的默认时间 】
// 三分之一的 看门狗时间,自动续期 。每隔十秒就是 自动再次续期,续成 30 s。
// 最佳实战
// 1).省掉了 这个那个续期操作,手动解锁
lock.lock(30, TimeUnit.SECONDS);
try {
//业务代码
System.out.println("业务代码");
} catch (Exception e) {
e.printStackTrace();
} finally {
//解锁:为了防止 业务代码异常
lock.unlock();
}
return R.ok();
}
2.读写锁原理 与 使用:
1)读写锁 作用:
a:改数据加写锁,读数据加读锁。( 改的时候不能读,读的时候不能改 )(写锁是一个 互斥锁,独享锁)
b:写锁没释放,读必须等待
c:几种情况分析:
先写 + 后读 :等待写锁释放
先写 + 后写 :等待写锁释放
先读 + 后写 :等待 读锁释放,才能进行写锁加锁。
先读 + 后读 :不需要等待 读锁的 释放,相当于 无锁状态,所有的读锁,都会加锁成功。
d:总结:只要有写的存在,都要 等待锁。
2)代码示例 – 写锁:
@RequestMapping("/write")
public R write() throws InterruptedException {
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-clock");
RLock rLock = readWriteLock.writeLock();
String s = "";
try {
//1)改数据,加写锁
rLock.lock();
s = UUID.randomUUID().toString();
Thread.sleep(30000);
stringRedisTemplate.opsForValue().set("write", s);
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return R.ok().put("data", s);
}
3)代码示例 – 读锁:
@RequestMapping("/read")
public R read() throws InterruptedException {
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-clock");
RLock rLock = readWriteLock.readLock();
rLock.lock();
String read = null;
try {
read = stringRedisTemplate.opsForValue().get("write");
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return R.ok().put("data", read);
}
3.闭锁 原理 与 使用:
1)闭锁 介绍:
a:等待 其他 全部完成,才来执行主程序。
b:案例:学校放假锁门,要等 5 个班,全部没人了,才能锁门。
解决:上面 主程序,下面 子程序,(子程序都走完,主程序才走)
@RequestMapping("/lockDoor")
public R lockDoor() throws InterruptedException {
RCountDownLatch door = redissonClient.getCountDownLatch("door");
//等待 闭锁都完成(等待,计数 自减 5次)
door.trySetCount(5);
door.await();
return R.ok();
}
public String gogogo(Long id) {
RCountDownLatch door = redissonClient.getCountDownLatch("door");
//计数 减一
door.countDown();
return id + "班级人都走了";
}
4.信号量 原理 与 使用:
1)信号量 介绍:
a:
b:案例 1:车库停车,一共三个车位,来一个车加一个;走一个车,减掉一个。
解决:上面程序是车进来,下面程序是车走。
@RequestMapping("/parkcome")
public R parkcome() throws InterruptedException {
RSemaphore semaphore = redissonClient.getSemaphore("park-lock");
//获取一个信号(一个值)(占一个车位)
//阻塞式获取,一定要获取到一个车位。
semaphore.acquire();
return R.ok();
}
@RequestMapping("/parkgo")
public R parkgo() throws InterruptedException {
RSemaphore semaphore = redissonClient.getSemaphore("park-lock");
//释放一个信号(一个值)(释放一个车位)
semaphore.release();
return R.ok();
}
c:案例 1:限流操作:
解决:如果 没有获取到车位,直接返回 false。不阻塞。
@RequestMapping("/parkcome")
public R parkcome() throws InterruptedException {
RSemaphore semaphore = redissonClient.getSemaphore("park-lock");
//尝试 获取一个车位。不阻塞,没有直接返回 false
boolean b = semaphore.tryAcquire();
if (b) {
//执行业务
} else {
return R.ok().put("data", "当前人数过多,请稍后访问");
}
return R.ok();
}
4.分布式 缓存一致性 解决:
1)缓存数据 和 数据库数据,保持一致:
a:双写模式:改完数据库,更新缓存。
双写模式 会产生 缓存数据不一致解决方案:
方案1:写 数据库 和 写缓存,加锁,保持 数据库 和 缓存 数据一致性。
方案2:看 服务 允不允许 暂时性 数据不一致 问题。缓存到了过期时间后,自动删除。缓存更新。
b:失效模式:改完数据库,删除缓存。(使用多)
失效模式 会产生 缓存数据不一致解决方案:(和双写模式解决方案一样)
方案1:写 数据库 和 写缓存,加锁,保持 数据库 和 缓存 数据一致性。
方案2:看 服务 允不允许 暂时性 数据不一致 问题。缓存到了过期时间后,自动删除。缓存更新。
2)缓存数据 一致性 解决方案:
a:无论是 双写模式 还是 失效模式,都会导致 缓存的不一致问题,即 多个实例同时更新 会产生。
情况 1:如果是 用户 纬度数据,这种并发几率很小,不用考虑这个问题,缓存数据加上过期时间,一段时间自动更新即可。 情况 2:如果是 菜单,商品介绍等基础数据,大部分是可以容忍暂时缓存不一致问题。(也可以使用 canal 订阅 binlog 的方式 解决)
情况 3:缓存数据 + 过期时间,也足够解决 大部分业务对于缓存的要求。
情况 4:加 读写锁,写的时候排队,读不需要排队。
b:我们系统 一致性解决方案:
方案 1:缓存所有的数据,都有过期时间,数据过期,下一次查询主动触发更新。
方案 2:读写数据的时候,加上 分布式的 分布式读写锁。经常写,经常读。
c:使用 Canal :只要更新 数据库,就自动更新缓存。
3)总结:
a:放入缓存的,就不应该是,实时性,一致性,要求超高的,所以,大部分缓存,只要加上过期时间,保证拿到当天最新数据即可。
b:遇到实时性,一致性要求高的数据,就应该直接查询数据库,即使速度会慢一点,