缓存设计

缓存穿透:

就是redis始终查不到,导致每次都越过缓存,去查DB
失去了缓存保护DB的意义
原因

  1. 代码问题,数据问题
  2. 恶意攻击

解决方案:

1:缓存空对象
过期时间设置

  1. String get(String key) {
  2. // 从缓存中获取数据
  3. String cacheValue = cache.get(key);
  4. // 缓存为空
  5. if (StringUtils.isBlank(cacheValue)) {
  6. // 从存储中获取
  7. String storageValue = storage.get(key);
  8. cache.set(key, storageValue);
  9. // 如果存储数据为空, 需要设置一个过期时间(300秒)
  10. if (storageValue == null) {
  11. cache.expire(key, 60 * 5);
  12. }
  13. return storageValue;
  14. } else {
  15. // 缓存非空
  16. return cacheValue;
  17. }
  18. }

2:布隆过滤器

判断一个key是否存在
不能保证一定存在,但一定确定不存在

大型的位数组和多个无篇hash函数

  1. 把key进行hash运算,得到整数索引,
  2. 对位数组取模,把对应位置为1
  3. 同样对其他数组,用不同的hash函数,把其他数组位改成1

判断存在时:
同样把key,经过同样的Hash ,取对应位的数值,是否都是1,
如果有一个0 ,表示不存在
也有可能从都是1, 也不存在

  1. <dependency>
  2. <groupId>org.redisson</groupId>
  3. <artifactId>redisson</artifactId>
  4. <version>3.6.5</version>
  5. </dependency>
  1. public class RedissonBloomFilter {
  2. public static void main(String[] args) {
  3. Config config = new Config();
  4. config.useSingleServer().setAddress("redis://localhost:6379");
  5. //构造Redisson
  6. RedissonClient redisson = Redisson.create(config);
  7. RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
  8. //初始化布隆过滤器:预计元素为100000000L,误差率为3%,根据这两个参数会计算出底层的bit数组大小
  9. bloomFilter.tryInit(100000000L,0.03);
  10. //将zhuge插入到布隆过滤器中
  11. bloomFilter.add("zhuge");
  12. //判断下面号码是否在布隆过滤器中
  13. System.out.println(bloomFilter.contains("guojia"));//false
  14. System.out.println(bloomFilter.contains("baiqi"));//false
  15. System.out.println(bloomFilter.contains("zhuge"));//true
  16. }
  17. }
  1. //初始化布隆过滤器
  2. RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
  3. //初始化布隆过滤器:预计元素为100000000L,误差率为3%
  4. bloomFilter.tryInit(100000000L,0.03);
  5. //把所有数据存入布隆过滤器
  6. void init(){
  7. for (String key: keys) {
  8. bloomFilter.put(key);
  9. }
  10. }
  11. String get(String key) {
  12. // 从布隆过滤器这一级缓存判断下key是否存在
  13. Boolean exist = bloomFilter.contains(key);
  14. if(!exist){
  15. return "";
  16. }
  17. // 从缓存中获取数据
  18. String cacheValue = cache.get(key);
  19. // 缓存为空
  20. if (StringUtils.isBlank(cacheValue)) {
  21. // 从存储中获取
  22. String storageValue = storage.get(key);
  23. cache.set(key, storageValue);
  24. // 如果存储数据为空, 需要设置一个过期时间(300秒)
  25. if (storageValue == null) {
  26. cache.expire(key, 60 * 5);
  27. }
  28. return storageValue;
  29. } else {
  30. // 缓存非空
  31. return cacheValue;
  32. }
  33. }

注意:布隆过滤器不能删除数据,如果要删除得重新初始化数据。

缓存失效(击穿)

缓存同时批量,大量的同时失效导致,可能会造成宕机引发雪崩

解决方案:

  1. 设置的一个随机的超时时间,默认时间+ 一个随机数

    1. String get(String key) {
    2. // 从缓存中获取数据
    3. String cacheValue = cache.get(key);
    4. // 缓存为空
    5. if (StringUtils.isBlank(cacheValue)) {
    6. // 从存储中获取
    7. String storageValue = storage.get(key);
    8. cache.set(key, storageValue);
    9. //在一个基准时间上设置一个时间范围
    10. int expireTime = new Random().nextInt(300) + 300;
    11. if (storageValue == null) {
    12. cache.expire(key, expireTime);
    13. }
    14. return storageValue;
    15. } else {
    16. // 缓存非空
    17. return cacheValue;
    18. }
    19. }

    缓存雪崩

    由一个redis挂了,大量请求到DB,,导致DB挂了,进而影响服务挂了,

    解决方案:

  2. 保证高可用,集群,主从,哨兵

  3. 消费降级,控流
  4. 提前演练

热点缓存key重建优化

  1. 热点事件导致冷数据变成热点
  2. 重建缓存时,需要时间计算

解决方案
在实际获取DB数据的地方加上分布式锁

  1. String get(String key) {
  2. // 从Redis中获取数据
  3. String value = redis.get(key);
  4. // 如果value为空, 则开始重构缓存
  5. if (value == null) {
  6. // 只允许一个线程重建缓存, 使用nx, 并设置过期时间ex
  7. String mutexKey = "mutext:key:" + key;
  8. if (redis.set(mutexKey, "1", "ex 180", "nx")) {
  9. // 从数据源获取数据
  10. value = db.get(key);
  11. // 回写Redis, 并设置过期时间
  12. redis.setex(key, timeout, value);
  13. // 删除key_mutex
  14. redis.delete(mutexKey);
  15. }// 其他线程休息50毫秒后重试
  16. else {
  17. //这个设计很巧妙,值得借鉴
  18. Thread.sleep(50);
  19. get(key);
  20. }
  21. }
  22. return value;
  23. }

