Redis缓存技术分享
大纲:
- 什么是缓存(优点和缺点)
- 为什么需要缓存(应用场景,解决的问题)
- 有哪些缓存的方案
- 值得注意的问题
- 缓存工具包(Jedis、RedisTemplate)
什么是缓存(缓存服务器)
缓存是一种用空间换时间的方案,以服务器**内存**中的数据暂时代替从数据库读取最新的数据
优点:
- 数据保存在内存,存取速度快,并发能力强
- 减少数据库IO,减轻服务器压力,减少网络延迟,加快页面打开速度
缺点:
- 牺牲数据的实时性
- 单点存储资源有限(内存资源)
Redis(2013) 与 Memcached(2010) 的区别:
- Memcached仅支持 k/v 类型的数据,而 Redis还提供 list,set,hash等数据结构的存储;
- Redis支持主从同步数据备份、负载均衡;
- 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;
}
}
- 缓存预热,提前将热点数据加载到缓存,如:定时任务
缓存雪崩
- 缓存服务器挂了;
- 缓存集中失效。(大面积的缓存击穿)
参考方案:
- 双缓存:原始缓存失效时,访问备用缓存,备用缓存失效时间可以设置长些
缓存的淘汰机制
在内存达到设置的最大上限(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老师:可能是偶尔的网络异常,现在线上有万分之一左右的超时或者网络错误。
参考资料:
- ⭐️ Redis、Jedis、RedisTemplate之间的关系 https://blog.csdn.net/CSDN2497242041/article/details/102675435
- Redis如何更新缓存 https://www.php.cn/redis/436675.html
- Using Read-Through and Write-Through in a Distributed Cache https://dzone.com/articles/using-read-through-amp-write-through-in-distribute-2
- SpringBoot整合Redis与Cache与实现(cache aside) https://www.jianshu.com/p/5e78ce36e7aa
- 缓存淘汰机制(LRU/LFU) https://www.jianshu.com/p/5f2ccc2ef256
- ⭐️ 深入理解Redis数据缓存的LRU实现机制 https://blog.csdn.net/azurelaker/article/details/85045245
- ⭐️ Redis的缓存淘汰策略LRU与LFU https://www.jianshu.com/p/c8aeb3eee6bc