redis 汇总

缓存集中失效

秒杀商品、微博热搜排行、或者一些活动数据,都是通过跑任务方式,将DB数据批量、集中预热到缓存中,缓存数据有着近乎相同的过期时间。当过这批数据过期时,会一起过期,此时,对这批数据的所有请求,都会出现缓存失效,从而将压力转嫁到DB,DB的请求量激增,压力变大,响应开始变慢。

解决:

我们可以从缓存的过期时间入口,将原来的固定过期时间,调整为过期时间=基础时间+随机时间,让缓存慢慢过期,避免瞬间全部过期,对DB产生过大压力。

缓存穿透

当查询缓存中不存在的数据时,缓存无法命中,就会去数据库中查询。如何有人恶意发起大量不存在的查询,会严重影响系统的性能

解决:

  1. 方案一:查存DB 时,如果数据不存在,预热一个特殊空值到缓存中。这样,后续查询都会命中缓存,但是要对特殊值,解析处理。
  2. 方案二:构造一个布隆过滤器,初始化全量数据,当接到请求时,在布隆中判断这个key是否存在,如果不存在,直接返回即可,无需再查询缓存和DB

布隆过滤器:布隆过滤器是一种数据结构,对所有可能査询的参数以hash形式存储,在控制层先进行校验,不符合则丟弃,从而避免了对底层存储系统的查询压力。就像hashMap<int,int>,将查询参数的hash值为key,结果存放1.当请求获取结果时,先计算查询条件的hash,判断map中有则继续去获取,如没有则直接返回

缓存击穿(量太大,缓存过期)

现象:缓存击穿,是某个key的查询非常高频,当这key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。

解决方案

  1. 热点数据永不过期
  2. 加互斥锁:引入一把全局锁,当缓存未命中时,先尝试获取全局锁,如果拿到锁,才有资格去查询DB,并将数据预热到缓存中。其他的读取操作在队列中等待。

缓存雪崩

缓存雪崩是指部分缓存节点不可用,进而导致整个缓存体系甚至服务系统不可用的情况。

解决方案

  1. redis高可用(集群):①停掉某些功能,贡献出更多的机器,② 异地多活
  2. 限流降级:在缓存失效后,通过使用锁或者对列来限制读数据库写缓存的线程数。
  3. 增加实时监控,及时预警。通过机器替换、各种故障自动转移策略,快速恢复缓存对外的服务能力

缓存大Key

当访问缓存时,如果key对应的value过大,读写、加载很容易超时,容易引发网络拥堵。另外缓存的字段较多时,每个字段的变更都会引发缓存数据的变更,频繁的读写,导致慢查询。如果大key过期被缓存淘汰失效,预热数据要花费较多的时间,也会导致慢查询。

所以我们在设计缓存的时候,要注意缓存的粒度,既不能过大,如果过大很容易导致网络拥堵;也不能过小,如果太小,查询频率会很高,每次请求都要查询多次。

解决方案:

  1. 设置一个阈值,当value的长度超过阈值时,对内容启动压缩,降低kv的大小
  2. 颗粒划分,将大key拆分为多个小key,独立维护,成本会降低不少
  3. 大key要设置合理的过期时间,尽量不淘汰那些大key
  4. 评估大key所占的比例,由于很多框架采用池化技术,如:Memcache,可以预先分配大对象空间。真正业务请求时,直接拿来即用。

如何检查大key?

  1. 带命令redis-cli --bigkeys该命令是redis自带,但是只能找出五种数据类型里最大的key
  2. rdb_bigkeys工具这是用go写的一款工具,分析rdb文件,找出文件中的大key,实测发现,不管是执行时间还是准确度都是很高的

缓存数据一致性

缓存是用来加速的,一般不会持久化储存。所以,一份数据通常会存在DB和缓存中,由此会带来一个问题,如何保证这两者的数据一致性。另外,缓存热点问题会引入多个副本备份,也可能会发生不一致现象。

解决方案:

  1. 先删缓存,在更新数据库,将读操作放到队列中。
  2. 当缓存更新失败后,进行重试,如果重试失败,将失败的key写入MQ消息队列,通过异步任务补偿缓存,保证数据的一致性。
  3. 设置一个较短的过期时间,通过自修复的方式,在缓存过期后,重新加载最新的数据

