简介

使用场景

  • 数据缓存
  • 分布式锁(setnx)
  • 全局ID(incr)
  • 计数器/限流(incr)
  • 位统计(bitmap)
  • 时间线timeline(list)
  • 消息队列:List提供了两个阻塞的弹出操作:blpop/brpop,可以设置超时时间
  • 抽奖:自带一个随机获得值:spop myset
  • 点赞、签到、打卡(set)
  • 商品标签(set)
  • 商品筛选:获取差集:sdiff set1 set2;获取交集(intersection ):sinter set1 set2;获取并集:sunion set1 set2
  • 用户关注、推荐模型(set)
  • 排行榜(zset)

Redis 和 Memcached 的区别和共同点

现在公司一般都是用 Redis 来实现缓存,而且 Redis 自身也越来越强大了!不过,了解 Redis 和 Memcached 的区别和共同点,有助于我们在做相应的技术选型的时候,能够做到有理有据!

共同点 :

  1. 都是基于内存的数据库,一般都用来当做缓存使用。
  2. 都有过期策略。
  3. 两者的性能都非常高。

区别 :

  1. Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只支持最简单的 k/v 数据类型。
  2. Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 把数据全部存在内存之中。
  3. Redis 有灾难恢复机制。 因为可以把缓存中的数据持久化到磁盘上。
  4. Redis 在服务器内存使用完之后,可以将不用的数据放到磁盘上。但是,Memcached 在服务器内存使用完之后,就会直接报异常。
  5. Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 目前是原生支持 cluster 模式的。
  6. Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。 (Redis 6.0 引入了多线程 IO )
  7. Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言。
  8. Memcached 过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。

关于Redis多线程

1.Redis6.0之前的版本真的是单线程吗?

     Redis在处理客户端的请求时,包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。但如果严格来讲从Redis4.0之后并不是单线程,除了主线程外,它也有后台线程在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大 key 的删除等等。

2.Redis6.0之前为什么一直不使用多线程?

 官方曾做过类似问题的回复:使用Redis时,几乎不存在CPU成为瓶颈的情况, Redis主要受限于内存和网络。例如在一个普通的Linux系统上,Redis通过使用pipelining每秒可以处理100万个请求,所以如果应用程序主要使用O(N)或O(log(N))的命令,它几乎不会占用太多CPU。

使用了单线程后,可维护性高。多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。Redis通过AE事件模型以及IO多路复用等技术,处理性能非常高,因此没有必要使用多线程。单线程机制使得 Redis 内部实现的复杂度大大降低,Hash 的惰性 Rehash、Lpush 等等 “线程不安全” 的命令都可以无锁进行。


3.Redis6.0为什么要引入多线程呢?

    Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,对于小数据包,Redis服务器可以处理80,000到100,000 QPS,这也是Redis处理的极限了,对于80%的公司来说,单线程的Redis已经足够使用了。

    但随着越来越复杂的业务场景,有些公司动不动就上亿的交易量,因此需要更大的QPS。常见的解决方案是在分布式架构中对数据进行分区并采用多个服务器,但该方案有非常大的缺点,例如要管理的Redis服务器太多,维护代价大;某些适用于单个Redis服务器的命令不适用于数据分区;数据分区无法解决热点读/写问题;数据偏斜,重新分配和放大/缩小变得更加复杂等等。

    从Redis自身角度来说,因为读写网络的read/write系统调用占用了Redis执行期间大部分CPU时间,瓶颈主要在于网络的 IO 消耗。所以总结起来,redis支持多线程主要就是两个原因:

    • 可以充分利用服务器 CPU 资源,目前主线程只能利用一个核

    • 多线程任务可以分摊 Redis 同步 IO 读写负荷


4.Redis6.0默认是否开启了多线程? 

    Redis6.0的多线程默认是禁用的,只使用主线程。如需开启需要修改redis.conf配置文件:io-threads-do-reads yes

5.Redis6.0多线程开启时,线程数如何设置?

    开启多线程后,还需要设置线程数,否则是不生效的。同样修改redis.conf配置文件

    关于线程数的设置,官方有一个建议:4核的机器建议设置为2或3个线程,8核的建议设置为6个线程,线程数一定要小于机器核数。还需要注意的是,线程数并不是越大越好,官方认为超过了8个基本就没什么意义了。


