redis

" Redis 是一个开源(BSD 许可)的、内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如字符串(strings),散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询,bitmaps, hyperloglogs 和地理空间(geospatial)索引半径查询。 Redis 内置了 复制(replication),LUA 脚本(Lua scripting),LRU 驱动事件(LRU eviction),事务(transactions) 和不同级别 磁盘持久化(persistence), 并通过 [Redis 哨兵(Sentinel)和自动 [分区(Cluster)提供高可用性。"

redis 查看所有sentinel redis查看内容_redis 查看所有sentinel


使用 Redis 有哪些好处?

(1)速度快,因为数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是 O1)

(2)支持丰富数据类型,支持 string,list,set,Zset,hash 等

(3)支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行

(4)丰富的特性:可用于缓存,消息,按 key 设置过期时间,过期后将会自动删除

Redis 是单进程单线程的?
  • Redis 是单进程单线程的,redis 利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销

redis 支持哪些数据类型,以及每种数据类型的使用场景?

  • String,redis 中的 String 类型和其他很多编程语言中的语义类似,value 可以是 String 也可以是数字。 一般做一些复杂的计数功能的缓存,比如说微博数,粉丝数等
  • Hash,也可以称为 hashes, 是一个 string 类型的 key 和 value 的映射表,特别适合用于存储对象。比如我们可以 hash 数据结构来存储用户信息,商品信息等等。内部可以用 hashtable 和 ziplist 两种承载方式来实现
  • List,list 是 redis 最重要的数据结构之一,可以做简单的消息队列的功能,比如论坛点赞人列表、微博粉丝列表等;另外还可以利用 lrange 命令,可以从某个元素开始读取多少个元素,实现简单的高性能分页,类似微博那种下拉不断分页的东西,性能极佳,用户体验好
  • Set,set 类似 List,但是它是一个无序集合,且其中的元素不重复。可以做全局去重的功能,比如说是否给帖子点赞数;也可以判断某个元素是否在 set,比如说判断是否给某个回复点赞。另外还可以利用交集、并集、差集等操作来支撑更多的业务场景,比如说找出两个微博 ID 的共同好友等
  • Sorted Set,sorted set 相比 set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,比如说可以用于取排行榜 top N 的用户

说一说 redis 的过期策略以及内存淘汰机制 ?

(1)redis 的过期策略

  • 定期删除策略。用一个定时器来负责检查 key,过期则删除 key,注意这里并不是检查所有的 key 而是随机抽取进行检查。定期策略虽然让内存及时释放,但也会额外消耗 CPU 资源,通常 CPU 应该将时间尽量用于处理业务请求,而不是删除 key
  • 惰性删除策略。在你获取某个 key 的时候,redis 会检查一下,这个 key 如果设置了过期时间那么是否过期了,如果过期则删除该 key

(2) 内存淘汰机制

  • 如果定期删除没删除 key,然后也没及时去请求 key,即惰性删除也没生效,持续下去 redis 的内存会越来越高,当超过 redis 设置的内存最大使用量时,就会进行内存数据淘汰。redis 有 6 种淘汰策略:

策略


volatile-lru

从已设置过期时间的数据集中挑选最近最少使用的数据淘汰

volatile-ttl

从已设置过期时间的数据集中挑选将要过期的数据淘汰

volatile-random

从已设置过期时间的数据集中任意选择数据淘汰

allkeys-lru

从所有数据集中挑选最近最少使用的数据淘汰

allkeys-random

从所有数据集中任意选择数据进行淘汰

noeviction

当内存不足以容纳新写入数据时,新写入操作会报错。很少使用

  • 注意这里的 6 种机制,前缀 volatile 和 allkeys 用于区分淘汰数据的数据集是从已设置过期时间的数据集还是从全部数据集中选取,后面的 lru、ttl 以及 random 是三种不同的淘汰策略,再加上一种 no-enviction 永不回收的策略。其中最常使用的是 volatile-lru / allkeys-lru

Redis 常见性能问题和解决方案有哪些?

(1)Master 最好不要写内存快照,如果 Master 写内存快照,save 命令调度 rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务

(2)如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次

(3)为了主从复制的速度和连接的稳定性,Master 和 Slave 最好在同一个局域网

(4)尽量避免在压力很大的主库上增加从库

为什么redis 需要把所有数据放到内存中?
  • Redis 为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。所以 redis 具有快速和数据持久化的特征。如果不将数据放在内存中,磁盘 I/O 速度为严重影响 redis 的性能。在内存越来越便宜的今天,redis 将会越来越受欢迎。如果设置了最大使用的内存,则数据已有记录数达到内存限值后不能继续插入新值