使用redis 做分布式锁

使用 setnx 设置锁,可以实现分布式锁,但是应用加上锁之后出异常,导致死锁;

解决方法:设置超时时间

注意:如果在设置锁和设置超时时间中间,应用宕机,导致锁无法释放;

解决方式:采用原子性的设置锁和超时时间的命令;

虽然有了超时时间,但是在高并发场景中,如果应用执行时间长,可能超过了锁有效时间,导致第一次上的锁自动失效,第二把锁被第一个线程释放,第三部锁被第二个线程释放。。。的锁失效问题;
或者,程序没有执行完,但锁已经失效;

如果是主从架构,如果Master上锁,同步到salve前时,Master宕机
那如果应用性能要求非常高,用redis集群,但是被redis单线程分布式锁限制了性能

Redis 常用的 5 种数据结构和应用场景?

  1. String:缓存、计数器、分布式锁等
  2. List:链表、队列、微博关注人时间轴列表等
    文章列表或者数据分页展示的应用。比如,我们常用的博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表,而且当文章多时,都需要分页展示,这时可以考虑使用redis的列表,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能。大大提高查询效率。
  3. Hash:用户信息、Hash表等
  4. Set:取交集 差集、去重、赞、踩、共同好友等
  5. Zset:访问量排行榜、点击量排行榜等

Redis的单线程与多线程呢?

Redis的多线程主要是处理数据的读写、协议解析执行命令还是采用单线程顺序执行

主要是因为redis的性能瓶颈在于网络IO而非CPU,使用多线程进行一些周边预处理,提升了IO的读写效率,从而提高了整体的吞吐量

过期键Key的删除策略有哪些?

有3种过期删除策略。惰性删除、定期删除、定时删除

  1. 惰性删除。使用key时才进行检查,如果已经过期,则删除。缺点:过期的key如果没有被访问到,一直无法删除,一直占用内存,造成空间浪费。
  2. 定期删除。每隔一段时间做一次检查,删除过期的key,每次只是随机取一些key去检查。
  3. 定时删除。为每个key设置过期时间,同时创建一个定时器。一旦到期,立即执行删除。缺点:如果过期键比较多时,占用CPU较多,对服务的性能有很大影响。

如果Redis的内存空间不足,淘汰机制?

  1. volatile-lru:从已设置过期时间的key中,移出最近最少使用的key进行淘汰
  2. ** allkeys-lru**:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
  3. volatile-ttl:从已设置过期时间的key中,移出将要过期的key
  4. volatile-random:从已设置过期时间的key中,随机选择key淘汰
  5. allkeys-random:从key中随机选择key进行淘汰
  6. no-eviction:禁止淘汰数据。当内存达到阈值的时候,新写入操作报错
  7. volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰(LFU(Least Frequently Used)算法,也就是最频繁被访问的数据将来最有可能被访问到)
  8. allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key。

Redis 持久化有哪些方式?

  1. 快照RDB。将某个时间点上的数据库状态保存到RDB文件中,RDB文件是一个压缩的二进制文件,保存在磁盘上。当Redis崩溃时,可用于恢复数据。通过SAVE或BGSAVE来生成RDB文件。(会丢失最后一次快照到宕机前的数据)

    1. SAVE:会阻塞redis进程,直到RDB文件创建完毕,在进程阻塞期间,redis不能处理任何命令请求。
    2. BGSAVE:会fork出一个子进程,然后由子进程去负责生成RDB文件,父进程还可以继续处理命令请求,不会阻塞进程。
  2. 只追加文件AOF。以日志的形式记录每个写操作(非读操作)。当不同节点同步数据时,读取日志文件的内容将写指令从前到后执行一次,即可完成数据恢复。(两个都开启时,redis会优先采用aof)

AOF 重写机制

随着Redis的运行,AOF的日志会越来越长,如果实例宕机重启,那么重放整个AOF将会变得十分耗时,而在日志记录中,又有很多无意义的记录,比如我现在将一个数据 incr一千次,那么就不需要去记录这1000次修改,只需要记录最后的值即可。所以就需要进行 AOF 重写。

Redis 提供了bgrewriteaof指令用于对AOF日志进行重写,该指令运行时会开辟一个子进程对内存进行遍历,然后将其转换为一系列的 Redis 的操作指令,再序列化到一个日志文件中。完成后再替换原有的AOF文件,至此完成。

