记一次Redis缓存技术分享

Redis缓存技术分享

大纲:

  • 什么是缓存(优点和缺点)
  • 为什么需要缓存(应用场景,解决的问题)
  • 有哪些缓存的方案
  • 值得注意的问题
  • 缓存工具包(Jedis、RedisTemplate)

什么是缓存(缓存服务器)

缓存是一种用空间换时间的方案,以服务器**内存**中的数据暂时代替从数据库读取最新的数据

优点:

  1. 数据保存在内存,存取速度快,并发能力强
  2. 减少数据库IO,减轻服务器压力,减少网络延迟,加快页面打开速度

缺点:

  1. 牺牲数据的实时性
  2. 单点存储资源有限(内存资源)

Redis(2013) 与 Memcached(2010) 的区别:

  1. Memcached仅支持 k/v 类型的数据,而 Redis还提供 list,set,hash等数据结构的存储;
  2. Redis支持主从同步数据备份、负载均衡;
  3. Redis支持数据的持久化,定期会将数据保存到磁盘,但Memcached如果挂掉数据将丢失。

为什么需要缓存

高性能

非实时变化的热点数据:Redis查询数据速度很快,仅需1~2ms,性能极高。

高并发

Redis缓存位于服务器的内存,内存对高并发有良好的支持,支持1s 几十万的并发(redis的瓶颈在网络),远高于MySql。

常见缓存方案

Cache Aside (被动缓存)

  • 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
  • 更新的时候,先更新数据库,然后再删除或更新缓存。
  • 优点:逻辑简单、容易开发

在这里插入图片描述

Read/Write Through (主动缓存)

  • 代码只操作缓存,由缓存负责操作数据库;
  • 查询时,命中则返回,没命中,从数据库中拉出数据存在缓存中;
  • 更新时,命中则更新缓存,缓存自动**同步**更新数据库,没命中,直接更新数据库。

在这里插入图片描述

Write Back

  • 数据更新时,只更新缓存,缓存异步批量更新数据库

在这里插入图片描述

缓存的一些问题

过期时间

更新缓存时,会覆盖原先的过期时间。

缓存穿透

查询不存在的数据。(恶意攻击、系统bug)

参考方案:

  • 过滤非法请求(参数不符合要求的,如 id 为负数)
  • 将空数据放到缓存中,过期时间可以相应短些。
缓存击穿

查询 key 过期的热点数据。(类似缓存穿透)

参考方案:

  • 该 key 不设置过期时间;
  • 互斥锁 SETNX(SET if Not eXist),当 key 不存在时,阻塞其他线程针对该 key 的查询,直到本次查询将内容添加至缓存。保证每次只有一个线程去查询数据库。可以设置过期时间,防止慢查询导致阻塞时间过长影响性能。
public String get(key) {
    String value = redis.get(key);
    if (value == null) { 
        if (redis.setnx("lock_key", "lock_vlue", 3 * 60)) {  
            //锁需要设置超时,防止删除锁操作失败时不能读数据库
            value = database.get(key);
            redis.set(key, value, expire_time);
            redis.delete("lock_key");
        } else { 
            sleep(50);
            get(key);  //重试
        }
    } else {
      return value;      
    }
}
  • 缓存预热,提前将热点数据加载到缓存,如:定时任务
缓存雪崩
  1. 缓存服务器挂了;
  2. 缓存集中失效。(大面积的缓存击穿)

参考方案:

  • 双缓存:原始缓存失效时,访问备用缓存,备用缓存失效时间可以设置长些
缓存的淘汰机制

在内存达到设置的最大上限(maxmemory)时启动。

Redis淘汰策略:

  • noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键
  • allkeys-lru:首先通过LRU算法驱逐最久没有使用的键
  • volatile-lru:首先从设置了过期时间的键集合中驱逐最久没有使用的键
  • allkeys-lfu:从所有键中驱逐使用频率最少的键
  • volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
  • allkeys-random:从所有key随机删除
  • volatile-random:加入键的时候如果过限,从过期键的集合中随机驱逐
  • volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键

LRU(Least Recently Used,最少近期使用)

淘汰长时间不使用的key

根据 key 的最近访问时间与系统时间的差值判断 key 的热度,差值越大热度越低。