Redis 支持的 Java 客户端都有哪些?官方推荐用哪个?Jedis 与 Redisson 对比有什么优缺点?
  • Redisson、Jedis、lettuce 等等,官方推荐使用 Redisson。
  • Jedis 是 Redis 的 Java 实现的客户端,其 API 提供了比较全面的 Redis 命令的支持;Redisson 实现了分布式和可扩展的 Java 数据结构,和 Jedis 相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等 Redis 特性。Redisson 的宗旨是促进使用者对 Redis 的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上
Redis 如何做内存优化?
  • 尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的 web 系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的 key,而是应该把这个用户的所有信息存储到一张散列表里面
假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如果将它们全部找出来?
  • 使用 keys 指令可以扫出指定模式的 key 列表

如果这个 redis 正在给线上的业务提供服务,那使用 keys 指令会有什么问题?

  • (redis 关键的一个特性:redis 的单线程的)。keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan 指令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长

redis是单线程为啥那么快?

  • 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
  • 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的
  • 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
  • 使用多路I/O复用模型,非阻塞IO
  • 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求

说说 redis 的持久化方式 ?

  • redis 提供两种持久化方式:一种是 RDB(Redis DataBase),用数据集快照的方式,定时将 redis 存储的数据生成快照并存储到磁盘等介质上;另外一种是 AOF(Append -only file),是指所有的命令行记录以 redis 命令请求协议的格式完全持久化存储) 保存为 aof 文件
RDB 和 AOF 各自的优缺点是什么?
  • RDB 的优点:
  1. 特别适合备份
  2. 性能最大化,fork 子进程来完成写操作,让主进程继续处理命令且不会进行任何 IO 操作的,这样就确保了 redis 极高的性能
  3. 相对于数据集大时,比 AOF 的启动效率更高
  • RDB 的缺点:
    数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发 生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候
  • AOF 的优点:
    1. 数据安全,aof 持久化可以配置 append fsync 属性,比如无 fsync,每秒钟一次 fsync,或者每次执行写入命令时 fsync,一般只会丢失一秒钟的数据,或者最后一次执行的数据,对缓存来说,这已经足够
    2. 某些场景下还可以恢复数据。比如说某同学在操作 redis 时,不小心执行了 FLUSHALL,导致 redis 内存中的数据全部被清空了。如果 AOF 文件还没有被重写(rewrite),我们就可以用最快的速度暂停 redis 并编辑 AOF 文件,将最后一行的 FLUSHALL 命令删除,然后重启 redis,就可以恢复 redis 的所有数据到 FLUSHALL 之前的状态了
  • AOF 的缺点:
  • AOF 文件比 RDB 文件大,且根据不同的 fsync 策略,其恢复速度可能较慢
  • 数据集大的时候,比 rdb 启动效率低
  • RDB 和 AOF 对比

命令

RDB

AOF

启动优先级



体积



恢复速度



数据安全性

丢数据

根据策略决定

轻重



AOF 文件太大会怎么样?
  • AOF 文件过大时,后台会自动地对 AOF 进行重写(rewrite),重写时会压缩 AOF 文件的内容,只保留可以恢复数据的最小指令集。比如说,假如我们调用了 100 次 INCR 指令,在 AOF 文件中就要存储 100 条指令,但这明显是很低效的,完全可以把这 100 条指令合并成一条 SET 指令
  • 在进行 AOF 重写时,仍然是采用先写临时文件,全部完成后再替换的流程,所以断电、磁盘满等问题都不会影响 AOF 文件的可用性

(注:以上为基础内容,详细内容应查阅和学习相关书籍)

缓存扩展:

什么是缓存雪崩?一般怎么解决?

  • 缓存雪崩,即缓存同一时间大面积的失效(比如说大量的 key 设置了相同的过期时间,导致在缓存在同一时刻全部失效),造成瞬时 DB 请求量过大、压力骤增。
  • 通常的解决方案:
    1. 将缓存过期时间分散开,比如说设置过期时间时再加上一个较小的随机值时间,使得每个 key 的过期时间,不会集中在同一时刻失效
    2. 采用限流算法,限制请求流量,业务有损
    3. 加锁访问,但是吞吐量会明显下降
  • 缓存雪崩的原因通常有两种:一种是恶意攻击,这种恶意攻击正好碰到缓存失效,这种情景下方案 2 的限流算法效果更好,还可以在网络接入层、应用层进行拦截;二是特定业务批量插入缓存,这种情况方案 1 的随机过期时间方案更合适