同样的也可以在redis.config中对重写机制的触发进行配置:

通过将no-appendfsync-on-rewrite设置为yes,开启重写机制;auto-aof-rewrite-percentage 100意为比上次从写后文件大小增长了100%再次触发重写;

auto-aof-rewrite-min-size 64mb意为当文件至少要达到64mb才会触发制动重写。

Redis 主从数据同步(主从复制)的过程?

  1. slave启动后,向master发送sync命令
  2. master收到sync之后,执行bgsave保存快照,生成RDB全量文件
  3. master把slave的写命令记录到缓存
  4. bgsave执行完毕之后,发送RDB文件到slave,slave执行
  5. master发送缓冲区的写命令给slave,slave接收命令并执行,完成复制初始化。
  6. 此后,master每次执行一个写命令都会同步发送给slave,保持master与slave之间数据的一致性

主从复制的优缺点?

优点:

  1. master能自动将数据同步到slave,可以进行读写分离,分担master的读压力
  2. master、slave之间的同步是以非阻塞的方式进行的,同步期间,客户端仍然可以提交查询或更新请求

缺点:

  1. 不具备自动容错与恢复功能,master 节点宕机后,需要手动指定新的 master
  2. 如果master宕机前数据没有同步完,则切换IP后会存在数据不一致的问题
  3. 难以支持在线扩容,Redis的容量受限于单机配置

普通的主从模式,当主数据库崩溃时,需要手动切换从数据库成为主数据库:

在从数据库中使用SLAVE NO ONE命令将从数据库提升成主数据继续服务。

启动崩溃的主库后,然后使用SLAVEOF命令将其设置成从库,即可同步数据。

Sentinel(哨兵)模式的优缺点?

哨兵模式基于主从复制模式,增加了哨兵来监控与自动处理故障。哨兵本身也有单点故障的问题,所以在一个一主多从的Redis系统中,可以使用多个哨兵进行监控,哨兵不仅会监控主数据库和从数据库,哨兵之间也会相互监控。每一个哨兵都是一个独立的进程,作为进程,它会独立运行。

优点:

  1. 哨兵模式基于主从复制模式,所以主从复制模式有的优点,哨兵模式也有
  2. master 挂掉可以自动进行切换,系统可用性更高

缺点:

  1. Redis的容量受限于单机配置
  2. 需要额外的资源来启动sentinel进程

哨兵实现原理

哨兵在启动进程时,会读取配置文件的内容,通过如下的配置找出需要监控的主数据库:

sentinel monitor master-name ip port quorum
#master-name是主数据库的名字
#ip和port 是当前主数据库地址和端口号
#quorum表示在执行故障切换操作前,需要多少哨兵节点同意。

这里之所以只需要连接主节点,是因为通过主节点的info命令,获取从节点信息,从而和从节点也建立连接,同时也能通过主节点的info信息知道新增从节点的信息。

一个哨兵节点可以监控多个主节点,但是并不提倡这么做,因为当哨兵节点崩溃时,同时有多个集群切换会发生故障。哨兵启动后,会与主数据库建立两条连接。

  1. 订阅主数据库_sentinel_:hello频道以获取同样监控该数据库的哨兵节点信息

  2. 定期向主数据库发送info命令,获取主数据库本身的信息。

跟主数据库建立连接后会定时执行以下三个操作:

  1. 每隔10s向master和 slave发送info命令。作用是获取当前数据库信息,比如发现新增从节点时,会建立连接,并加入到监控列表中,当主从数据库的角色发生变化进行信息更新。

  2. 每隔2s向主数据里和从数据库的_sentinel_:hello频道发送自己的信息。作用是将自己的监控数据和哨兵分享。每个哨兵会订阅数据库的_sentinel:hello频道,当其他哨兵收到消息后,会判断该哨兵是不是新的哨兵,如果是则将其加入哨兵列表,并建立连接。

  3. 每隔1s向所有主从节点和所有哨兵节点发送ping命令,作用是监控节点是否存活。

哨兵的故障转移过程