访问 key 时会使 lru = server.lruclock,而 server.lruclock 定时被同步为 server.unixtime

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:REDIS_LRU_BITS; /* 对象内部24位时钟 */
    int refcount;
    void *ptr;
} robj;

若在||处启动了淘汰机制,LRU算法会将B保留,而清除A

A~~A~~A~~A~~A~~A~~A~~A~~A~~A~~~~~||
B~~~~~B~~~~~B~~~~~B~~~~~~~~~~~~~B||

LFU(Least Frequently Used,最不经常使用)

根据 key 的访问频率,频率越高,热度越高。

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:REDIS_LRU_BITS; /* 前16位依然代表时钟,后8位代表计数器 */
    int refcount;
    void *ptr;
} robj;

key命中后递增计数器,一段时间没命中后衰减计数器。

/* LFU 递增计数器函数 */
uint8_t LFULogIncr(uint8_t counter) {
    if (counter == 255) return 255;
    double r = (double)rand()/RAND_MAX;
    double baseval = counter - LFU_INIT_VAL;
    if (baseval < 0) baseval = 0;
    double p = 1.0/(baseval*server.lfu_log_factor+1);
    if (r < p) counter++;
    return counter;
}

/* LFU 衰减计数器函数 */
unsigned long LFUDecrAndReturn(robj *o) {
    unsigned long ldt = o->lru >> 8; /* 取前16位,代表时钟 */
    unsigned long counter = o->lru & 255; /* 取后8位,代表计数器 */
    unsigned long num_periods = server.lfu_decay_time ? 
      LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
    if (num_periods)
        counter = (num_periods > counter) ? 0 : counter - num_periods;
    return counter;
}

通过调整 lfu_log_factor 控制递增速度,参数越大,递增效果越小;

通过调整 lfu_decay_time 控制衰减速度,参数越大,衰减效果越小。

缓存工具包

  • SpringCache(注解方式)

  • RedisTemplate + JedisPool

  • RedisTemplate + Lettuce

RedisTemplate + JedisPool

RedisTemplate —操作—> JedisPool —操作—> Redis

  • 使用 spring-data-redis 包中的 RedisTemplate 操作 Redis

  • RedisTemplate 是对 Redis 底层开发包(Jedis、Lettuce)的高度封装,增加了缓存对象的操作,多线程安全

JedisConnectionException: Unexpected end of stream

现象:隔一段较长时间不用redis,然后使用redis会报这个错

方案:修改 redis 配置,启动空闲检测,移除空闲的连接,问题不再出现

推测:没有做空闲检测,长时间不使用 redis 时,连接被 redis 单方面断开,导致下次使用连接时报错

public class JedisPoolConfig extends GenericObjectPoolConfig {
  public JedisPoolConfig() {
    setTestWhileIdle(true); //是否空闲检测
    setMinEvictableIdleTimeMills(60000); //空闲资源移除标准,达到该值将被移除
    setTimeBetweenEvictionRunsMills(30000); //空闲检测周期
    setNumTestsPerEvictionRun(-1); //每次检测的空闲资源 (-1对所有连接做空闲检测)
  }
}
JedisConnectionException: Could not get a resource from the pool,
Cause by: java.net.SocketException: Connection reset

现象:线上每天随机出现2、3次

xx老师:可能是偶尔的网络异常,现在线上有万分之一左右的超时或者网络错误。

参考资料:

  1. ⭐️ Redis、Jedis、RedisTemplate之间的关系 https://blog.csdn.net/CSDN2497242041/article/details/102675435
  2. Redis如何更新缓存 https://www.php.cn/redis/436675.html
  3. Using Read-Through and Write-Through in a Distributed Cache https://dzone.com/articles/using-read-through-amp-write-through-in-distribute-2
  4. SpringBoot整合Redis与Cache与实现(cache aside) https://www.jianshu.com/p/5e78ce36e7aa
  5. 缓存淘汰机制(LRU/LFU) https://www.jianshu.com/p/5f2ccc2ef256
  6. ⭐️ 深入理解Redis数据缓存的LRU实现机制 https://blog.csdn.net/azurelaker/article/details/85045245
  7. ⭐️ Redis的缓存淘汰策略LRU与LFU https://www.jianshu.com/p/c8aeb3eee6bc
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值