缓存设计
缓存穿透:
就是redis始终查不到,导致每次都越过缓存,去查DB
失去了缓存保护DB的意义
原因
- 代码问题,数据问题
- 恶意攻击
解决方案:
1:缓存空对象
过期时间设置
String get(String key) {
// 从缓存中获取数据
String cacheValue = cache.get(key);
// 缓存为空
if (StringUtils.isBlank(cacheValue)) {
// 从存储中获取
String storageValue = storage.get(key);
cache.set(key, storageValue);
// 如果存储数据为空, 需要设置一个过期时间(300秒)
if (storageValue == null) {
cache.expire(key, 60 * 5);
}
return storageValue;
} else {
// 缓存非空
return cacheValue;
}
}
2:布隆过滤器
判断一个key是否存在
不能保证一定存在,但一定确定不存在
大型的位数组和多个无篇hash函数
- 把key进行hash运算,得到整数索引,
- 对位数组取模,把对应位置为1
- 同样对其他数组,用不同的hash函数,把其他数组位改成1
判断存在时:
同样把key,经过同样的Hash ,取对应位的数值,是否都是1,
如果有一个0 ,表示不存在
也有可能从都是1, 也不存在
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
public class RedissonBloomFilter {
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
//构造Redisson
RedissonClient redisson = Redisson.create(config);
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
//初始化布隆过滤器:预计元素为100000000L,误差率为3%,根据这两个参数会计算出底层的bit数组大小
bloomFilter.tryInit(100000000L,0.03);
//将zhuge插入到布隆过滤器中
bloomFilter.add("zhuge");
//判断下面号码是否在布隆过滤器中
System.out.println(bloomFilter.contains("guojia"));//false
System.out.println(bloomFilter.contains("baiqi"));//false
System.out.println(bloomFilter.contains("zhuge"));//true
}
}
//初始化布隆过滤器
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
//初始化布隆过滤器:预计元素为100000000L,误差率为3%
bloomFilter.tryInit(100000000L,0.03);
//把所有数据存入布隆过滤器
void init(){
for (String key: keys) {
bloomFilter.put(key);
}
}
String get(String key) {
// 从布隆过滤器这一级缓存判断下key是否存在
Boolean exist = bloomFilter.contains(key);
if(!exist){
return "";
}
// 从缓存中获取数据
String cacheValue = cache.get(key);
// 缓存为空
if (StringUtils.isBlank(cacheValue)) {
// 从存储中获取
String storageValue = storage.get(key);
cache.set(key, storageValue);
// 如果存储数据为空, 需要设置一个过期时间(300秒)
if (storageValue == null) {
cache.expire(key, 60 * 5);
}
return storageValue;
} else {
// 缓存非空
return cacheValue;
}
}
注意:布隆过滤器不能删除数据,如果要删除得重新初始化数据。
缓存失效(击穿)
缓存同时批量,大量的同时失效导致,可能会造成宕机引发雪崩
解决方案:
设置的一个随机的超时时间,默认时间+ 一个随机数
String get(String key) {
// 从缓存中获取数据
String cacheValue = cache.get(key);
// 缓存为空
if (StringUtils.isBlank(cacheValue)) {
// 从存储中获取
String storageValue = storage.get(key);
cache.set(key, storageValue);
//在一个基准时间上设置一个时间范围
int expireTime = new Random().nextInt(300) + 300;
if (storageValue == null) {
cache.expire(key, expireTime);
}
return storageValue;
} else {
// 缓存非空
return cacheValue;
}
}
缓存雪崩
由一个redis挂了,大量请求到DB,,导致DB挂了,进而影响服务挂了,
解决方案:
保证高可用,集群,主从,哨兵
- 消费降级,控流
- 提前演练
热点缓存key重建优化
- 热点事件导致冷数据变成热点
- 重建缓存时,需要时间计算
解决方案
在实际获取DB数据的地方加上分布式锁
String get(String key) {
// 从Redis中获取数据
String value = redis.get(key);
// 如果value为空, 则开始重构缓存
if (value == null) {
// 只允许一个线程重建缓存, 使用nx, 并设置过期时间ex
String mutexKey = "mutext:key:" + key;
if (redis.set(mutexKey, "1", "ex 180", "nx")) {
// 从数据源获取数据
value = db.get(key);
// 回写Redis, 并设置过期时间
redis.setex(key, timeout, value);
// 删除key_mutex
redis.delete(mutexKey);
}// 其他线程休息50毫秒后重试
else {
//这个设计很巧妙,值得借鉴
Thread.sleep(50);
get(key);
}
}
return value;
}
缓存数据库双写不一致
双写不一致:
读写不一致
延迟双删
就是删除之后,一段时间后再删一次,没必要
解决方案:
- **无法容忍的不一致数据,库存:分布式锁读写锁
- cannal监听binlog日志,同步缓存
增加了系统复杂度,不太建议 - 个人维度的信息,不太重要的不一致数据,加过期时间,每隔一段时间主动更新即可
- 并发高,但不太敏感的数据不一致,名称,分类,可以加时间,定期更新即可
读多写少:缓存效果好
写多读多: 不建议缓存
不要为了绝对的一致性过度设计和控制,增加系统负责性
开发规范
一:键值设计
1:key名称设计:
- 可读性,可管理性
- 简洁性,不要太长
- 不要包含特殊字符
2: value设计
- 拒绝bigkey,网络问题,慢查询
redis字符串最大可以512M,实际上不要这么干
1:字符串类型:超过10k就算大了
2:集合类型,元素不要超过5000个 - 非字符串bigkey ,不要用del
使用hscan, sscan,zscan 渐进式删除,
防范bigkey过期自动删除的问题,会引起del,造成阻塞
bigkey危害
1:redis阻塞
2:网络拥塞,
3:过期删除
redis4.0要设置过期异步删除否则会阻塞
lazyfree-lazy-expire yes
bigkey的产生
设计不合理导致的
1:社交类,粉丝列表
2:统计类,
3:缓存类,直接序列化放进缓存
优化bigkey
- 拆分
list ,hash - 如果不可拆分
避免每次取出全部,尽量hmget 而不是 hgetall - 合理设计数据类型
比如实体可以放在一个hash中,而不是对每个属性设计一个字符串缓存 - 控制key的生命周期
避免缓存同时失效,
二:命令使用规范
- 【推荐】 O(N)命令关注N的数量
hgetall、lrange、smembers、zrange、sinter等并非不能使用,但是需要明确N的值。有遍历的需求可以使用hscan、sscan、zscan代替。 - 禁用命令
禁止线上使用keys、flushall、flushdb等,通过redis的rename机制禁掉命令,或者使用scan的方式渐进式处理。 - 【推荐】使用批量操作提高效率
原生命令:例如mget、mset。 非原生命令:可以使用pipeline提高效率。 -
三: 客户端使用规范
避免多个应用使用一个Redis实例
- 使用带有连接池的数据库
连接池设计
| 序号 | 参数名 | 含义 | 默认值 | 使用建议 | | —- | —- | —- | —- | —- | | 1 | maxTotal | 资源池中最大连接数 | 8 | 设置建议见下面 | | 2 | maxIdle | 资源池允许最大空闲的连接数 | 8 | 设置建议见下面 | | 3 | minIdle | 资源池确保最少空闲的连接数 | 0 | 设置建议见下面 | | 4 | blockWhenExhausted | 当资源池用尽后,调用者是否要等待。只有当为true时,下面的maxWaitMillis才会生效 | true | 建议使用默认值 | | 5 | maxWaitMillis | 当资源池连接用尽后,调用者的最大等待时间(单位为毫秒) | -1:表示永不超时 | 不建议使用默认值 | | 6 | testOnBorrow | 向资源池借用连接时是否做连接有效性检测(ping),无效连接会被移除 | false | 业务量很大时候建议设置为false(多一次ping的开销)。 | | 7 | testOnReturn | 向资源池归还连接时是否做连接有效性检测(ping),无效连接会被移除 | false | 业务量很大时候建议设置为false(多一次ping的开销)。 | | 8 | jmxEnabled | 是否开启jmx监控,可用于监控 | true | 建议开启,但应用本身也要开启 |
参数:
maxTotal : 最大连接数
nodes(应用个数)* maxTotal 不能超过maxClients**maxIdle和minIdle
最大连接数:给出余量,maxIdle,不要过小,否则会new jedis
最佳 maxTotal = maxIdle 避免连接池伸缩带来的干扰
minIdle:最小空闲连接数,至少需要保持的空闲连接数
**
线程池的预热:
让连接池的空闲链接快速提升到minIdle
List<Jedis> minIdleJedisList = new ArrayList<Jedis>(jedisPoolConfig.getMinIdle());
for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
Jedis jedis = null;
try {
jedis = pool.getResource();
minIdleJedisList.add(jedis);
jedis.ping();
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
//注意,这里不能马上close将连接还回连接池,否则最后连接池里只会建立1个连接。。
//jedis.close();
}
}
//统一将预热的连接还回连接池
for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
Jedis jedis = null;
try {
jedis = minIdleJedisList.get(i);
//将连接归还回连接池
jedis.close();
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
}
}
高并发下建议添加熔断功能
合适使用密码
Redis过期策略
- 被动删除:当读写一个已经过期的key时,才去删掉
- 主动删除:定期主动删除已过期的key,只是一批一批的删,不是同时
- 当已用内存超过maxMemory,会触发主动策略
淘汰策略
主动清理策略在Redis 4.0 之前一共实现了 6 种内存淘汰策略,在 4.0 之后,又增加了 2 种策略,总共8种:
a) 针对设置了过期时间的key做处理:
- volatile-ttl:在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期的越先被删除。
- volatile-random:就像它的名称一样,在设置了过期时间的键值对中,进行随机删除。
- volatile-lru:会使用 LRU 算法筛选设置了过期时间的键值对删除。
- volatile-lfu:会使用 LFU 算法筛选设置了过期时间的键值对删除。
b) 针对所有的key做处理:
- allkeys-random:从所有键值对中随机选择并删除数据。
- allkeys-lru:使用 LRU 算法在所有数据中进行筛选删除。
- allkeys-lfu:使用 LFU 算法在所有数据中进行筛选删除。
c) 不处理:
- noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息”(error) OOM command not allowed when used memory”,此时Redis只响应读操作。
LRU 算法(Least Recently Used,最近最少使用)
淘汰很久没被访问过的数据,以最近一次访问时间作为参考。
热点数据效果好,
LFU 算法(Least Frequently Used,最不经常使用)
淘汰最近一段时间被访问次数最少的数据,以次数作为参考。
根据自身业务类型,配置好maxmemory-policy(默认是noeviction),推荐使用volatile-lru。如果不设置最大内存,当 Redis 内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换 (swap),会让 Redis 的性能急剧下降。
当Redis运行在主从模式时,只有主结点才会执行过期删除策略,然后把删除操作”del key”同步到从结点删除数据