哨兵节点发送ping命令时,当超过一定时间(down-after-millisecond)后,如果节点未回复,则哨兵认为主观下线。主观下线表示当前哨兵认为该节点已经下面,如果该节点为主数据库,哨兵会进一步判断是够需要对其进行故障切换,这时候就要发送命令(SENTINEL is-master-down-by-addr)询问其他哨兵节点是否认为该主节点是主观下线,当达到指定数量(quorum)时,哨兵就会认为是客观下线

当主节点客观下线时就需要进行主从切换,主从切换的步骤为:

  1. 选出领头哨兵。

  2. 领头哨兵所有的slave选出优先级最高的从数据库。优先级可以通过slave-priority选项设置。

  3. 如果优先级相同,则从复制的命令偏移量越大(即复制同步数据越多,数据越新),越优先。

  4. 如果以上条件都一样,则选择run ID较小的从数据库。

  5. 选出一个从数据库后,哨兵发送slave no one命令升级为主数据库,并发送slaveof命令将其他从节点的主数据库设置为新的主数据库。

各大厂的redis 集群方案 1 客户端分片

客户端分片是把分片的逻辑放在Redis客户端实现,(比如:jedis已支持Redis Sharding功能,即ShardedJedis),通过Redis客户端预先定义好的路由规则(使用一致性哈希),把对Key的访问转发到不同的Redis实例中,查询数据时把返回结果汇集。这种方案的模式如图所示。
在这里插入图片描述

客户端分片的优缺点:

优点:客户端sharding技术使用hash一致性算法分片的好处是所有的逻辑都是可控的,不依赖于第三方分布式中间件。服务端的Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强。开发人员清楚怎么实现分片、路由的规则,不用担心踩坑。

1.一致性哈希算法:

是分布式系统中常用的算法。如果采用普通的hash方法,将数据映射到具体的节点上,如mod(key,d),key是数据的key,d是机器节点数,如果有一个机器加入或退出这个集群,则所有的数据映射都无效了

一致性哈希算法解决了普通余数Hash算法伸缩性差的问题,可以保证在上线、下线服务器的情况下尽量有多的请求命中原来路由到的服务器。

2.实现方式:一致性hash算法,比如MURMUR_HASH散列算法、ketamahash算法

比如Jedis的Redis Sharding实现,采用一致性哈希算法(consistent hashing),将key和节点name同时hashing,然后进行映射匹配,采用的算法是MURMUR_HASH。

不足:

这是一种静态的分片方案,需要增加或者减少Redis实例的数量,需要手工调整分片的程序。

运维成本比较高,集群的数据出了任何问题都需要运维人员和开发人员一起合作,减缓了解决问题的速度,增加了跨部门沟通的成本。

在不同的客户端程序中,维护相同的路由分片逻辑成本巨大。比如:java项目、PHP项目里共用一套Redis集群,路由分片逻辑分别需要写两套一样的逻辑,以后维护也是两套。

客户端分片有一个最大的问题就是,服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。如果能把客户端分片模块单独拎出来,形成一个单独的模块(中间件),作为客户端 和 服务端连接的桥梁就能解决这个问题了,此时代理分片就出现了。

2 代理分片

redis代理分片用得最多的就是Twemproxy,由Twitter开源的Redis代理,其基本原理是:通过中间件的形式,Redis客户端把请求发送到Twemproxy,Twemproxy根据路由规则发送到正确的Redis实例,最后Twemproxy把结果汇集返回给客户端。

Twemproxy通过引入一个代理层,将多个Redis实例进行统一管理,使Redis客户端只需要在Twemproxy上进行操作,而不需要关心后面有多少个Redis实例,从而实现了Redis集群。

在这里插入图片描述

Twemproxy的优点:
  1. 客户端像连接Redis实例一样连接Twemproxy,不需要改任何的代码逻辑。
  2. 支持无效Redis实例的自动删除。
  3. Twemproxy与Redis实例保持连接,减少了客户端与Redis实例的连接数。
Twemproxy的不足:
  1. 由于Redis客户端的每个请求都经过Twemproxy代理才能到达Redis服务器,这个过程中会产生性能损失。
  2. 没有友好的监控管理后台界面,不利于运维监控。
  3. Twemproxy最大的痛点在于,无法平滑地扩容/缩容。对于运维人员来说,当因为业务需要增加Redis实例时工作量非常大。

3 Codis

豌豆荚自主研发了Codis,一个支持平滑增加Redis实例的Redis代理软件,其基于Go和C语言开发。
在这里插入图片描述

