一.起步
1.导入依赖
<!--redis引用-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<!--排除spring-boot-starter-data-redis使用的默认客户端
BUG:永远会造成堆外内存溢出异常-->
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--使用jedis客户端-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
2.配置Redis连接信息
spring:
redis:
host: xxx.xx.xxx.xxx
password: 密码
database: 7 //使用的数据库
3.简单测试
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
void testStringRedisTemplate(){
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
// 保存
ops.set("msg","hello,world"+ UUID.randomUUID().toString());
// 查询
String msg = ops.get("msg");
System.out.println(msg);
}
结果:打印 hello,world67245659-1f7f-42cb-98d2-bed399d100ba
4.实际业务运用
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
// 1、加入缓存逻辑,缓存中存的数据的json字符串
String catelogData = redisTemplate.opsForValue().get("catelogData");
if(StringUtils.isEmpty(catelogData)){
// 缓存中没有,则查询数据库
Map<String, List<Catelog2Vo>> catalogJsonFromDB = getCatalogJsonFromDB();
// 将查到的数据以json存入redis
String catalogJsonString = JSON.toJSONString(catalogJsonFromDB);
redisTemplate.opsForValue().set("catelogData",catalogJsonString);
// 将查询到的数据直接放回
return catalogJsonFromDB;
}
// 返回指定对象
Map<String, List<Catelog2Vo>> res = JSON.parseObject(catelogData, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return res;
}
二.缓存失效问题
1、缓存穿透
- 缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数 据库也无此记录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次 请求都要到存储层去查询,失去了缓存的意义。
- 在流量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是 漏洞。
- 解决:
缓存空结果、并且设置短的过期时间。
2、缓存雪崩
- 缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失 效,请求全部转发到 DB,DB 瞬时压力过重雪崩。
- 解决:
原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的 重复率就会降低,就很难引发集体失效的事件。
3、缓存击穿
- 对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问, 是一种非常“热点”的数据。
- 这个时候,需要考虑一个问题:如果这个 key 在大量请求同时进来前正好失效,那么所 有对这个 key 的数据查询都落到 db,我们称为缓存击穿。
- 解决:
加锁
三.分布式锁与本地锁
场景:
1.实现本地锁(适合单体架构项目)
请求进来首先查询缓存,缓存未命中则查询数据,所以我们要在查询数据库的代码加上锁,这里使用 synchronized(this){ 数据库查询....},注意,在查询完数据库后,将查到的数据放入缓存中,这一步一定也要在synchronized代码块中,否则可能第一个请求查询完数据库后并未将数据放入缓存就释放了锁,第二个请求查询缓存就未命中,导致重复查询数据库。
查询数据库代码如下:
/**
* 从数据库查询数据并封装数据
* @return
*/
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDB() {
synchronized(this){
// 只要是同一把锁,就能锁住需要这个锁的所有线程
String catelogData = redisTemplate.opsForValue().get("catelogData");
if(!StringUtils.isEmpty(catelogData)){
// 返回指定对象
Map<String, List<Catelog2Vo>> res = JSON.parseObject(catelogData, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return res;
}
/**
* 1、将数据库多次查询改为一次
*/
List<CategoryEntity> selectList = baseMapper.selectList(null);
// 1 查出所有分类
List<CategoryEntity> level1Categorys = getParent_cid(selectList,0l);
Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
List<CategoryEntity> categoryEntities = getParent_cid(selectList,v.getCatId());
List<Catelog2Vo> catelog2Vos = null;
if (categoryEntities != null) {
catelog2Vos = categoryEntities.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
List<CategoryEntity> level3category = getParent_cid(selectList,l2.getCatId());
if(level3category!=null){
List<Catelog2Vo.Catelog3Vo> collect = level3category.stream().map(l3 -> {
Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(),l3.getCatId().toString(),l3.getName());
return catelog3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(collect);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
// 将查到的数据以json存入redis
String catalogJsonString = JSON.toJSONString(parent_cid);
redisTemplate.opsForValue().set("catelogData",catalogJsonString,12,TimeUnit.HOURS);
return parent_cid;
}
}
2.简单实现分布式锁
基本逻辑:
代码实现:
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDBWithReidsLock() {
// 1、占分布式锁,去redis占锁
// setIfAbsent 相当于 setnx 有值 则 不设置
// 设置过期时间,以防程序发生错误或机器断电造成死锁
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if(lock){
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";
Long delLock = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
return dataFromDB;
}else{
// 加锁失败...重试
return getCatalogJsonFromDBWithReidsLock(); // 自旋的方式
}
}
其中: 必须保证 加锁与设置过期时间、查询锁与删锁必须为原子操作,且无论业务代码执行如何,最终都要释放锁,避免死锁。
解锁需使用Lua脚本,在查询到时redis就帮我们删除该Key
Lua脚本:
if redis call('get',KEYS[1]) == ARGV[1] then return redis call('del',KEYS[1]) else return 0 end