6.Redis6.0采用多线程后,性能的提升效果如何?

    Redis 作者 antirez 在 RedisConf 2019分享时曾提到:Redis 6 引入的多线程 IO 特性对性能提升至少是一倍以上。国内也有大牛曾使用unstable版本在阿里云esc进行过测试,GET/SET 命令在4线程 IO时性能相比单线程是几乎是翻倍了。

7.Redis6.0多线程的实现机制? 

流程简述如下:

1、主线程负责接收建立连接请求,获取 socket 放入全局等待读处理队列

2、主线程处理完读事件之后,通过 RR(Round Robin) 将这些连接分配给这些 IO 线程

3、主线程阻塞等待 IO 线程读取 socket 完毕

4、主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行

5、主线程阻塞等待 IO 线程将数据回写 socket 完毕

6、解除绑定,清空等待队列

该设计有如下特点:

1、IO 线程要么同时在读 socket,要么同时在写,不会同时读或写

2、IO 线程只负责读写 socket 解析命令,不负责命令处理

8.开启多线程后,是否会存在线程并发安全问题? 

从上面的实现机制可以看出,Redis的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。所以我们不需要去考虑控制 key、lua、事务,LPUSH/LPOP 等等的并发及线程安全问题。

9.Redis作者是如何点评 “多线程”这个新特性的?

关于多线程这个特性,在6.0 RC1时,Antirez曾做过说明:

Redis支持多线程有2种可行的方式:第一种就是像“memcached”那样,一个Redis实例开启多个线程,从而提升GET/SET等简单命令中每秒可以执行的操作。这涉及到I/O、命令解析等多线程处理,因此,我们将其称之为“I/O threading”。另一种就是允许在不同的线程中执行较耗时较慢的命令,以确保其它客户端不被阻塞,我们将这种线程模型称为“Slow commands threading”。

经过深思熟虑,Redis不会采用“I/O threading”,redis在运行时主要受制于网络和内存,所以提升redis性能主要是通过在多个redis实例,特别是redis集群。接下来我们主要会考虑改进两个方面:

1. Redis集群的多个实例通过编排能够合理地使用本地实例的磁盘,避免同时重写AOF。

2.提供一个Redis集群代理,便于用户在没有较好的集群协议客户端时抽象出一个集群。

补充说明一下,Redis和memcached一样是一个内存系统,但不同于Memcached。多线程是复杂的,必须考虑使用简单的数据模型,执行LPUSH的线程需要服务其他执行LPOP的线程。

我真正期望的实际是“slow operations threading”,在redis6或redis7中,将提供“key-level locking”,使得线程可以完全获得对键的控制以处理缓慢的操作。

详见:http://antirez.com/news/126

内存碎片化

1、Redis 存储存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间。(通常2^n分配)

2、频繁修改 Redis 中的数据也会产生内存碎片。

查看 Redis 内存碎片的信息:info memory

清理 Redis 内存碎片:Redis4.0-RC3 版本以后自带了内存整理,可以避免内存碎片率过大的问题。

缓存读写策略/更新策略

1.旁路缓存模式

写 :1)先更新 DB;2)然后直接删除 cache 。

读 :1)从cache中读数据,读取到就返回;2)读取不到就从 DB 中读取数据返回;3)再把数据放到 cache 中。

问题1:数据不在缓存中时,一个线程改/一个线程读,最后可能存的是读线程写进去的旧值;可以加缓存过期时间(一段时间不一致)。

问题2:可以先删除 cache ,后更新 DB 吗——更新的时候其它线程访问又把旧数据放到缓存中了;可以加一个加载命令队列。

2.其它

比如缓存自带更新db的能力,应用只用读写缓存即可

缓存穿透/雪崩/热点

缓存(千万千万不要设计复杂的缓存,到时候各种不一致问题烦死你)

cdn、nginx缓存、网关缓存、数据层缓存redis、db本身也有缓存(sql结果缓存、读取的磁盘分页缓存)

缓存穿透:1本身无数据(添加默认值缓存/布隆过滤器) 2未生成缓存(使用分布式锁限制读db,注意识别爬虫并禁止/但可能影响seo)

缓存雪崩:缓存实效后大家都在更新缓存导致系统性能急剧下降(1消息队列通知后台更新、2使用分布式更新锁)

缓存热点:大部分业务都会命中的同一份缓存,比如1000w+粉丝的微博消息,复制多分缓存副本,key里面加副本编号将请求分散,且设置过期范围,而不是所有副本固定同一过期时间。

缓存框架看一下设计思路:echcache、网友分享https://github.com/qiujiayu/AutoLoadCache