缓存数据库双写不一致

  1. 双写不一致:
    image.png

  2. 读写不一致

image.png

延迟双删
就是删除之后,一段时间后再删一次,没必要

解决方案:

  1. **无法容忍的不一致数据,库存:分布式锁读写锁
  2. cannal监听binlog日志,同步缓存
    增加了系统复杂度,不太建议
    image.png
  3. 个人维度的信息,不太重要的不一致数据,加过期时间,每隔一段时间主动更新即可
  4. 并发高,但不太敏感的数据不一致,名称,分类,可以加时间,定期更新即可

读多写少:缓存效果好
写多读多: 不建议缓存
不要为了绝对的一致性过度设计和控制,增加系统负责性


开发规范

一:键值设计

1:key名称设计:

  1. 可读性,可管理性
  2. 简洁性,不要太长
  3. 不要包含特殊字符

2: value设计

  1. 拒绝bigkey,网络问题,慢查询
    redis字符串最大可以512M,实际上不要这么干
    1:字符串类型:超过10k就算大了
    2:集合类型,元素不要超过5000个
  2. 非字符串bigkey ,不要用del
    使用hscan, sscan,zscan 渐进式删除,
    防范bigkey过期自动删除的问题,会引起del,造成阻塞

bigkey危害
1:redis阻塞
2:网络拥塞,
3:过期删除
redis4.0要设置过期异步删除否则会阻塞
lazyfree-lazy-expire yes
bigkey的产生
设计不合理导致的
1:社交类,粉丝列表
2:统计类,
3:缓存类,直接序列化放进缓存
优化bigkey

  1. 拆分
    list ,hash
  2. 如果不可拆分
    避免每次取出全部,尽量hmget 而不是 hgetall
  3. 合理设计数据类型
    比如实体可以放在一个hash中,而不是对每个属性设计一个字符串缓存
  4. 控制key的生命周期
    避免缓存同时失效,

二:命令使用规范

  1. 【推荐】 O(N)命令关注N的数量
    hgetall、lrange、smembers、zrange、sinter等并非不能使用,但是需要明确N的值。有遍历的需求可以使用hscan、sscan、zscan代替。
  2. 禁用命令
    禁止线上使用keys、flushall、flushdb等,通过redis的rename机制禁掉命令,或者使用scan的方式渐进式处理。
  3. 【推荐】使用批量操作提高效率
    原生命令:例如mget、mset。 非原生命令:可以使用pipeline提高效率。
  4. 不要过多使用事务,可以使用luna脚本

    三: 客户端使用规范

  5. 避免多个应用使用一个Redis实例

  6. 使用带有连接池的数据库

    连接池设计

    | 序号 | 参数名 | 含义 | 默认值 | 使用建议 | | —- | —- | —- | —- | —- | | 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 | 建议开启,但应用本身也要开启 |

参数:

  1. maxTotal : 最大连接数
    nodes(应用个数)* maxTotal 不能超过maxClients

  2. **maxIdle和minIdle
    最大连接数:给出余量,maxIdle,不要过小,否则会new jedis
    最佳 maxTotal = maxIdle 避免连接池伸缩带来的干扰
    minIdle:最小空闲连接数,至少需要保持的空闲连接数

**

线程池的预热:
让连接池的空闲链接快速提升到minIdle

  1. List<Jedis> minIdleJedisList = new ArrayList<Jedis>(jedisPoolConfig.getMinIdle());
  2. for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
  3. Jedis jedis = null;
  4. try {
  5. jedis = pool.getResource();
  6. minIdleJedisList.add(jedis);
  7. jedis.ping();
  8. } catch (Exception e) {
  9. logger.error(e.getMessage(), e);
  10. } finally {
  11. //注意,这里不能马上close将连接还回连接池,否则最后连接池里只会建立1个连接。。
  12. //jedis.close();
  13. }
  14. }
  15. //统一将预热的连接还回连接池
  16. for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
  17. Jedis jedis = null;
  18. try {
  19. jedis = minIdleJedisList.get(i);
  20. //将连接归还回连接池
  21. jedis.close();
  22. } catch (Exception e) {
  23. logger.error(e.getMessage(), e);
  24. } finally {
  25. }
  26. }

高并发下建议添加熔断功能

合适使用密码

Redis过期策略

  1. 被动删除:当读写一个已经过期的key时,才去删掉
  2. 主动删除:定期主动删除已过期的key,只是一批一批的删,不是同时
  3. 当已用内存超过maxMemory,会触发主动策略

淘汰策略
主动清理策略在Redis 4.0 之前一共实现了 6 种内存淘汰策略,在 4.0 之后,又增加了 2 种策略,总共8种:
a) 针对设置了过期时间的key做处理:

  1. volatile-ttl:在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期的越先被删除。
  2. volatile-random:就像它的名称一样,在设置了过期时间的键值对中,进行随机删除。
  3. volatile-lru:会使用 LRU 算法筛选设置了过期时间的键值对删除。
  4. volatile-lfu:会使用 LFU 算法筛选设置了过期时间的键值对删除。

b) 针对所有的key做处理:

  1. allkeys-random:从所有键值对中随机选择并删除数据。
  2. allkeys-lru:使用 LRU 算法在所有数据中进行筛选删除。
  3. allkeys-lfu:使用 LFU 算法在所有数据中进行筛选删除。

c) 不处理:

  1. 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”同步到从结点删除数据