Codis引入了Redis Server Group,其通过指定一个主CodisRedis和一个或多个从CodisRedis,实现了Redis集群的高可用。当一个主CodisRedis挂掉时,Codis不会自动把一个从CodisRedis提升为主CodisRedis,这涉及数据的一致性问题(Redis本身的数据同步是采用主从异步复制,当数据在主CodisRedis写入成功时,从CodisRedis是否已读入这个数据是没法保证的),需要管理员在管理界面上手动把从CodisRedis提升为主CodisRedis。

如果手动处理觉得麻烦,豌豆荚也提供了一个工具Codis-ha,这个工具会在检测到主CodisRedis挂掉的时候将其下线并提升一个从CodisRedis为主CodisRedis。

Codis中采用预分片的形式,启动的时候就创建了1024个slot,1个slot相当于1个箱子,每个箱子有固定的编号,范围是11024。slot这个箱子用作存放Key,至于Key存放到哪个箱子,可以通过算法“crc32(key)%1024”获得一个数字,这个数字的范围一定是11024之间,Key就放到这个数字对应的slot。

Codis最大的优势在于支持平滑增加(减少)Redis Server Group(Redis实例),能安全、透明地迁移数据,这也是Codis 有别于Twemproxy等静态分布式 Redis 解决方案的地方。Codis增加了Redis Server Group后,就牵涉到slot的迁移问题。

slot的迁移方式
  1. 管理工具Codisconfig手动重新分配
  2. 管理工具Codisconfig的rebalance功能,会自动根据每个Redis Server Group的内存对slot进行迁移,以实

Redis Cluster 模式的优缺点?

实现了Redis的分布式存储,即每台节点存储不同的内容,来解决在线扩容的问题。Redis Cluster是一种服务器Sharding技术(分片和路由都是在服务端实现),采用多主多从,每一个分区都是由一个Redis主机和多个从机组成,片区和片区之间是相互平行的。Redis Cluster集群采用了P2P的模式,完全去中心化。

优点:

  1. 无中心架构,数据按照slot分布在多个节点
  2. 集群中的每个节点都是平等的,每个节点都保存各自的数据和整个集群的状态。每个节点都和其他所有节点连接,而且这些连接保持活跃,这样就保证了我们只需要连接集群中的任意一个节点,就可以获取到其他节点的数据。
  3. 可线性扩展到1000多个节点,节点可动态添加或删除
  4. 能够实现自动故障转移,节点之间通过gossip协议交换状态信息,用投票机制完成slave到master的角色转换

缺点:

  1. 数据通过异步复制,不保证数据的强一致性
  2. slave充当 “冷备”,不对外提供读、写服务,只作为故障转移使用。
  3. 批量操作限制,目前只支持具有相同slot值的key执行批量操作,对mset、mget、sunion等操作支持不友好
  4. key事务操作支持有限,只支持多key在同一节点的事务操作,多key分布在不同节点时无法使用事务功能
  5. 不支持多数据库空间,一台redis可以支持16个db,集群模式下只能使用一个,即db 0。Redis Cluster模式不建议使用pipeline和multi-keys操作,减少max redirect产生的场景。

Redis 如何做扩容?

为了避免数据迁移失效,通常使用一致性哈希实现动态扩容缩容,有效减少需要迁移的Key数量。

但是Cluster 模式,采用固定Slot槽位方式(16384个),对每个key计算CRC16值,然后对16384取模,然后根据slot值找到目标机器,扩容时,我们只需要迁移一部分的slot到新节点即可。

Redis 事务执行流程?

通过MULTI、EXEC、WATCH等命令来实现事务机制,事务执行过程将一系列多个命令按照顺序一次性执行,在执行期间,事务不会被中断,也不会去执行客户端的其他请求,直到所有命令执行完毕。

具体过程:

  1. 服务端收到客户端请求,事务以MULTI开始
  2. 如果正处于事务状态时,则会把后续命令放入队列同时返回给客户端QUEUED,反之则直接执行这个命令
  3. 当收到客户端的EXEC命令时,才会将队列里的命令取出、顺序执行,执行完将当前状态从事务状态改为非事务状态
  4. 如果收到 DISCARD 命令,放弃执行队列中的命令,可以理解为Mysql的回滚操作,并且将当前的状态从事务状态改为非事务状态

