一、缓存Redis
1、 数据结构
(1)String
String是Redis的基础数据类型,所有的基本类型在Redis中都是以String体现,其值最大可存储 512M,二进制安全(Redis 的 String 可以包含任何二进制数据,包含 jpg 对象等)。
(2)Hash
String元素组成的字典,一种field-value型的数据结构,适用于存储对象。
(3)List
列表,按照 String 元素插入顺序排序。其顺序为后进先出。由于其具有栈的特性,所以可以实现如“最新消息排行榜”这类的功能。
(4)Set
String 元素组成的无序集合,通过哈希表实现(增删改查时间复杂度为 O(1)),不允许重复。另外,当我们使用 Smembers 遍历 Set 中的元素时,其顺序也是不确定的,是通过 Hash 运算过后的结果。
Redis 还对集合提供了求交集、并集、差集等操作,可以实现如同共同关注,共同好友等功能 。
(5)Sorted Set
Strring 元素组成的不可重复、有序集合,每一个元素都需要指派一个分数(score),根据score对元素进行升序排序,如果多个元素拥有相同的score,则以字典进行升序排序。
(6)特殊数据类型(BitMap、Geo、HyperLogLog)
BitMap 就是通过一个 bit 位来表示某个元素对应的值或者状态, 其中的 key 就是对应元素本身,实际上底层也是通过对字符串的操作来实现。
GEO 这个功能可以将用户给定的地理位置信息储存起来, 并对这些信息进行操作。
HyperLogLog 基数统计,这个结构可以非常省内存的去统计各种计数,比如注册 IP 数、每日访问 IP 数、页面实时UV、在线用户数等。但是它也有局限性,就是只能统计数量,而没办法去知道具体的内容是什么。
2、持久化
持久化,即将数据持久存储,而不因断电或其他各种复杂外部环境影响数据的完整性。
由于 Redis 将数据存储在内存而不是磁盘中,所以内存一旦断电,Redis 中存储的数据也随即消失,所以 Redis 有持久化机制来保证数据的安全性。
(1)RDB 持久化
RDB持久化会在某个特定的间隔保存那个时间点的全量数据的快照。
RDB 配置文件,redis.conf:
RDB的指令创建:
SAVE:阻塞 Redis 的服务器进程,直到 RDB 文件被创建完毕。SAVE 命令很少被使用,因为其会阻塞主线程来保证快照的写入,由于 Redis 是使用一个主线程来接收所有客户端请求,这样会阻塞所有客户端请求。
BGSAVE:该指令会 Fork 出一个子进程来创建 RDB 文件,不阻塞服务器进程,子进程接收请求并创建 RDB 快照,父进程继续接收客户端的请求。子进程在完成文件的创建时会向父进程发送信号,父进程在接收客户端请求的过程中,在一定的时间间隔通过轮询来接收子进程的信号。可以通过使用 lastsave 指令来查看 BGSAVE 是否执行成功,lastsave 可以返回最后一次执行成功 BGSAVE 的时间。
自动化触发 RDB 持久化的方式:
根据 redis.conf 配置里的 SAVE m n 定时触发(实际上使用的是 BGSAVE)。
- 主从复制时,主节点自动触发。
- 执行 Debug Reload。
- 执行 Shutdown 且没有开启 AOF 持久化。
(2)AOF持久化
打开 redis.conf 配置文件,将 appendonly 属性改为 yes。
修改 appendfsync 属性,该属性可以接收三种参数,分别是 always,everysec,no 。always 表示总是即时将缓冲区内容写入 AOF 文件当中,everysec 表示每隔一秒将缓冲区内容写入 AOF 文件,no 表示将写入文件操作交由操作系统决定。
AOF rewrite功能:
可以重写AOF文件,只保留能够把数据恢复到最新状态的最小写操作集。
自动触发配置:
指令触发AOF rewrite:
BGREWRITEAOF : 当前AOF/RDB数据持久化没有在执行,那么执行,反之,等当前AOF/RDB数据持久化结束后执行AOF rewrite
(3)混合持久化
Redis4.0开始支持RDB和AOF的混合持久化,快速加载的同时避免丢失过多的数据
(4)RDB和AOF的优缺点
- RDB 优点:全量数据快照,文件小,恢复快。
- RDB 缺点:无法保存最近一次快照之后的数据。内存数据全量同步,数据量大的情况下,fork子进程会很费时间以及耗cpu(需要压缩数据集),就会很慢。
- AOF 优点:可读性高,适合保存增量数据,数据不易丢失。
- AOF 缺点:文件体积大,恢复时间长。
3、Pipeline、事务、脚本
(1)Pipeline
Pipeline 和 Linux 的管道类似,它可以让 Redis 批量执行指令。
Redis 基于请求/响应模型,单个请求处理需要一一应答。如果需要同时执行大量命令,则每条命令都需要等待上一条命令执行完毕后才能继续执行,这中间不仅仅多了 RTT,还多次使用了系统 IO。
Pipeline 由于可以批量执行指令,所以可以节省多次 IO 和请求响应往返的时间。但是如果指令之间存在依赖关系,则建议分批发送指令。
(2)事务
Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
Redis事务没有隔离级别的概念:
批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到。
Redis不保证原子性:
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
(3)脚本
Redis 可以执行lua脚本,可以把客户端与Redis之间密集的读写交互放在服务端进行,避免过多的数据交互,提升性能。
4、高可用
(1)主从复制
Redis支持一主多从的主从复制架构,一个Master实例负责处理所有的写请求,Master将写操作同步至所有的Slave。
当Slave启动后,会从Master进行一次冷启动数据同步,从节点向主节点发送psync命令,由Mster触发BGSAVE生成RDB文件推送给Slave进行导入,导入完成后Master再将增量数据通过Redis Protocal 同步给Slave。之后主从之间的数据便一直以Redis Protocol进行同步。
Redis 提供的主从复制功能,实现了一份数据存在多个相同的副本,它是实现 Redis 高可用的基础,作用有如下几个:
- 数据冗余:主从复制实现了数据的热备份,是 Redis 持久化之外的一种数据冗余方式
- 故障恢复:当主节点出现故障时,可以将从节点晋升为主节点继续提供服务,实现快速的故障恢复
- 读写分离:主从复制可以实现读写分离,主节点写,从节点读,读写分离提高了服务器的负载能力
- 高可用的基石:主从复制是哨兵和集群能够实施的基础,因此说主从复制是 Redis 高可用的基础
Redis搭建主从复制模式的时候需要注意以下配置:
- bind 可以直接配置内网IP(网卡ip地址),或者0.0.0.0 ,不然其他虚拟机无法访问
- requirepass 、masterauth 登录请求redis的密码 如果master配置了requirepass ,slave 需要配置masterauth 和master的requirepass一样。通常 requirepass
、masterauth 保持一致,便于管理。
(2)Sentinel
Redis Sentinel 是一个能够自动完成故障发现和故障转移并通知应用方,从而实现真正的高可用的分布式架构。
Redis Sentinel 需要至少部署三个实例才能形成选举关系。Sentinel有以下功能:
- 监控(Monitoring):哨兵会不断地检查主节点和从节点是否运作正常。
- 自动故障转移(Automatic failover):当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
- 配置提供者(Configuration provider):客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址。
- 通知(Notification):哨兵可以将故障转移的结果发送给客户端
监控和自动故障转移使得 Sentinel 能够完成主节点故障发现和自动转移,配置提供者和通知则是实现通知客户端主节点变更的关键。
Redis搭建哨兵模式的时候需要注意以下配置:
- bind 可以直接配置内网IP(网卡ip地址),或者0.0.0.0 ,不然也会出现sentinel没法互相访问的问题
- sentinel auth-pass mymaster 密码 如果master配置了requirepass,sentinel的配置需要加这个配置,不然sentinel无法访问master
,状态为sdown。
(3)集群
Redis Cluster 的每个数据分片都采用了主从复制的结构。
Redis Cluster 中共有16384个hash slot ,Redis会计算每个key的CRC16,将结果与16384取模,来决定该key存储在哪一个hash slot中,同时需要指定集群中每个数据分片负责的slot数,slot的分配在任何时间点都可以进行重新分配。
客户端在对key进行读写操作时,可以连接Cluster中的任意一个分片,如果操作的key不在此分片负责的slot范围内,会自动将请求重定向到正确的分片上。
redis 的集群部署用到工具 redis-trib.rb ,该工具在redis 源码的 src目录下 ,而它的运行依赖 ruby ,rubygem和 redis.gem
需要安装 zlib-devel 、openssl-devel ,编译时生成zlib 和openssl 依赖。如果已经安装zlib 和 openssl 可以在编译前执行
如果已经安装openssl 直接在ruby 编译时配置openssl 地址
./configure --with-openssl-dir=/usr/local/ssl
否则编译安装时会出现如下提示:
redis.gem是通过 rubygems 命令安装,在安装ruby会一起带有rubygems。如果没有生成zlib 和 openssl的依赖。安装redis.gem时就会出现如下错误:
通过gem成功安装redis:
通过 redis-trib.rb 配置集群: --replicas 2 表示每个master节点有两个 slave节点
集群配置需要用到两种端口:命令端口和集群总线端口,命令端口和集群总线端口偏移是固定的,始终为10000所以防火墙除了需要开放配置中的命令端口还需要放开集群总线的端口 。不然会出现如下一直等待的情况:
集群配置失败后,从新配置后直接启动的话会出现如下的错误,需要删除nodes.conf 和 pid文件,重启redis-server. 再重新配置集群
集群配置成功:
登录集群进行测试测试:
redis-cli -c -p 6379
-c 表示以集群模式登录,不然如果登录从节点的话会报:
查看集群信息:
cluster nodes
可以查看到 0-5460 分片 、5461-10922 分片 10923-16383 分片 分别在三个 master 节点上,每个master节点分配了两个slave 节点.
如果获取的数据不在该节点的分片上,会自动定位到数据节点分片,并返回数据:
二、缓存跨数据中心同步
多数据中心首先要解决的是数据复制问题,即数据如何从一个DC传输到另外一个DC,通常有如下方案:
1、客户端双写
客户端端写入本数据中心的Cache,同时异步双写到另外一个数据中心的Cache。如果写入一个DC成功,另外一个DC失败,那数据可能会不一致。为了保证一致,可能需要先写入一个队列,然后再将队列的数据发送到两个IDC。
如果队列是本地队列,那当前服务器挂掉,数据可能会丢失;如果队列是远程队列,又给整体的方案带来了很大的复杂度。
2、proxy
proxy模式解决了多客户端写可能会导致数据不一致的问题。客户端和Cache之间增加Proxy层,读请求直接走Cache,写请求走Proxy,保证读请求的低延迟,Proxy将请求转发给本数据中心的cache后,异步复制写入消息队列,复制到另外一个数据中心。
三、缓存的使用规范
1、Key设计
key的命名规范,业务名:库名:表名:其他,以冒号分割
合理控制key的长度
key中不要包含特殊字符,比如包含空格、换行、单双引号以及其他转义字符
尽可能对热点key进行拆分,均摊单个节点cpu压力
2、Value设计
拒绝Bigkey,String类型大小最大控制在1KB左右。文章正文,建议不要存入redis,用文档型数据库MongoDB代替或者直接缓存到CDN上等方式优化,hash、list、set、zset、元素个数不要超过5000。
非字符串的bigkey,不要使用del删除,使用hscan、sscan、zscan方式渐进式删除
要注意防止bigkey过期时间自动删除问题(例如一个200万的zset设置一小时过期,会触发del操作,造成阻塞)
数据写入时必须设置ttl,设置合理的过期时间,选择合理的过期策略
通常来说找到redis中的big key有如下几种方法:
redis-cli自带–bigkeys,例如:redis-cli -h -a --bigkeys
获取生产Redis的rdb文件,通过rdbtools分析rdb生成csv文件,再导入MySQL或其他数据库中进行分析统计,根据size_in_bytes统计bigkey
通过python脚本,迭代scan key,每次scan 1000,对扫描出来的key进行类型判断,例如:string长度大于10K,list长度大于10240认为是big bigkeys
其他第三方工具,例如:redis-rdb-cli
地址:https://github.com/leonchen83/redis-rdb-cli
3、命令使用
禁止线上使用keys、flushall、flushdb、shutdown、monitor、save等
hgetall、lrange、smembers、zrange等命令并非不能用,但是需要明确N的值
有遍历的需求可以使用hscan、sscan、zscan代替
四、缓存的使用场景
1、原样缓存
这种场景是最常见的场景,也是很多架构使用缓存的适合,最先涉及到的场景。
基本就是数据库里面啥样,我缓存也啥样,数据库里面有商品信息,缓存里面也放商品信息,唯一不同的是,数据库里面是全量的商品信息,缓存里面是最热的商品信息。
每当应用要查询商品信息的时候,先查缓存,缓存没有就查数据库,查出来的结果放入缓存,从而下次就查到了。
这个是缓存最最经典的更新流程。这种方式简单,直观,很多缓存的库都默认支持这种方式。
2、列表排序分页缓存
有时候我们需要获得一些列表数据,并对这些数据进行排序和分页。
例如我们想获取点赞最多的评论,或者最新的评论,然后列出来,一页一页的翻下去。
在这种情况下,缓存里面的数据结构和数据库里面完全不一样。
如果完全使用数据库进行实现,则按照某种条件将所有的行查询出来,然后按照某个字段进行排序,然后进行分页,一页一页的展示。
但是当数据量比较大的时候,这种方式往往成为瓶颈,首先涉及的数据库行数比较多,而且排序也是个很慢的活,尽管可能有索引,分页也是翻页到最后,越是慢。
在缓存里面,就没必要每行一个key了,而是可以使用Redis的列表方式进行存储,当然列表的长短是有限制的,肯定放不下数据库里面这么多,但是大家会发现其实对于所有的列表,用户往往没有耐心看个十页八页的,例如百度上搜个东西,也是有排序和分页的,但是你每次都往后翻了吗,每页就十条,就算是十页,或者一百页,也就一千条数据,如果保持ID的话,完全放的下。
如果已经排好序,放在Redis里面,那取出列表,翻页就非常快了。
可以后台有一个线程,异步的初始化和刷新缓存,在缓存里面保存一个时间戳,当有更新的时候,刷新时间戳,异步任务发现时间戳改变了,就刷新缓存。
3、计数缓存
计数对于数据库来讲,是一个非常繁重的工作,需要查询大量的行,最后得出计数的结论,当数据改变的时候,需要重新刷一遍,非常影响性能。
因此可以有一个计数服务,后端是一个缓存,将计数作为结果放在缓存里面,当数据有改变的时候,调用计数服务增加或者减少计数,而非通过异步数据库count来更新缓存。
计数服务可以使用Redis进行单个计数,或者hash表进行批量计数
4、重构维度缓存
有时候数据库里面保持的数据的维度是为了写入方便,而非为了查询方便的,然而同时查询过程,也需要处理高并发,因而需要为了查询方便,将数据重新以另一个维度存储一遍,或者说将多给数据库的内容聚合一下,再存储一遍,从而不用每次查询的时候都重新聚合,如果还是放在数据库,比较难维护,放在缓存就好一些。
例如一个商品的所有的帖子和帖子的用户,以及一个用户发表过的所有的帖子就是属于两个维度。
这需要写入一个维度的时候,同时异步通知,更新缓存中的另一个维度。
在这种场景下,数据量相对比较大,因而单纯用内存缓存memcached或者redis难以支撑,往往会选择使用levelDB进行存储,如果levelDB的性能跟不上,可以考虑在levelDB之前,再来一层memcached。
5、较大的内容数据缓存
对于评论的详情,或者帖子的详细内容,属于非结构化的,而且内容比较大,因而使用memcached比较好。
五、缓存问题
1、缓存实时性和一致性问题
虽然使用了缓存,但是实时性和一致性得不到完全的保证,毕竟数据保存了多份,数据库一份,缓存中一份,当数据库中因写入而产生了新的数据,往往缓存是不会和数据库操作放在一个事务里面的,如何将新的数据更新到缓存里面,什么时候更新到缓存里面读取的过程,应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。如果命中,应用程序从cache中取数据,取到后返回,不同的策略不一样。
2、缓存的穿透问题
可能读取的是冷数据,原来从来没有访问过,所以需要到数据库里面查询一下,然后放入缓存,再返回给客户。
可能数据因为有了写入,被实时的从缓存中删除了,就如第一个问题中描述的那样,为了保证实时性,当数据库中的数据更新了之后,马上删除缓存中的数据,导致这个时候的读取读不到,需要到数据库里面查询后,放入缓存,再返回给客户。
可能是缓存实效了,每个缓存数据都会有实效时间,过了一段时间没有被访问,就会失效,这个时候数据就访问不到了,需要访问数据库后,再放入缓存。
数据被换出,由于缓存内存是有限的,当使用快满了的时候,就会使用类似LRU策略,将不经常使用的数据换出,所以也要访问数据库。
后端确实也没有,应用访问缓存没有,于是查询数据库,结果数据库里面也没有,只好返回客户为空,但是尴尬的是,每次出现这种情况的时候,都会面临着一次数据库的访问,纯属浪费资源,常用的方法是,讲这个key对应的结果为空的事实也进行缓存,这样缓存可以命中,但是命中后告诉客户端没有,减少了数据库的压力。
3、缓存的刷新策略
(1)实时策略
读取的过程,应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。如果命中,应用程序从cache中取数据,取到后返回。
写入的过程,把数据存到数据库中,成功后,再让缓存失效,失效后下次读取的时候,会被写入缓存。
(2)异步策略
当读取的时候读不到的时候,不直接访问数据库,而是返回一个fallback数据,然后往消息队列里面放入一个数据加载的事件,在背后有一个任务,收到事件后,会异步的读取数据库,由于有队列的作用,可以实现消峰,缓冲对数据库的访问,甚至可以将多个队列中的任务合并请求,合并更新缓存,提高了效率。
当更新的时候,异步策略总是先更新数据库和缓存中的一个,然后异步的更新另一个。
一是先更新数据库,然后异步更新缓存。当数据库更新后,同样生成一个异步消息,放入消息队列中,等待背后的任务通过消息进行缓存更新,同样可以实现消峰和任务合并。缺点就是实时性比较差,估计要过一段时间才能看到更新,好处是数据持久性可以得到保证。
一是先更新缓存,然后异步更新数据库。这种方式读取和写入都用缓存,将缓存完全挡在了数据库的前面,把缓存当成了数据库在用。所以一般会使用有持久化机制和主备的redis,但是仍然不能保证缓存不丢数据,所以这种情况适用于并发量大,但是数据没有那么关键的情况,好处是实时性好。
在实时策略扛不住大促的时候,可以根据场景,切换到上面的两种模式的一个,算是降级策略
(3)定时策略
如果并发量实在太大,数据量也大的情况,异步都难以满足,可以降级为定时刷新的策略,这种情况下,应用只访问缓存,不访问数据库,更新频率也不高,而且用户要求也不高,例如详情,评论等。
这种情况下,由于数据量比较大,建议将一整块数据拆分成几部分进行缓存,而且区分更新频繁的和不频繁的,这样不用每次更新的时候,所有的都更新,只更新一部分。并且缓存的时候,可以进行数据的预整合,因为实时性不高,读取预整合的数据更快。