目录
上两篇文章介绍了,缓存的使用以及高并发下缓存失效带来的问题
接下来实际的解决方案。
在高并发下的缓存失效问题:缓存击穿、穿透、雪崩中我们说到缓存击穿、缓存穿透、缓存雪崩的解决方案是:
-
空结果缓存:解决缓存穿透
-
设置过期时间(加随机值):解决缓存雪崩
-
加锁:解决缓存击穿。
那我们在这里进行示例:
未解决之前的代码:
/**
* 查询所有分类信息从redis缓存中
*
* @return
*/
@Override
public Map<String, List<Catelog2Vo>> getCatalogJsonByRedisCache() {
String catalogJson = redisTemplate.opsForValue().get("catalogJson");
if (StringUtils.isEmpty(catalogJson)) {
Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
redisTemplate.opsForValue().set("catalogJson", JSON.toJSONString(catalogJsonFromDb), 30 * 1000L, TimeUnit.MILLISECONDS);
return catalogJsonFromDb;
}
Map<String, List<Catelog2Vo>> catalogJsonMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return catalogJsonMap;
}
一、本地锁示例:
加解决方案后:
public Map<String, List<Catelog2Vo>> getCatalogJsonByRedisCacheAndLocalLock() {
String catalogJson = redisTemplate.opsForValue().get("catalogJsonLocalLock");
if (StringUtils.isEmpty(catalogJson)) {
//当查询缓存为空时,尝试加锁
synchronized (this) {
//加锁成功后,再进行双重判断,避免不必要的多查一遍数据库
catalogJson = redisTemplate.opsForValue().get("catalogJsonLocalLock");
if (StringUtils.isEmpty(catalogJson)) {
//缓存依旧为空时,再查询数据库放入Redis中
Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
//将查询数据放入redis缓存中。(当然整个示例只有这个一个缓存,过期时间就没加随机值了)
redisTemplate.opsForValue().set("catalogJsonLocalLock", JSON.toJSONString(catalogJsonFromDb), 30 * 1000L, TimeUnit.MILLISECONDS);
return catalogJsonFromDb;
}
}
}
Map<String, List<Catelog2Vo>> catalogJsonMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return catalogJsonMap;
}
1、单机服务
jmeter并发汇总:
服务日志打印:
我们可以从日志中看出,确实只查询了一次数据量。
2、微服务时
jmeter并发汇总
服务日志打印:
8081端口服务打印:
8082端口服务打印:
可以看出,在请求时,两个服务都请求了一次数据库。我们加锁的本意并没有达成。
原因在于synchronized锁,只能作用于当前服务,不能跨服务加锁,在单机服务下,是可以的,但是在微服务情况下无法满足。
这个时候就需要使用到分布式锁。
常见实现分布式锁的方式有:数据库、Redis、Zookeeper。
这其中又以 Redis 最为常见。
二、Redis实现分布式锁示例
Redis官方给提供了加锁的方法,我们先看下,并示例:
1、setNx
代码示例:
/**
* 查询所有分类信息从redis缓存中加Redis setnx锁
*
* @return
*/
@Override
public Map<String, List<Catelog2Vo>> getCatalogJsonByRedisCacheAndRedisLockSetNx() {
String catalogJson = redisTemplate.opsForValue().get("catalogJsonRedisLock");
Map<String, List<Catelog2Vo>> catalogJsonMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
//判断缓存是否存在
if (CollectionUtils.isEmpty(catalogJsonMap)) {
//加锁
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("SetNxLock", uuid, 30L, TimeUnit.SECONDS);
//判断加锁是否成功
if (Boolean.TRUE.equals(lock)) {
try {
//加锁成功,双重判断
catalogJson = redisTemplate.opsForValue().get("catalogJsonRedisLock");
log.info("catalogJson: {}", catalogJson);
if (StringUtils.isEmpty(catalogJson)) {
catalogJsonMap = getCatalogJsonFromDb();
redisTemplate.opsForValue().set("catalogJsonRedisLock", JSON.toJSONString(catalogJsonMap), 30 * 1000L, TimeUnit.MILLISECONDS);
return catalogJsonMap;
}
catalogJsonMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return catalogJsonMap;
} catch (Exception e) {
log.error("{}", e.getMessage(), e);
} finally {
//解锁
redisTemplate.delete("SetNxLock");
}
} else {
//加锁失败,重试
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonByRedisCacheAndRedisLockSetNx();
}
}
return catalogJsonMap;
}
继续并发测试,服务的日志打印只查询了一次数据库。(必然的就不贴日志了)。
我们这里测试的是查询数据库的时间实际上是很短的。而我们设置的锁的过期时间是30秒,那这里有一个问题了:
如果,如果业务执行时间(查询数据库时间)超过了30秒,会怎么样呢?
我们来思考下这个过程发生了什么:
-
当线程一业务执行时间超过30秒,而锁已过期。这时线程二将拿到锁继续执行,将又执行一遍数据库查询。
-
线程二持有锁并且业务未执行完成时,第一个线程已经执行完成,又执行了释放锁的操作。(此时锁的持有者是线程二)
-
线程二执行完成,又刷新了一遍缓存,并且也将此时其他线程持有的锁释放。
这里我们能看到两个问题:
-
业务执行时间超过锁过期时间,锁自动释放。没发续期
-
锁被超过过期时间的其他线程释放。
如果只是像示例中的分类信息,倒不是特别在意这些。如果是涉及库存、订单及金额的就十分严重了。
首先,第二个问题,redis也给我们提供了解决方案:
示例代码:
/**
* 查询所有分类信息从redis缓存中加Redis setnx锁
*
* @return
*/
@Override
public Map<String, List<Catelog2Vo>> getCatalogJsonByRedisCacheAndRedisLockSetNx() {
String catalogJson = redisTemplate.opsForValue().get("catalogJsonRedisLock");
Map<String, List<Catelog2Vo>> catalogJsonMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
//判断缓存是否存在
if (CollectionUtils.isEmpty(catalogJsonMap)) {
//加锁
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("SetNxLock", uuid, 30L, TimeUnit.SECONDS);
//判断加锁是否成功
if (Boolean.TRUE.equals(lock)) {
try {
//加锁成功,双重判断
catalogJson = redisTemplate.opsForValue().get("catalogJsonRedisLock");
log.info("catalogJson: {}", catalogJson);
if (StringUtils.isEmpty(catalogJson)) {
catalogJsonMap = getCatalogJsonFromDb();
redisTemplate.opsForValue().set("catalogJsonRedisLock", JSON.toJSONString(catalogJsonMap), 30 * 1000L, TimeUnit.MILLISECONDS);
return catalogJsonMap;
}
catalogJsonMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return catalogJsonMap;
} catch (Exception e) {
log.error("{}", e.getMessage(), e);
} finally {
//解锁
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<>(script, Long.class), Collections.singletonList("SetNxLock"), uuid);
}
} else {
//加锁失败,重试
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonByRedisCacheAndRedisLockSetNx();
}
}
return catalogJsonMap;
}
通过脚本对比,当前线程加锁的随机字符串是否一致,一致则删除,否则不做处理。
对于第一个问题,Redis也给我提供了解决方案:redisson
2、Redisson
示例:可参考Redisson官网
第一步:引入maven依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
第二步:添加Redisson配置类
/**
* Redisson配置类
*/
@Configuration
public class RedissonConfig {
@Bean(destroyMethod="shutdown")
public RedissonClient getRedissonClient(){
Config config = new Config();
//单机配置
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
return Redisson.create(config);
}
}
第三步:引入依赖
@Autowired
private RedissonClient redissonClient;
第四步:使用
public Map<String, List<Catelog2Vo>> getCatalogJsonByRedisCacheAndRedissonLock() {
String catalogJson = redisTemplate.opsForValue().get("catalogJsonRedisLock");
Map<String, List<Catelog2Vo>> catalogJsonMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
if (CollectionUtils.isEmpty(catalogJsonMap)) {
RLock redissonLock = redissonClient.getLock("redissonLock");
try {
redissonLock.lock();
catalogJson = redisTemplate.opsForValue().get("catalogJsonRedisLock");
catalogJsonMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
if (CollectionUtils.isEmpty(catalogJsonMap)) {
catalogJsonMap = getCatalogJsonFromDb();
redisTemplate.opsForValue().set("catalogJsonRedisLock", JSON.toJSONString(catalogJsonMap), 30 * 1000L, TimeUnit.MILLISECONDS);
return catalogJsonMap;
}
return catalogJsonMap;
}finally {
redissonLock.unlock();
}
}
return catalogJsonMap;
}
使用不自定义过期时间(Redisson默认30秒)的锁,就可以解决锁过期问题了。
为了验证锁过期问题是否解决,我们加上打印及时间等待来观察:
public Map<String, List<Catelog2Vo>> getCatalogJsonByRedisCacheAndRedissonLock() {
String catalogJson = redisTemplate.opsForValue().get("catalogJsonRedisLock");
Map<String, List<Catelog2Vo>> catalogJsonMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
if (CollectionUtils.isEmpty(catalogJsonMap)) {
RLock redissonLock = redissonClient.getLock("redissonLock");
try {
redissonLock.lock();
log.info("加锁成功");
try {
Thread.sleep(33*1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
catalogJson = redisTemplate.opsForValue().get("catalogJsonRedisLock");
catalogJsonMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
if (CollectionUtils.isEmpty(catalogJsonMap)) {
catalogJsonMap = getCatalogJsonFromDb();
redisTemplate.opsForValue().set("catalogJsonRedisLock", JSON.toJSONString(catalogJsonMap), 30 * 1000L, TimeUnit.MILLISECONDS);
return catalogJsonMap;
}
return catalogJsonMap;
}finally {
redissonLock.unlock();
}
}
return catalogJsonMap;
}
日志打印:
我们可以看到,redisson锁的默认过期时间是30秒,而我又加了33秒的睡眠时间,日志打印的两个线程加锁的时间差是33秒,说明锁在业务执行完成之前,并没有过期。已经达到我们想要的结果。
Redisson为什么能达到这样的效果呢?
这依赖于Redisson的看门狗机制。