redis 底层数据结构

redis内部整体的存储结构是一个大的hashmap,内部是数组实现的hash,key冲突通过挂链表去实现,每个dictEntry为一个key/value对象,value为定义的redisObject。

结构图如下:
在这里插入图片描述

redisObject的结构

redis对象底层的八种数据结构

  1. REDIS_ENCODING_INT(long 类型的整数)
  2. REDIS_ENCODING_EMBSTR embstr (编码的简单动态字符串)
  3. REDIS_ENCODING_RAW (简单动态字符串)
  4. REDIS_ENCODING_HT (字典)
  5. REDIS_ENCODING_LINKEDLIST (双端链表)
  6. REDIS_ENCODING_ZIPLIST (压缩列表)
  7. REDIS_ENCODING_INTSET (整数集合)
  8. REDIS_ENCODING_SKIPLIST (跳跃表和字典)

String数据结构

String类型的转换顺序

  1. 当保存的值为整数且值的大小不超过long的范围,使用整数存储
  2. 当字符串长度不超过44字节时,使用EMBSTR 编码
  3. 大于44字符时,使用raw编码

它只分配一次内存空间,redisObject和sds是连续的内存,查询效率会快很多,也正是因为redisObject和sds是连续在一起,伴随了一些缺点:当字符串增加的时候,它长度会增加,这个时候又需要重新分配内存,导致的结果就是整个redisObject和sds都需要重新分配空间,这样是会影响性能的,所以redis用embstr实现一次分配后,只允许读,如果修改数据,那么它就会转成raw编码,不再用embstr编码。embstr和raw都为sds编码。

sds结构体优点(为什么不用c语言的字符串呢,而是用sds结构体。)

  1. 低复杂度获取字符串长度:由于len存在,可以直接查出字符串长度,复杂度O(1);如果用c语言字符串,查询字符串长度需要遍历整个字符串,复杂度为O(n);

  2. 避免缓冲区溢出:进行两个字符串拼接c语言可使用strcat函数,但如果没有足够的内存空间。就会造成缓冲区溢出;而用sds在进行合并时会先用len检查内存空间是否满足需求,如果不满足,进行空间扩展,不会造成缓冲区溢出

  3. 减少修改字符串的内存重新分配次数:c语言字符串不记录字符串长度,如果要修改字符串要重新分配内存,如果不进行重新分配会造成内存缓冲区泄露;

redis sds实现了空间预分配和惰性空间释放两种策略

空间预分配

  1. 如果sds修改后,sds长度(len的值)小于1mb,则会分配与len相同大小的未使用空间,此时len与free值相同。例如,修改之后字符串长度为100字节,那么会给分配100字节的未使用空间。最终sds空间实际为 100 + 100 + 1(保存空字符’\0’);
  2. 如果大于等于1mb,每次给分配1mb未使用空间,

惰性空间释放

  1. 对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用 free 属性将这些字节的数量记录下来,等待后续使用(sds也提供api,我们可以手动触发字符串缩短);

二进制安全:因为C字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等),内容可能包括空字符串,因此C字符串无法正确存取;而所有 SDS 的API 都是以处理二进制的方式来处理 buf 里面的元素,并且 SDS 不是以空字符串来判断是否结束,而是以 len 属性表示的长度来判断字符串是否结束;

依然保留遵从每个字符串都是以空字符串结尾的惯例,这样可以重用 C 语言库<string.h> 中的一部分函数。

为什么小于44字节用embstr编码呢?

typedef struct redisObject {
    // 类型 4bits
    unsigned type:4;
    // 编码方式 4bits
    unsigned encoding:4;
    // LRU 时间(相对于 server.lruclock) 24bits
    unsigned lru:22;
    // 引用计数 Redis里面的数据可以通过引用计数进行共享 32bits
    int refcount;
    // 指向对象的值 64-bit
    void *ptr;
} robj;

短字符串的embstr用最小的sdshdr8,所以redisObject占用空间: 4 + 4 + 24 + 32 + 64 = 128bits = 16字节;sdshdr8占用空间1(uint8_t) + 1(uint8_t)+ 1 (unsigned char)+ 1(buf[]中结尾的’\0’字符)= 4字节
初始最小分配为64字节,所以只分配一次空间的embstr最大为 64 - 16- 4 = 44字节