Redis实现

线程模型

Redis是单线程模型为什么效率还这么高?

  • 纯内存操作:数据存放在内存中,内存的响应时间大约是100纳秒,这是Redis每秒万亿级别访问的重要基础
  • 非阻塞的I/O多路复用机制:Redis采用epoll做为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接,读写,关闭都转换为了事件,不在I/O上浪费过多的时间
  • C语言实现:距离操作系统更近,执行速度会更快
  • 单线程避免切换开销:单线程避免了多线程上下文切换的时间开销,预防了多线程可能产生的竞争问题

c redis 添加集合 redis的集合实现原理_AOF

数据类型/实现方式

常用的 5 种数据类型

  • String: 缓存、计数器、分布式锁、分布式session等。String 数据结构是简单的 key-value 类型,value 不仅可以是 String,也可以是数字(当数字类型用 Long 可以表示的时候encoding 就是整型,其他都存储在 sdshdr 当做字符串)。常用命令: set,get,strlen,exists,decr,incr,setex 等等。
  • List: 链表、队列、微博关注人时间轴列表等(lpush+lpop=Stack(栈) lpush+rpop=Queue(队列) lpush+ltrim=Capped Collection(有限集合) lpush+brpop=Message Queue(消息队列))
  • Hash: 用户信息、Hash/json数据等,可以只修改某一项属性值
  • Set: 去重、赞、踩、共同好友等
  • Zset: 访问量排行榜、点击量排行榜等

特殊数据结构

  • HyperLogLog(基数统计)
  • Geo(地理空间信息)
  • Pub/Sub(发布订阅)
  • Bitmap(位图-节约存储) 用户在线状态、用户签到状态、统计独立用户
  • BloomFilter(布隆过滤)/布谷:解决缓存穿透、黑名单校验、Web拦截器

c redis 添加集合 redis的集合实现原理_AOF_02

使用 keys 指令可以扫出指定模式的 key 列表

如果这个 Redis 正在给线上的业务提供服务,那使用 keys 指令会有什么问题?这个时候你要回答 Redis 关键的一个特性:Redis 的单线程的。keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。

这个时候可以使用 scan 指令,scan 指令可以无阻塞地提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。

RedisBigKey

有一个不是特别精确的参考标准:string 类型的 value 超过 10 kb,复合类型的 value 包含的元素超过 5000 个(对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多)。

使用 Redis 自带的 --bigkeys 参数来查找:redis-cli -p 6379 --bigkeys

事务

Redis 可以通过 MULTI,EXEC,DISCARD 和 WATCH 等命令来实现事务(transaction)功能。

  1. 开始事务(MULTI)。
  2. 命令入队(批量操作 Redis 的命令,先进先出(FIFO)的顺序执行)。
  3. 通过DISCARD命令取消事务,它会清空事务队列中保存的所有命令(并不是回滚)。
  4. 执行事务(EXEC)。
  5. 当调用 EXEC 命令执行事务时,如果一个被 WATCH 命令监视的键被修改的话,整个事务都不会执行,直接返回失败。

Redis 是不支持 roll back 的,因而不满足原子性的(而且不满足持久性)。EXEC时相当于批量执行,如果中途失败,会出现前面的成功,后面的失败的情况。

持久化方式

RDB

在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。

【手动触发 or 配置自动触发规则:每xx秒更新了n条】

优点

  • RDB快照是一个编码过的非常紧凑的文件,文件占用小。
  • 保存RDB是fork子进程操作,对父进程继续提供服务的性能不会造成影响。
  • 与AOF相比,恢复大数据集的时候会更快

缺点

  • 保存整个数据集的过程是比繁重的,每次都是全量快照
  • 几分钟一次,如果服务器宕机,那么就可能丢失几分钟的数据
  • Redis数据集较大时,fork的子进程要完成快照会比较耗CPU、耗时

AOF

所有的命令行记录以 Redis 命令请求协议的格式完全持久化存储保存为 aof 文件。Redis 是先执行命令,把数据写入内存,然后才记录日志。因为该模式是只追加的方式,所以没有任何磁盘寻址的开销,所以很快,有点像 Mysql 中的binlog,AOF更适合做热备。

【配置规则:每条都追加刷盘、每秒刷盘、根据系统自己决定】

