前言
- redis在项目中是经常会使用的一种缓存数据库,了解其一些配置和设计思路有利于我们更有效安全的使用它
- 这次介绍一下关于缓存设计优化和性能优化。
缓存设计解析
缓存穿透
- 缓存穿透是指查询一个根本不存在的数据,在缓存和数据库层都无法命中。
- 导致每次查询不存在的数据的请求最后都要到数据库层中查询,数据库查不出数据也无法写入缓存,失去了缓存过滤请求保护DB的意义。
- 而造成缓存穿透的原因基本有两个:
- 自身业务代码或者数据出现问题
- 一些恶意攻击,爬虫等造成大量空数据命中
- 缓存穿透的几种解决方案:
- 缓存空值对象:
/**
* @AUTHOR ZRH
* @DATE 2021/7/31
*/
@Service
public class RedisServiceImpl {
@Autowired
private RedisUtils redisUtils;
@Autowired
private StorageMapper storageMapper;
/**
* 缓存过期时间
*/
private final static Integer EXPIRE = 60000;
private final static Object DEFAULT_VALUE = new Object();
public String redisCache(String key) {
// 在缓存中获取数据
String value = redisUtils.get(key);
if (null == value) {
// 在数据库层获取数据
value = storageMapper.getDataToDB(key);
if (null == value) {
// 数据库查询数据为空,保存默认数据到缓存层,并设置较短的过期时间
redisUtils.set(key, DEFAULT_VALUE, 1000, TimeUnit.MILLISECONDS);
} else {
redisUtils.set(key, value, EXPIRE, TimeUnit.MILLISECONDS);
}
}
return value;
}
}
- 布隆过滤器:
- 对于恶意攻击,向服务器中请求大量不存在的数据造成缓存穿透(比如查询用户ID为-1的数据),可以用布隆过滤器先做一次过滤
- 当布隆过滤器说某个值存在,这个值可能不存在。当它说不存在,那就一定不存在
- 布隆过滤器具体实现方式可以参考文章# 分布式布隆过滤器实践
缓存击穿(失效)
- 某一热点数据(比如秒杀活动的某一热销商品信息)缓存时间过期了,这时正好有大量请求无法命中这一数据从而直达数据库,可能数据库压力瞬间增大导致机器宕机等
- 缓存击穿的几种解决方案:
- 设置热点数据永不过期(不推荐)
- 加互斥锁
/**
* @AUTHOR ZRH
* @DATE 2021/7/31
*/
@Service
public class RedisServiceImpl {
@Autowired
private RedisUtils redisUtils;
@Autowired
private StorageMapper storageMapper;
@Autowired
private RedissonClient redissonClient;
/**
* 缓存过期时间
*/
private final static Integer EXPIRE = 60000;
private final static Object DEFAULT_VALUE = new Object();
public String redisCache(String key) {
// 在缓存中获取数据
String value = redisUtils.get(key);
if (null == value) {
// 使用redisson实现分布式锁
RLock lock = redissonClient.getLock(key);
try {
lock.lock();
// 在数据库层获取数据
value = storageMapper.getDataToDB(key);
if (null == value) {
// 数据库查询数据为空,保存默认数据到缓存层,并设置较短的过期时间
redisUtils.set(key, DEFAULT_VALUE, 1000, TimeUnit.MILLISECONDS);
} else {
redisUtils.set(key, value, EXPIRE, TimeUnit.MILLISECONDS);
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
return value;
}
}
缓存雪崩
- 缓存雪崩指在某一时刻,缓存中有大量的key同时失效,导致请求直接命中数据库,导致数据库压力瞬间增大甚至宕机
- 缓存雪崩的几种解决方案:
- 设置热点数据永不过期(不推荐,一直占有资源)
- 使用互斥锁(不推荐,阻塞效率慢)
- 缓存标记(对热点数据进行监控,如果缓存过期会触发更新缓存数据)
- 针对不同key通过随机数设置不同的过期时间
int nextInt = new Random().nextInt(60000);
redisUtils.set(key, value, EXPIRE + nextInt, TimeUnit.MILLISECONDS);
缓存与数据库数据一致性问题
- 在高并发情况下,同时操作数据库和缓存会存在数据一致性问题
- 双写导致数据不一致
- 读写导致数据不一致
- 对于并发量小或者能容忍短时间内数据不一致的业务场景,基本不用考虑这个问题
- 对于并发量高且无法容忍数据不一致,可以通过加读写锁保证并发读写和写写的顺序执行,读读相当于无锁
- 可以使用阿里开源的canal通过监听mysql的binlog日志及时去修改缓存数据,需要引入新的中间件
性能优化、
键值设计
- key名设计尽量具有可读性、可管理性、简洁性、不要包含特殊字符
- 以业务名(或数据库名)为前缀(防止key冲突),用冒号分隔,比如业务名:表名:id
- 保证语义的前提下,控制key的长度,当key较多时,内存占用也不容忽视
- value设计拒绝bigkey(防止网卡流量,慢查询),在Redis中一个字符串最大512M,一个二级缓存数据结构(hash,list,set,zset)可以存储2^32-1个元素。但在下列情况也会认为是一个bigkey
- 字符串类型:它的big体现在单个value值很大,一般认为超过10KB就是bigkey
- 非字符串类型:哈希、列表、集合、有序集合,它们的big体现在元素个数太多。一般来说,string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000
- 拆分
- big list: list1、list2、…listN
- big hash:可以讲数据分段存储,比如一个大的key,假设存了1百万的用户数据,可以拆分成200个key,每个key下面存放5000个用户数据
- 如果bigkey不可避免,也要思考一下要不要每次把所有元素都取出来(例如有时候仅仅需要hmget,而不是hgetall),删除也是一样,尽量使用优雅的方式来处理。
过期策略
1. 被动删除:当读/写一个过期的数据时,会触发惰性删除,直接删除当前数据
2. 主动删除:被动删除无法保证冷数据被及时删除,所以Redis会定期删除一些过期数据
3. 当前已用内存超过maxmemory限定时,会触发主动清理策略
- 主动清理策略在Redis 4.0之前有6种内存淘汰策略,4.0后新增了两种lfu淘汰策略,共有8种:
1. volatile-ttl:针对设置了过期时间的键值对,根据过期时间的先后顺序进行删除,越早过期越先删除
2. volatile-random:对设置了过期时间的键值对,进行随机删除
3. volatile-lru:使用lru算法在设置了过期时间的键值对筛选删除
4. volatile-lfu:使用lfu算法在设置了过期时间的键值对筛选删除
5. allkeys-random:从所以键值对中随机删除
6. allkeys-lru:使用lru算法在所有数据的键值对筛选删除
7. allkeys-lfu:使用lfu算法在所有数据的键值对筛选删除
8. noeviction:不删除数据,内存满了后拒绝写入操作并返回客户端错误信息
最后