List存储结构

  1. Redis3.2之前的底层实现方式:压缩列表ziplist 或者 双向循环链表linkedlist
    当list存储的数据量比较少且同时满足下面两个条件时,list就使用ziplist存储数据:
    1. list中保存的每个元素的长度小于 64 字节;
    2. 列表中数据个数少于512个
  2. Redis3.2及之后的底层实现方式:quicklist
    quicklist是一个双向链表,而且是一个基于ziplist的双向链表,quicklist的每个节点都是一个ziplist,结合了双向链表和ziplist的优点。

ziplist

ziplist是一种压缩链表,它的好处是更能节省内存空间,因为它所存储的内容都是在连续的内存区域当中的。当列表对象元素不大,每个元素也不大的时候,就采用ziplist存储。但当数据量过大时就ziplist就不是那么好用了。因为为了保证他存储内容在内存中的连续性,插入的复杂度是O(N),即每次插入都会重新进行realloc

为什么数据量大时不用ziplist?

因为ziplist是一段连续的内存,插入的时间复杂化度为O(n),而且每当插入新的元素需要realloc做内存扩展;而且如果超出ziplist内存大小,还会做重新分配的内存空间,并将内容复制到新的地址。如果数量大的话,重新分配内存和拷贝内存会消耗大量时间。所以不适合大型字符串,也不适合存储量多的元素。

快速列表(quickList)

快速列表是ziplist和linkedlist的混合体,是将linkedlist按段切分,每一段用ziplist来紧凑存储,多个ziplist之间使用双向指针链接。

quicklist下是用多个ziplist组成的,同时为了进一步节约空间,Redis还会对ziplist进行压缩存储,使用LZF算法压缩,可以选择压缩深度。quicklist默认的压缩深度是0,也就是不压缩。压缩的实际深度由配置参数list-compress-depth决定。为了支持快速push/pop操作,quicklist 的首尾两个 ziplist 不压缩,此时深度就是 1。如果深度为 2,就表示 quicklist 的首尾第一个 ziplist 以及首尾第二个 ziplist 都不压缩。

为什么不直接使用linkedlist?

linkedlist的附加空间相对太高,prev和next指针就要占去16个字节,而且每一个结点都是单独分配,会加剧内存的碎片化,影响内存管理效率。

Hash类型

当Hash中数据项比较少的情况下,Hash底层才用压缩列表ziplist进行存储数据,随着数据的增加,底层的ziplist就可能会转成dict,具体配置如下

hash-max-ziplist-entries 512
hash-max-ziplist-value 64

每个dict中都有两个hashtable,但是通常情况下只有一个hashtable是有值的。但是在dict扩容缩容的时候,需要分配新的hashtable,然后进行渐近式搬迁,这时候两个hashtable存储的 旧的hashtable和新的hashtable。搬迁结束后,旧hashtable删除,新的取而代之。

采用渐进式rehash 的好处在于它采取分而治之的方式,避免了集中式rehash 带来的庞大计算量。特别的在进行rehash时只能对h[0]元素减少的操作,如查询和删除;而查询是在两个哈希表中查找的,而插入只能在ht[1]中进行,ht[1]也可以查询和删除。)

什么是渐进式rehash

由于大字典的扩容是比较消耗时间的,需要重新申请新的数组,然后将旧字典所有链表的元素重新挂接到新的数组下面,是一个O(n)的操作。但是redis是单线程的,无法承受这样的耗时过程,所以采用了渐进式rehash小步搬迁,虽然慢一点,但是可以搬迁完毕。

扩容条件

一般会在Hash表中的元素个数等于第一维数组的长度的时候,就会开始扩容。扩容的大小是原数组的两倍。不过在redis在做bgsave(RDB持久化操作的过程),为了减少内存页的过多分离(Copy On Write),redis不会去扩容。但是如果hash表的元素个数已经到达了第一维数组长度的5倍的时候,就会强制扩容,不管你是否在持久化。

缩容条件

元素个数低于数组长度的10%,redis就会对hash表进行缩容来减少第一维数组长度的空间占用。并且缩容不考虑是否在做redis持久化。

rehash步骤

