Redis 理论部分
redis 速度快的原因
1、纯内存操作
2、单线程操作,避免了频繁的上下文切换和资源争用问题,多线程需要占用更多的 CPU 资源
3、采用了非阻塞 I/O 多路复用机制
4、提供了非常高效的数据结构,例如双向链表、压缩页表和跳跃表等,可以根据实际数据类型选择合理的数据编码
Redis 是基于内存的操作,CPU 一般不会是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存的大小或者网络宽带。既然单线程容易实现,而且 CPU 不会成为瓶颈,那么采用单线程的方案。
注意:本质上 Redis 并不是单纯的单线程服务模型,一些辅助工作比如持久化刷盘、惰性删除等任务是由 BIO线程来完成的,这里说的单线程主要是说与客户端交互完成命令请求和回复的工作线程。重点:执行命令的核心模块是单线程的。新的命令并不会立即被执行,而是统一的放到了队列中,一条一条的执行。
单线程还有一个问题:就是对于每个命令的执行时间是有要求的,如果其中的某一个命令执行过长,会造成其他命令的阻塞,这对于 Redis 这种高性能的服务来说是致命的,记住 Redis 是面向快速执行场景的数据库
充当缓存的 Redis 和 Memcached
1、存储方式上:Memcache 会把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。Redis 有部分数据存在硬盘上,这样能保证数据的持久性。
2、数据支持类型上:Memcache 对数据类型的支持简单,只支持简单 key-value,而 Redis 最基本都要支持五种数据类型。
3、使用底层模型不同:它们之间底层实现方式以及与客户端之间通信的应用协议不一样。Redis 直接自己构建了 VM 机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
4、值大小:Redis 可以达到 1GB,而 Memcache 只有 1MB。
Redis 缓存使用场景
1、降低后端负载
2、加速请求响应
3、大量写合并为批量写
Redis 缓存策略
1、LRU、LFU、FIFO
2、超时剔除
3、主动更新
内存淘汰策略:
1、volatile-lru 从已设置过期时间的数据集中挑选最近最少使⽤的数据淘汰
2、volatile-random 从已设置过期时间的数据集中任意选择数据淘汰
3、allkeys-lru 当内存不⾜以容纳新写⼊数据时,在键空间中,移除最近最少使⽤的 key,常用
4、allkeys-random 从数据集中任意选择数据淘汰
5、volatile-ttl 从已设置过期时间的数据集中挑选将要过期的数据淘汰
6、no-eviction 禁止驱逐数据,也就是说当内存不⾜以容纳新写⼊数据时,新写⼊操作会报错 OOM。
4.0 版本后增加两种:
7、volatile-lfu 从已设置过期时间的数据集中挑选最不经常使⽤的数据淘汰
8、allkeys-lfu 当内存不⾜以容纳新写⼊数据时,在键空间中,移除最不经常使⽤的 key。
常见选择:
-
allkeys-lru 用于应用对缓存的访问符合幂律分布,也就是存在相对热点数据,或者不太清楚应用的缓存访问分布状况,可以选择 allkeys-lru 策略。
-
allkeys-random 应用对于缓存 key 的访问概率相等,则可以使用这个策略。
-
volatile-ttl 策略使得可以向 Redis 提示哪些 key 更适合被移除
Redis 删除策略:
redis 开辟了一个空间用来存放值的地址和其过期时间,删除策略是为了在内存和 cpu 之间找到一个平衡,过期数据通常是在 cpu 闲暇之余被删除的。
Redis 中的过期数据删除情况:redis 服务器当中有很多的操作需要被执行,执行会导致 CPU 的工作大大的增加,当内存的空间还足够时,已被删除的数据的内存空间并未直接释放,而是对客户端的指令先执行,redis 中的数据删除策略包括定时删除、惰性删除、定期删除。
内存占用 | CPU 占用 | 特征 | |
---|---|---|---|
定时删除 | 节约内存,无占用 | 不分时段占用 CPU 资源,频度高 | 时间换空间,适用于小内存,强 CPU |
惰性删除 | 内存占用严重 | 延时执行,CPU 利用率高 | 空间换时间,适用于大内存,弱 CPU |
定期删除 | 内存定期随机清理 | 每秒花费固定的 CPU 资源维护内存 | 随机抽查,重点抽查 |
定时删除是对 CPU 和内存消耗取得一个折中方案,通过每隔一段时间执行一次删除过期 key 的操作,并且通过限制删除操作执行的时长和频率来减少删除操作对 CPU 造成的影响;周期性轮询 redis 库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度。一般在 redis 应用中会使用惰性删除和定期删除两种方式
Redis 缓存问题:
1、缓存穿透:大量请求缓存中数据库并不存在的数据。
解决方案:1、布隆过滤器。2、缓存空对象
2、缓存击穿:大量请求缓存中同时访问一个过期数据。
解决方案:1、设置 key 永不过期和随机时间失效。2、互斥锁使访问有序
3、缓存雪崩:大量请求缓存中大面积失效的缓存数据。
解决方案:
1、缓存数据设置随机的过期时间,防止同一时间大量数据集合失效。
2、集群,将数据分布在不同的缓存数据库中。
3、限流,通过加锁或队列来控制读数据库写缓存的线程数量
redis 编程客户端
1、Jedis 是 Redis 的 Java 实现客户端,提供了比较全面的 Redis 命令的支持。
-
优点:提供了比较全面的 Redis 操作特性的 API;API 基本与 Redis 的指令一一对应,使用简单易理解。
-
缺点:同步阻塞 IO、不支持异步、线程不安全
2、Lettuce 支持同步、异步通信的方式 API 调用,也支持响应式编程 API,包括发布/订阅消息、高可用性服务部署架构。Lettuce 高级 Redis 客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。
-
优点:线程安全;基于 Netty 框架的事件驱动的通信,可异步调用;适用于分布式缓存
-
缺点:API 更抽象,学习使用成本高
使用 Jedis 和 lettuce 总结
1、调大连接池大小能够提高 jedis 的吞吐量,但是不能避免出现超时错误和长时间等待。jedis 连接方式最大连接数和最小、最大空闲连接数设置为一样有利于减少上下文切换时间,提升效率。
2、lettuce 调大连接池大小反而会影响性能,最佳个数=CPU 核数+1,lettuce 整体稳定性和性能优于 jedis 方式。
常见 value 的数据类型
典型工具类编程
@Slf4j
@Component
public class RedisUtil {
@Autowired
private RedisTemplate redisTemplate;
public boolean set(final String key, Object value){ //写入缓存
boolean res=false;
try{
this.set(key,value,new Random().nextInt(60)+10); 10 到 70 秒的随机数
res=true;
} catch (Exception e){
log.debug("问题:"+e.getMessage());
}
return res;
}
public boolean set(String key,Object value,int timeout){ 写入缓存设置时效时间
boolean res=false;
try{
redisTemplate.opsForValue().set(key,value);
redisTemplate.expire(key, Duration.ofSeconds(timeout));
res=true;
} catch (Exception e){
log.debug("问题:"+e.getMessage());
}
return res;
}
public Object get(String key){ //读取缓存
Object res=null;
try {
res = redisTemplate.opsForValue().get(key);
} catch (Exception e){
log.debug("问题:"+e.getMessage()); }
return res;
}
public void remove(String key){ //删除 key,也删除对应的 value
if(exists(key)) redisTemplate.delete(key);
}
public boolean exists(String key){// 判断缓存中是否有对应的 value
return redisTemplate.hasKey(key); }
public long getExpire(String key) { //根据 key 获取过期时间,单位为秒
return redisTemplate.getExpire(key, TimeUnit.SECONDS); }
public boolean expire(String key, long time) { //指定缓存失效时间
try {
if (time > 0) redisTemplate.expire(key, time, TimeUnit.SECONDS);
return true;
} catch (Exception e) {
log.debug("问题:"+e.getMessage());
return false; }
}
public Set<String> keys(String keyPattern){ 获取指定对应 pattern 模板的所有 key
return redisTemplate.keys(keyPattern); }
}
Redis 序列化器
针对数据的序列化/反序列化提供了多种可选择策略 RedisSerializer
1、JdkSerializationRedisSerializer 用于 POJO 对象的存取场景,使用 JDK 本身序列化机制,将 pojo 类通过ObjectInputStream/ObjectOutputStream 进行序列化操作,最终 redis-server 中将存储字节序列。是目前最常用的序列化策略。
2、StringRedisSerializer用于Key和value为字符串的场景,根据指定的charset对数据的字节序列编码成string,是 new String(bytes, charset)和 string.getBytes(charset)的直接封装,是最轻量级和高效的策略。
3、JacksonJsonRedisSerializer 是 jackson-json 工具提供的 javabean 与 json 之间的转换能力,可以将 pojo 实例序列化成 json 格式存储在 redis 中,也可以将 json 格式的数据转换成 pojo 实例。因为 jackson 工具在序列
化和反序列化时,需要明确指定 Class 类型,因此此策略封装起来稍微复杂。
缓存与数据库双写不一致
缓存可以提升性能、缓解数据库压力,但是使用缓存也会导致数据不一致性的问题。一般写数据操作使用缓存有三种经典的缓存模式 Cache-Aside Pattern、Read-Through/Write through 和 Write behind,这三种常见的更
新策略实际上都是保证数据的最终一致性的方法,可以总结为
1、先更新数据库再更新缓存,后续线程会读取旧数据
2、先删除缓存再新数据库,并发线程读取旧数据并写到缓存
3、先更新数据库再删除缓存,后续线程会读取旧数据
常见解决方案是延时双删,但是延时的时长不好控制
总结:
1、并发几率很小的数据,几乎不用考虑,加上缓存过期时间,缓存失效后查询主动更新
2、业务上是否能容忍一定时间的不一致,如能容忍的话,加上缓存过期时间;如果不能容忍缓存数据不一致,可以通过加分布式读写锁保证并发读写或写写的时候按顺序排好队,读读的时候相当于无锁。
3、可以用阿里开源的 canal 通过监听数据库的 binlog 日志及时的去修改缓存,但是引入了新的中间件,增加了系统的复杂度。
Cache-Aside Pattern 即旁路缓存模式,读操作:读的时候,先读缓存,缓存命中的话,直接返回数据;缓存没有命中的话,就去读数据库,从数据库取出数据,放入缓存后,同时返回响应。写操作:更新的时候,先更新
数据库,然后再删除缓存。
Read-Through/Write-Through 读写穿透模式中,读操作:从缓存读取数据,读到直接返回;如果读取不到的话,从数据库加载,写入缓存后,再返回响应。写操作:Write-Through 模式下,当发生写请求时,也是由缓
存抽象层完成数据源和缓存数据的更新Write behind 异步缓存,一般写入是同步更新缓存和数据,Write Behind 则是只更新缓存,不直接更新数据库,
通过批量异步的方式来更新数据库。适合频繁写的场景,MySQL 的 InnoDB Buffer Pool 机制就使用到这种模式。