优点

  • 指令追加的日志文件,秒级数据丢失,安全性更高(取决fsync策略,如果是everysec,最多丢失1秒的数据)
  • 可以通过文件重写,从数据库中直接读取现有键落库后替换原有日志文件,从而缩小文件体积
  • 以Redis协议的格式保存,内容是可读的,适合误删紧急恢复

缺点

  • 对于相同的数据集,AOF文件的体积要大于RDB文件
  • 数据恢复也会比较慢,指令执行一遍的方式恢复
  • 根据所使用的fsync策略,AOF的速度可能会慢于RDB。 不过在一般情况下,每秒fsync的性能依然非常高

文件重写(手动触发 or 配置文件大小自动触发):为了解决 AOF 文件体积膨胀的问题,不需要对现有的 AOF 文件进行任何操作,从数据库中直接读取键现在的值,用一条命令记录键值对,从而代替之前记录这个键值对的多条命令。

混合模式

redis4.0开始支持该模式。开启后,AOF在重写时会直接读取RDB中的内容。

混合持久化结合了RDB持久化 和 AOF 持久化的优点, 由于绝大部分都是RDB格式,加载速度快,同时结合AOF,增量的数据以AOF方式保存了,数据更少的丢失。

过期策略

过期策略用于处理过期缓存数据。Redis采用过期策略:惰性删除 + 定期删除。

memcached采用过期策略:惰性删除。

定时过期

每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即对key进行清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。

惰性过期

只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。

定期过期

每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。expires字典会保存所有设置了过期时间的key的过期时间数据,其中 key 是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。

淘汰策略

LRU(最近最少使用)

  • volatile-lru:从已设置过期时间的key中,挑选**最近最少使用(最长时间没有使用)**的key进行淘汰
  • allkeys-lru:从所有key中,挑选最近最少使用的数据淘汰

LFU(频率最少使用)

  • volatile-lfu:从已设置过期时间的key中,挑选**频率最少使用(使用次数最少)**的key进行淘汰
  • allkeys-lfu:从所有key中,选择某段时间内内最近最不经常使用的数据淘汰

Random(随机淘汰)

  • volatile-random:从已设置过期时间的key中,任意选择数据淘汰
  • allkeys-random:从所有key中,任意选择数据淘汰

TTL(过期时间)

  • volatile-ttl:从已设置过期时间的key中,挑选将要过期的数据淘汰

No-Enviction(驱逐)

  • noenviction(驱逐):当达到最大内存时直接返回错误,不覆盖或逐出任何数据

部署架构

1.单节点(Single)

优点

  • 架构简单,部署方便
  • 普通场景都可满足

优点

  • 不保证数据的可靠性
  • 在缓存使用,进程重启后,数据丢失
  • 性能受限于单核CPU的处理能力(Redis是单线程机制),适合操作命令简单,排序/计算较少场景

2.主从复制(Replication)

主从同步:1.sync/psync初次同步rdb;2.追加同步增量

优点

  • 1.可以进行读写分离,分担master的读压力
  • 2.非阻塞方式同步,同步期间客户端仍可提交查询或更新请求
  • 3.数据备份

缺点

  • 1.不具自动容错和恢复的功能,master或slave宕机都可能导致客户端请求失败,需机器重启或手动切换客户端IP才能恢复
  • 2.宕机前未完成数据同步,则切换后存在数据不一致问题
  • 3.难以支持在线扩容, Redis的容量受限于单机配置

3.哨兵(Sentinel)

优点

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

缺点

  • 1.同样也继承了主从模式难以在线扩容的缺点,Redis的容量受限于单机配置
  • 2.需额外资源来启动Sentinel,实现相对复杂,同时Slave节点作为备份节点不提供服务

c redis 添加集合 redis的集合实现原理_缓存_03

 

4.集群(Cluster)

优点

  • 1.组件all-in-box,部署简单,节约机器资源
  • 2.性能比proxy模式好
  • 3.自动故障转移、Slot迁移中数据可用
  • 4.官方原生集群方案,更新与支持有保障(最小的redis集群,需要至少3个主节点,既然有3个主节点,而一个主节点搭配至少一个从节点,因此至少得6台redis)

缺点

  • 1.架构比较新,最佳实践较少
  • 2.多键操作支持有限(驱动可以曲线救国)
  • 3.为了性能提升,客户端需要缓存路由表信息
  • 4.节点发现、reshard操作不够自动化

c redis 添加集合 redis的集合实现原理_c redis 添加集合_04

 

其他:

redis设计与实现源码 http://redisbook.com/