什么是缓存穿透?一般怎么解决?

  • 缓存穿透,是指访问一个不存在的 key,此时请求会穿透到 DB,如果我们查询的某个数据在缓存中一直不存在,那么每次查询都会穿透到 DB,流量大时 DB 压力很大甚至会挂掉。
  • 通常的解决方案:
    1. 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,在查询的时候先去布隆过滤器去查询 key 是否存在,不存在的 key 就直接返回
    2. 查询不存在的 key 时 DB 返回结果为空,同时可以将该 key 在缓存中对应一个特殊的值,这样当下次再查询这个特殊的 key 时,就不再回查 DB;
    3. 多级缓存。L1 为原始缓存(过期时间设置为短期),L2 为二级缓存(过期时间设置为长期)。L1 失效时可以继续访问 L2,避免同时失效。不过多了一级缓存,存储成本更高,而且多个缓存之间的数据一致性问题挑战也大
  • 缓存穿透的原因通常有两种:一种是恶意攻击,可以用上述两种方案进行应对;另外一种是网站并发访问高,一个缓存失败可能出现多个进程同时查询 DB,通常情况下,这并不会有什么问题,但如果并发量很大,也可能造成回查 DB 压力过大。这种方式可以对缓存查询加锁,如果 KEY 不存在就加锁再回查 DB,然后将结果放入缓存,再解锁;其他进程发现有锁就等待,然后等解锁后返回数据或者回源查询

缓存与数据库双写不一致解决方案?

  • 方案一:先更新缓存,再更新数据库
    不推荐。先更新缓存若更新数据库失败,还需再更新缓存。
  • 方案二:先更新数据库,再更新缓存
    不推荐。同时有请求A和请求B进行更新操作,请求A与B在不同线程,可能会出现:
    请求A更新了数据库
    请求B更新了数据库
    请求B更新了缓存
    请求A更新了缓存
    这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑。
  • 方案三:先删除缓存,再更新数据库
    有点问题。有一个请求A进行更新操作,另一个请求B进行查询操作,可能会出现:
(1)、单个数据库
  	请求A进行写操作,删除缓存
  	请求B查询发现缓存不存在
  	请求B去数据库查询得到旧值
  	请求B将旧值写入缓存
  	请求A将新值写入数据库
  
  (2)、读写分离架构
  	请求A进行写操作,删除缓存
  	请求A将数据写入数据库了,
  	请求B查询缓存发现,缓存没有值
  	请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
  	请求B将旧值写入缓存
  	数据库完成主从同步,从库变为新值

上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。
解决方案:延时双删策略

public void write(String key,Object data){
      redis.delKey(key);
      db.updateData(data);
      Thread.sleep(1000);
      redis.delKey(key);
  }

先淘汰缓存
再写数据库(这两步和原来一样)
休眠1秒
再次淘汰缓存
自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

对于MySQL读写分离架构,只是睡眠时间修改为在主从同步的延时时间基础上,加几百ms。

  • 方案四:先更新数据库,再删除缓存
    极端情况有问题。有一个请求A进行更新操作,另一个请求B进行查询操作,可能会出现:
请求A查询数据库得到一个旧值
  请求B将新值写入数据库
  请求B删除缓存
  请求A将查到的旧值写入缓存

步骤2的写数据库操作比步骤1的读数据库操作耗时更短,才有可能使得步骤3先于步骤4。可是,大家想想,数据库的读操作的速度远快于写操作的,因此步骤2耗时比步骤1更短,这一情形很难出现。

解决方案:延时双删策略

public void write(String key,Object data){
      db.updateData(data);
      redis.delKey(key);
      Thread.sleep(1000);
      redis.delKey(key);
  }

先写数据库
再淘汰缓存
休眠1秒
再次淘汰缓存


  • 方案三与方案四还存在问题
    同步双删导致并发降低
    比如一个写数据请求,然后写入数据库了,删缓存失败了,这会就出现不一致的情况。
    问题1解决方案:异步
    问题二解决方案:提供一个保障的重试机制

方案一:消息队列方式

更新数据库数据
缓存因为种种问题删除失败
将需要删除的key发送至消息队列
自己消费消息,获得需要删除的key
继续重试删除操作,直到成功
业务线代码侵入较大。

方案二:订阅 binlong方式

更新数据库数据
数据库会将操作信息写入binlog日志当中
订阅程序提取出所需要的数据以及key
另起一段非业务代码,获得该信息
尝试删除缓存操作,发现删除失败
将这些信息发送至消息队列
重新从消息队列中获得该数据,重试操作。
订阅binlog程序在MySQL中有阿里开源的中间件叫canal。

如果对一致性要求不是很高,直接在程序中另起一个线程,每隔一段时间去重试也可。

总结:

  • 根据数据实时性要求,以及系统并发量考虑。
  • 实时性不强,则可以选择设定缓存过期时间,先删缓存再更新数据库或先更新数据库再删缓存方案都可行。
  • 实时性较强的,又有大并发量可以考虑延迟双删策略。
  • 至于其他如请求串行化,放入同一个队列中依次执行的,复杂没必要