1、为hashtable[1] 分配空间,让字典同时持有hashtable[0]和hashtable[1]两个哈希表;
2、定时维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash 开始;
3、在rehash进行期间,每次对字典执行CRUD操作时,程序除了执行指定的操作以外,还会将ht[0]中的数据rehash 到ht[1]表中,并且将rehashidx加一;
4、当ht[0]中所有数据转移到ht[1]中时,将rehashidx 设置成-1,表示rehash 结束;
5、将ht[0]释放,然后将ht[1]设置成ht[0],最后为ht[1]分配一个空白哈希表。

set数据结构

Redis 的集合相当于Java中的 HashSet,它内部的键值对是无序、唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值 NULL。集合Set类型底层编码包括hashtable和inset。

当存储的数据同时满足下面这样两个条件的时候,Redis 就采用整数集合intset来实现set这种数据类型:

存储的数据都是整数
存储的数据元素个数小于512个

当不能同时满足这两个条件的时候,Redis 就使用dict来存储集合中的数据。intset是一个有序集合,查找元素的复杂度为O(logN)(采用二分法),但插入时不一定为O(logN),因为有可能涉及到升级操作。比如当集合里全是int16_t型的整数,这时要插入一个int32_t,那么为了维持集合中数据类型的一致,那么所有的数据都会被转换成int32_t类型,涉及到内存的重新分配,这时插入的复杂度就为O(N)了。是intset不支持降级操作。

inset是有序不要和我们zset搞混,zset是设置一个score来进行排序,而inset这里只是单纯的对整数进行升序而已

Zset数据结构

Zset有序集合和set集合有着必然的联系,他保留了集合不能有重复成员的特性,但不同的是,有序集合中的元素是可以排序的,但是它和列表的使用索引下标作为排序依据不同的是,它给每个元素设置一个分数,作为排序的依据。

zet的底层编码有两种数据结构,一个ziplist,一个是skiplist。

ziplist做排序

每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)。

skiplist跳表

如果是链表,想查找到node5需要从node1查到node5,查询耗时,但如果在node上加上索引,这样通过索引就能直接从node1查找到node5,即跳表。

三大特殊数据类型

1 geospatial(地理位置)

geospatial将指定的地理空间位置(纬度、经度、名称)添加到指定的key中。
应用场景

  1. 查看附近的人
  2. 微信位置共享
  3. 地图上直线距离的展示

2 Hyperloglog(基数)

什么是基数? 不重复的元素。
hyperloglog 是用来做基数统计的,其优点是:输入的体积无论多么大,hyperloglog使用的空间总是固定的12KB ,利用12KB,它可以计算2^64个不同元素的基数!非常节省空间!但缺点是估算的值,可能存在误差

应用场景

  1. 浏览用户数量,同一天同一个ip多次访问算一次访问,目的是计数,而不是保存用户
    传统的方式,set保存用户的id,可以统计set中元素数量作为标准判断。但如果这种方式保存大量用户id,会占用大量内存,我们的目的是为了计数,而不是去保存id。

3 Bitmaps(位存储)

Redis提供的Bitmaps这个“数据结构”可以实现对位的操作。Bitmaps本身不是一种数据结构,实际上就是字符串,但是它可以对字符串的位进行操作。

可以把Bitmaps想象成一个以位为单位数组,数组中的每个单元只能存0或者1,数组的下标在bitmaps中叫做偏移量。单个bitmaps的最大长度是512MB,即2^32个比特位。

应用场景

两种状态的统计都可以使用bitmaps,例如:统计用户活跃与非活跃数量、登录与非登录、上班打卡等等。

redis 事务

redis事务提供了一种“将多个命令打包, 然后一次性、按顺序地执行”的机制, 并且事务在执行的期间不会主动中断 —— 服务器在执行完事务中的所有命令之后, 才会继续处理其他客户端的其他命令。

Redis中一个事务从开始到执行会经历开始事务(muiti)、命令入队和执行事务(exec)三个阶段,事务中的命令在加入时都没有被执行,直到提交时才会开始执行(Exec)一次性完成。

一组命令中存在两种错误不同处理方式

  1. 代码语法错误(编译时异常)所有命令都不执行
  2. 代码逻辑错误(运行时错误),其他命令可以正常执行 (该点不保证事务的原子性)

为什么redis不支持回滚来保证原子性

  1. Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
  2. 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值