目录
应用场景:
当前分布式应用中,往往存在这样的场景,高并发流量访问db,为了防止数据库被打挂,通常在db层以上加一层cache。
当今市面上使用的最多的缓存是用redis来作缓存,众所周知,redis缓存存在三个常见问题:
- 缓存穿透:高并发访问的数据在数据库(db,cache)根本没有,导致流量穿过cache直接打到db
- 缓存雪崩:大量数据同时在cache中失效,导致流量直接打到db
- 缓存击穿:高并发流量访问热点数据,当热点数据在cache中失效的时候,导致高并发直接打到db
而应对上述问题的办法往往是:
- 缓存穿透:设置空结果缓存,并设置过期时间,使不存在的数据也可以使用缓存
- 缓存雪崩:设置过期时间(加随机值),使数据大概率不会同时失效
- 缓存击穿:加锁,当热点数据在cache中失效的时候,使高并发流量中只有一个线程会进入db,并设置cache,之后流量可以访问cache获取数据
针对解决第三个问题中的加锁,
当没有加分布式锁,而是只用类似 synchronize(this) 这样的方式加本地锁的话,对于分布式应用,每个服务都会使用自己单独的锁,从而导致整个分布式应用还是会有很多线程进入db,从而可能发生不可预计的错误。
因此需要加分布式锁锁住整个分布式应用。
基本原理:
同时去一个地方“占坑”,如果占到,就执行罗炯,否则就必须等待,直到释放锁。
“占坑”可以去redis,可以去数据库,可以去任何大家都能访问的地方。
等待可以使用自旋的方式。
实现方式一:
使用redis做分布式锁
思路:
使用redis的SET “lock” value NX方式占坑,这里要注意几个点。
1、设置过期时间,防止setnx占好了位,业务代码异常或者redis在业务执行过程中宕机,从而导致没有执行锁删除逻辑,造成死锁。
2、设置过期时间和占位必须是原子的,redis支持使用setnx ex命令。
3、由于业务时间可能很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了。因此我们需要指定锁的值为uuid,每个人匹配自己的锁才删除。
4、接续上一条,如果正好判断是当前值,正要删除锁的时候,锁已经过期,别人已经设置到了新的值,那么我们删除的就是别人的锁了。因此必须保证锁删除具有原子性,使用redis+Lua脚本完成。
//从数据库取数据 (使用redis做分布式锁)
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("获取分布式锁成功");
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";
//先去redis查询下保证当前的锁是自己的
//获取值对比,对比成功删除=原子性 lua脚本解锁
//解锁
redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
return dataFromDb;
} else {
System.out.println("获取分布式锁失败...等待重试...");
//加锁失败...重试机制
//休眠一百毫秒
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonFromDBWithRedisLock(); //自旋的方式
}
}
实现方式二:
使用Redission做分布式锁
引入redission依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.0</version>
</dependency>
基于Redis的Redisson分布式可重入锁 RLock 对象实现了Java concurrent包下的Lock接口。
一、配置RedissionClient
@Configuration
public class MyRedissonConfig {
/**
* 所有对Redisson的使用都是通过RedissonClient
* @return
* @throws IOException
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws IOException {
//1、创建配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.56.10:6379");
//2、根据Config创建出RedissonClient实例
//Redis url should start with redis:// or rediss://
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
二、自动注入并使用
@Autowired
private RedissonClient redisson;
@ResponseBody
@GetMapping(value = "/hello")
public String hello() {
//1、获取一把锁,只要锁的名字一样,就是同一把锁
RLock myLock = redisson.getLock("my-lock");
//2、加锁
myLock.lock();
try {
System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
try {
TimeUnit.SECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
//3、解锁 假设解锁代码没有运行,Redisson会不会出现死锁
System.out.println("释放锁..." + Thread.currentThread().getId());
myLock.unlock();
}
return "hello";
}
注意:
加锁处是阻塞式等待,默认假的锁过期时间都是30s。
Redission实现的锁,有一个“看门狗”机制,可以自动给锁续期,如果业务超长,运行时自动给锁续期,不用担心业务时间长,锁自动过期被删除。加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题。
有两种加锁方式,即是否指定过期时间:
1、myLock.lock()
2、myLock.lock(10,TimeUnit.SECONDS) //10秒钟自动解锁,自动解锁时间一定要大于业务执行时间
对于方式一:如果我们没有指定锁的超时时间,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】,只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒(internalLockLeaseTime)都会自动的再次续期,续成30秒
// internalLockLeaseTime 【看门狗时间】 / 3 10秒
对于方式二:如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时时间就是我们制定的时间,但存在锁时间到了以后不会自动续期的问题。