Redis实现缓存及相关问题

认识缓存

缓存就是数据交换的缓冲区,是存贮数据的临时地方,一般读写性能较高。

缓存的作用:

降低后端负载提高读写效率,降低响应时间

缓存的成本:

数据一致性成本代码维护成本运维成本

添加缓存

缓存作用模型

查询商铺缓存的流程

添加缓存业务代码

@Override

public List getUserlist() {

Gson gson = new Gson();

// 1. 查询redis缓存

String cache = redisTemplate.opsForValue().get(CACHE_LIST_PRE);

// 2.1. 存在缓存

if (StrUtil.isNotBlank(cache)) {

// 3. 反序列化

return gson.fromJson(cache, new TypeToken>() {}.getType());

}

// 2.2. 不存在缓存

// 3. 查询数据库

List userList = list();

// 4. 信息脱敏

ArrayList userDTOList = new ArrayList<>();

for (User user : userList) {

UserDTO dto = new UserDTO();

BeanUtil.copyProperties(user, dto);

dto.setPhone(DesensitizedUtil.mobilePhone(dto.getPhone()));

userDTOList.add(dto);

}

// 5. 保存到Redis

redisTemplate.opsForValue()

.set(CACHE_LIST_PRE, gson.toJson(userDTOList), 2, TimeUnit.MINUTES);

return userDTOList;

}

缓存更新

缓存更新策略

内存淘汰超时剔除主动更新说明利用Redis的内存淘汰机制,内存不足时自动淘汰部分数据。给缓存数据添加TTL时间,到期后自动删除缓存。下次查询时更新缓存。编写业务逻辑,在修改数据库的同时,更新缓存。一致性差一般好维护成本无低高

低一致性需求:使用内存淘汰机制高一致性需求:主动更新 + 超时剔除

主动更新策略

读操作:

缓存命中则直接返回缓存未命中则查询数据库,并写入缓存,设定超时时间 写操作:

先操作数据库,然后再删除缓存要确保数据库与缓存操作的原子性(事物/分布式事物)

缓存穿透

什么是缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

缓存空对象

优点:实现简单,维护方便

缺点:额外的内存消耗、可能造成短期的不一致

业务实现

// 缓存空对象

@Override

public UserDTO getInfoById(Long id) {

// 缓存查询

String userString = redisTemplate.opsForValue()

.get(CACHE_USER_PRE + id);

if (StrUtil.isNotBlank(userString)) {

// 有缓存 => 真实数据

return gson.fromJson(userString, UserDTO.class);

}

if (userString != null) {

// 有缓存 => 空对象

throw new BusinessException(404, "用户不存在");

}

// 数据库查询

User user = getById(id);

if (user == null) {

// 缓存空对象

redisTemplate.opsForValue()

.set(CACHE_USER_PRE + id, "", 2, TimeUnit.MINUTES);

throw new BusinessException(404, "用户不存在");

}

// 信息脱敏

UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);

// 缓存真实数据

redisTemplate.opsForValue()

.set(CACHE_USER_PRE + id, gson.toJson(userDTO), 2, TimeUnit.MINUTES);

return userDTO;

}

布隆过滤器

优点:内存占用较少,没有多余key

缺点:实现复杂、存在误判可能

缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

给不同的Key的TTL添加随机值利用Redis集群提高服务的可用性给缓存业务添加降级限流策略给业务添加多级缓存

缓存击穿/热点Key

什么是缓存击穿

缓存击穿问题也叫热点Key问题:一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

互斥锁

优点:没有额外的内存消耗、保证一致性、实现简单

缺点:线程需要等待,性能受影响、可能有死锁风险

互斥锁流程图

互斥锁业务代码

// 热点key-互斥锁

@Override

public List getUserlist() throws InterruptedException {

// 1. 查询redis缓存

String cache = redisTemplate.opsForValue().get(CACHE_LIST_PRE);

// 2.1. 存在缓存

if (StrUtil.isNotBlank(cache)) {

// 3. 反序列化

return gson.fromJson(cache, new TypeToken>() {}.getType());

}

// ⭐️ 获取互斥锁

String lock = REDIS_LOCK_PRE + "userlist";

Boolean flag = redisTemplate.opsForValue()

.setIfAbsent(lock, "1", 30, TimeUnit.SECONDS);

if (BooleanUtil.isFalse(flag)) {

// ⭐️ 获取锁失败了 => 休眠 + 递归

Thread.sleep(200);

return getUserlist();

}

ArrayList userDTOList = new ArrayList<>();

try {

// 2.2. 不存在缓存

// 3. 查询数据库

List userList = list();

log.info("查询数据库");

// 4. 信息脱敏

for (User user : userList) {

UserDTO dto = new UserDTO();

BeanUtil.copyProperties(user, dto);

dto.setPhone(DesensitizedUtil.mobilePhone(dto.getPhone()));

userDTOList.add(dto);

}

// 5. 保存到Redis

redisTemplate.opsForValue()

.set(CACHE_LIST_PRE, gson.toJson(userDTOList), 10, TimeUnit.SECONDS);

} catch (Exception ignored) {

} finally {

// ⭐️ 释放锁

redisTemplate.delete(lock);

}

return userDTOList;

}

逻辑过期

优点:线程无需等待,性能较好

缺点:不保证一致性、有额外内存消耗、实现复杂

逻辑过期流程图

逻辑过期业务代码

1、LogicalExpiration逻辑过期实体类,使用泛型使其通用化

@Data

@AllArgsConstructor

@NoArgsConstructor

public class LogicalExpiration {

private T value;

private Date date;

}

2、核心业务代码

public UserDTO getUserById(Long id) {

// 1. 查询缓存

String userString = redisTemplate.opsForValue().get(CACHE_USER_PRE + id);

if (StrUtil.isBlank(userString)) {

// 没有缓存 -> 查询数据

User user = getById(id);

// 没有数据 -> 报错

if (user == null) {

throw new BusinessException(500, "用户不存在");

}

// 数据脱敏

UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);

// 新建缓存

Date date = new Date();

date.setTime(System.currentTimeMillis() + 2 * 60 * 1000);

redisTemplate.opsForValue().set(CACHE_USER_PRE + id, gson.toJson(

new LogicalExpiration<>(userDTO, date)

));

// 返回数据

return userDTO;

}

// 存在缓存 => 反序列化拿到对象

LogicalExpiration logicalExpiration = gson.fromJson(

userString, new TypeToken>() {}.getType());

// 判断缓存是否过期

if (logicalExpiration.getDate().before(new Date())) {

// 已经过期 => 新建线程进行更新缓存

ExecutorService executorService = Executors.newSingleThreadExecutor();

executorService.execute(() -> {

UserDTO userDTO = BeanUtil.copyProperties(getById(id), UserDTO.class);

Date date = new Date();

// 设置TTL为2min

date.setTime(System.currentTimeMillis() + 2 * 60 * 1000);

redisTemplate.opsForValue().set(CACHE_USER_PRE + id, gson.toJson(

new LogicalExpiration<>(userDTO, date)

));

});

}

// 返回数据

return logicalExpiration.getValue();

}

推荐阅读

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。