转自:https://www.jianshu.com/p/2f14bc570563
目录
- 概述
- Redis的数据结构和相关常用命令
- 数据持久化
- 内存管理与数据淘汰机制
- Pipelining
- 事务与Scripting
- Redis性能调优
- 主从复制与集群分片
- Redis Java客户端的选择
概述
Redis是一个开源的,基于内存的结构化数据存储媒介,可以作为数据库、缓存服务或消息服务使用。
Redis支持多种数据结构,包括字符串、哈希表、链表、集合、有序集合、位图、Hyperloglogs等。
Redis具备LRU淘汰、事务实现、以及不同级别的硬盘持久化等能力,并且支持副本集和通过Redis Sentinel实现的高可用方案,同时还支持通过Redis Cluster实现的数据自动分片能力。
Redis的主要功能都基于单线程模型实现,也就是说Redis使用一个线程来服务所有的客户端请求,同时Redis采用了非阻塞式IO,并精细地优化各种命令的算法时间复杂度,这些信息意味着:
- Redis是线程安全的(因为只有一个线程),其所有操作都是原子的,不会因并发产生数据异常
- Redis的速度非常快(因为使用非阻塞式IO,且大部分命令的算法时间复杂度都是O(1))
- 使用高耗时的Redis命令是很危险的,会占用唯一的一个线程的大量处理时间,导致所有的请求都被拖慢。(例如时间复杂度为O(N)的KEYS命令,严格禁止在生产环境中使用)
Redis的数据结构和相关常用命令
Key
Redis采用Key-Value型的基本数据结构,Key就是String类型的
String
底层是C++的一个以char数组为内部变量的一个结构体
struct sdshdr {
// 用于记录buf数组中使用的字节的数目
// 和SDS存储的字符串的长度相等
int len;
// 用于记录buf数组中没有使用的字节的数目
int free;
// 字节数组,用于储存字符串
char buf[];
};
List
底层是一个含双向链表的结构体。头尾操作时间复杂度为1,不能随机查找。
Hash
实现方式:数组+链表
为什么使用Hash而不使用序列化后的字符串呢?
1、获取其中某个值,不需要每次序列号所有内容。
2、更新或者插入更加方便。
3、有效地减少网络传输的消耗。
游标式的遍历:就是通过每次访问一个桶(数组的一个槽),并且返回下一个桶以及当前访问桶的所有数据。
Set
实现方式:数组+链表
Sorted Set
实现方式:跳表
数据持久化
RDB
Redis会定期保存数据快照至一个rdb文件。RDB持久化几乎不耗损Redis本身的性能,会fork子进程来实现。
默认的RDB策略。
save 900 1
save 300 10
save 60 10000
优点:
- 对性能影响最小
- 每次快照会生成一个完整的数据快照文件
- 使用RDB进行数据恢复比AOF快
缺点:
- 快照定期生成的,会丢失一部分数据
- 需要多核cpu
AOF
把每一个Redis请求记录在日志文件中。
AOF提供了三种fsync配置always/everysec/no,通过配置项[appendfsync]指定:
- appendfsync no:不进行fsync,将flush文件的时机交给OS决定,速度最快
- appendfsync always:每写入一条日志就进行一次fsync操作,数据安全性最高,但速度最慢
- appendfsync everysec:折中的做法,交由后台线程每秒fsync一次。
Rewrite:重写AOF,保留最小写操作集
优点:
- 安全,减少数据的丢失
- AOF文件容易读,可以定位到错误的某一条指令。
缺点:
- AOF文件较大
- 性能消耗较高
内存管理与数据淘汰机制
最大内存设置:
maxmemory 100mb
在内存占用达到了maxmemory后,再向Redis写入数据时,Redis会:
- 根据配置的数据淘汰策略尝试淘汰数据,释放空间。
- 如果没有数据可以淘汰,或者没有配置数据淘汰策略,那么Redis会对所有写请求返回错误,但读请求仍然可以正常执行。
在为Redis设置maxmemory时,需要注意:
- 如果采用了Redis的主从同步,主节点向从节点同步数据时,会占用掉一部分内存空间,如果maxmemory过于接近主机的可用内存,导致数据同步时内存不足。所以设置的maxmemory不要过于接近主机可用的内存,留出一部分预留用作主从同步。
数据淘汰机制:
Redis提供了5种数据淘汰策略:
- volatile-lru:使用LRU算法进行数据淘汰(淘汰上次使用时间最早的,且使用次数最少的key),只淘汰设定了有效期的key
- allkeys-lru:使用LRU算法进行数据淘汰,所有的key都可以被淘汰
- volatile-random:随机淘汰数据,只淘汰设定了有效期的key
- allkeys-random:随机淘汰数据,所有的key都可以被淘汰
- volatile-ttl:淘汰剩余有效期最短的key
一般来说,推荐使用的策略是volatile-lru,并辨识Redis中保存的数据的重要性。对于那些重要的,绝对不能丢弃的数据(如配置类数据等),应不设置有效期,这样Redis就永远不会淘汰这些数据。对于那些相对不是那么重要的,并且能够热加载的数据(比如缓存最近登录的用户信息,当在Redis中找不到时,程序会去DB中读取),可以设置上有效期,这样在内存不够时Redis就会淘汰这部分数据。
配置方法:
maxmemory-policy volatile-lru #默认是noeviction,即不进行数据淘汰
Pipelining
Pipelining
实现在一次交互中执行多条命令
Pipelining的局限性
Pipelining只能用于执行连续且无相关性的命令,当某个命令的生成需要依赖于前一个命令的返回时,就无法使用Pipelining了。
Scripting
Redis的事务可以确保复数命令执行时的原子性。通过MULTI和EXEC命令来把这两个命令加入一个事务中:
> MULTI
OK
> GET vCount
QUEUED
> SET vCount 0
QUEUED
> EXEC
1) 12384
2) OK
Redis在接收到MULTI命令后便会开启一个事务,这之后的所有读写命令都会保存在队列中但并不执行,直到接收到EXEC命令后,Redis会把队列中的所有命令连续顺序执行,并以数组形式返回每个命令的返回结果。
可以使用DISCARD命令放弃当前的事务,将保存的命令队列清空。
Redis事务不支持回滚
通过事务实现CAS
假设要实现将某个商品的状态改为已售:
if(exec(HGET stock:1001 state) == "in stock")
exec(HSET stock:1001 state "sold");
这一伪代码执行时,无法确保并发安全性,有可能多个客户端都获取到了”in stock”的状态,导致一个库存被售卖多次。
Redis提供了WATCH命令与事务搭配使用,实现CAS乐观锁的机制。
使用WATCH命令和事务可以解决这一问题:
exec(WATCH stock:1001);
if(exec(HGET stock:1001 state) == "in stock") {
exec(MULTI);
exec(HSET stock:1001 state "sold");
exec(EXEC);
}
WATCH的机制是:在事务EXEC命令执行时,Redis会检查被WATCH的key,只有被WATCH的key从WATCH起始时至今没有发生过变更,EXEC才会被执行。如果WATCH的key在WATCH命令到EXEC命令之间发生过变化,则EXEC命令会返回失败。
Scripting
可以让Redis执行LUA脚本。这就类似于RDBMS的存储过程一样。
Redis性能调优
针对Redis的性能优化,主要从下面几个层面入手:
- 最初的也是最重要的,确保没有让Redis执行耗时长的命令
- 使用pipelining将连续执行的命令组合执行
- 操作系统的Transparent huge pages功能必须关闭:
- 如果在虚拟机中运行Redis,可能天然就有虚拟机环境带来的固有延迟。可以通过./redis-cli –intrinsic-latency 100命令查看固有延迟。同时如果对Redis的性能有较高要求的话,应尽可能在物理机上直接部署Redis。
- 检查数据持久化策略
- 考虑引入读写分离机制
长耗时命令:
避免在使用这些O(N)命令时发生问题主要有几个办法:
- 不要把List当做列表使用,仅当做队列来使用
- 通过机制严格控制Hash、Set、Sorted Set的大小
- 可能的话,将排序、并集、交集等操作放在客户端执行
- 绝对禁止使用KEYS命令
- 避免一次性遍历集合类型的所有成员,而应使用SCAN类的命令进行分批的,游标式的遍历
网络引发的延迟
- 尽可能使用长连接或连接池,避免频繁创建销毁连接
- 客户端进行的批量数据操作,应使用Pipeline特性在一次交互中完成。
数据持久化引发的延迟
Redis的数据持久化工作本身就会带来延迟,需要根据数据的安全级别和性能要求制定合理的持久化策略:
- AOF + fsync always的设置虽然能够绝对确保数据安全,但每个操作都会触发一次fsync,会对Redis的性能有比较明显的影响
- AOF + fsync every second是比较好的折中方案,每秒fsync一次
- AOF + fsync never会提供AOF持久化方案下的最优性能
- 使用RDB持久化通常会提供比使用AOF更高的性能,但需要注意RDB的策略配置
- 每一次RDB快照和AOF Rewrite都需要Redis主进程进行fork操作。fork操作本身可能会产生较高的耗时,与CPU和Redis占用的内存大小有关。根据具体的情况合理配置RDB快照和AOF Rewrite时机,避免过于频繁的fork带来的延迟
Swap引发的延迟
当Linux将Redis所用的内存分页移至swap空间时,将会阻塞Redis进程,导致Redis出现不正常的延迟。Swap通常在物理内存不足或一些进程在进行大量I/O操作时发生,应尽可能避免上述两种情况的出现。
/proc//smaps文件中会保存进程的swap记录,通过查看这个文件,能够判断Redis的延迟是否由Swap产生。如果这个文件中记录了较大的Swap size,则说明延迟很有可能是Swap造成的。
数据淘汰引发的延迟
当同一秒内有大量key过期时,也会引发Redis的延迟。在使用时应尽量将key的失效时间错开。
引入读写分离机制
Redis的主从复制能力可以实现一主多从的多节点架构,在这一架构下,主节点接收所有写请求,并将数据同步给多个从节点。
在这一基础上,我们可以让从节点提供对实时性要求不高的读请求服务,以减小主节点的压力。
尤其是针对一些使用了长耗时命令的统计类任务,完全可以指定在一个或多个从节点上执行,避免这些长耗时命令影响其他请求的响应。
主从复制与集群分片
主从复制
Redis支持一主多从的主从复制架构。一个Master实例负责处理所有的写请求,Master将写操作同步至所有Slave。
借助Redis的主从复制,可以实现读写分离和高可用:
- 实时性要求不是特别高的读请求,可以在Slave上完成,提升效率。特别是一些周期性执行的统计任务,这些任务可能需要执行一些长耗时的Redis命令,可以专门规划出1个或几个Slave用于服务这些统计任务
- 借助Redis Sentinel可以实现高可用,当Master crash后,Redis Sentinel能够自动将一个Slave晋升为Master,继续提供服务。
当Slave启动后,会从Master进行一次冷启动数据同步,由Master触发BGSAVE生成RDB文件推送给Slave进行导入,导入完成后Master再将增量数据通过Redis Protocol同步给Slave。之后主从之间的数据便一直以Redis Protocol进行同步。
使用Sentinel做自动failover
Redis的主从复制功能本身只是做数据同步,并不提供监控和自动failover能力,要通过主从复制功能来实现Redis的高可用,还需要引入一个组件:Redis Sentinel
Redis Sentinel是Redis官方开发的监控组件,可以监控Redis实例的状态,通过Master节点自动发现Slave节点,并在监测到Master节点失效时选举出一个新的Master,并向所有Redis实例推送新的主从配置。
Redis Sentinel需要至少部署3个实例才能形成选举关系。
Redis Sentinel实现的自动failover不是在同一个IP和端口上完成的,也就是说自动failover产生的新Master提供服务的IP和端口与之前的Master是不一样的,所以要实现HA,还要求客户端必须支持Sentinel,能够与Sentinel交互获得新Master的信息才行。
集群分片
为何要做集群分片:
- Redis中存储的数据量大,一台主机的物理内存已经无法容纳
- Redis的写请求并发量大,一个Redis实例以无法承载
Redis Cluster的能力
- 能够自动将数据分散在多个节点上
- 当访问的key不在当前分片上时,能够自动将请求转发至正确的分片
- 当集群中部分节点失效时仍能提供服务
其中第三点是基于主从复制来实现的,Redis Cluster的每个数据分片都采用了主从复制的结构,原理和前文所述的主从复制完全一致,唯一的区别是省去了Redis Sentinel这一额外的组件,由Redis Cluster负责进行一个分片内部的节点监控和自动failover。
Redis Cluster分片原理
Redis Cluster中共有16384个hash slot,Redis会计算每个key的CRC16,将结果与16384取模,来决定该key存储在哪一个hash slot中,同时需要指定Redis Cluster中每个数据分片负责的Slot数。Slot的分配在任何时间点都可以进行重新分配。
客户端在对key进行读写操作时,可以连接Cluster中的任意一个分片,如果操作的key不在此分片负责的Slot范围内,Redis Cluster会自动将请求重定向到正确的分片上。
hash tags
在基础的分片原则上,Redis还支持hash tags功能,以hash tags要求的格式的key,将会确保进入同一个Slot中。例如:{uiv}user:1000和{uiv}user:1001拥有同样的hash tag {uiv},会保存在同一个Slot中。
使用Redis Cluster时,pipelining、事务和LUA Script功能涉及的key必须在同一个数据分片上,否则将会返回错误。如要在Redis Cluster中使用上述功能,就必须通过hash tags来确保一个pipeline或一个事务中操作的所有key都位于同一个Slot中。
有一些客户端(如Redisson)实现了集群化的pipelining操作,可以自动将一个pipeline里的命令按key所在的分片进行分组,分别发到不同的分片上执行。但是Redis不支持跨分片的事务,事务和LUA Script还是必须遵循所有key在一个分片上的规则要求。
主从复制 vs 集群分片
在设计软件架构时,要如何在主从复制和集群分片两种部署方案中取舍呢?
从各个方面看,Redis Cluster都是优于主从复制的方案
- Redis Cluster能够解决单节点上数据量过大的问题
- Redis Cluster能够解决单节点访问压力过大的问题
- Redis Cluster包含了主从复制的能力
那是不是代表Redis Cluster永远是优于主从复制的选择呢?
并不是
软件架构永远不是越复杂越好,复杂的架构在带来显著好处的同时,一定也会带来相应的弊端。采用Redis Cluster的弊端包括: - 维护难度增加。在使用Redis Cluster时,需要维护的Redis实例数倍增,需要监控的主机数量也相应增加,数据备份/持久化的复杂度也会增加。同时在进行分片的增减操作时,还需要进行reshard操作,远比主从模式下增加一个Slave的复杂度要高。
- 客户端资源消耗增加。当客户端使用连接池时,需要为每一个数据分片维护一个连接池,客户端同时需要保持的连接数成倍增多,加大了客户端本身和操作系统资源的消耗。
- 性能优化难度增加。你可能需要在多个分片上查看Slow Log和Swap日志才能定位性能问题。
- 事务和LUA Script的使用成本增加。在Redis Cluster中使用事务和LUA Script特性有严格的限制条件,事务和Script中操作的key必须位于同一个分片上,这就使得在开发时必须对相应场景下涉及的key进行额外的规划和规范要求。如果应用的场景中大量涉及事务和Script的使用,如何在保证这两个功能的正常运作前提下把数据平均分到多个数据分片中就会成为难点。
所以说,在主从复制和集群分片两个方案中做出选择时,应该从应用软件的功能特性、数据和访问量级、未来发展规划等方面综合考虑,只在确实有必要引入数据分片时再使用Redis Cluster。
下面是一些建议:
- 需要在Redis中存储的数据有多大?未来2年内可能发展为多大?这些数据是否都需要长期保存?是否可以使用LRU算法进行非热点数据的淘汰?综合考虑前面几个因素,评估出Redis需要使用的物理内存。
- 用于部署Redis的主机物理内存有多大?有多少可以分配给Redis使用?对比(1)中的内存需求评估,是否足够用?
- Redis面临的并发写压力会有多大?在不使用pipelining时,Redis的写性能可以超过10万次/秒(更多的benchmark可以参考 https://redis.io/topics/benchmarks )
- 在使用Redis时,是否会使用到pipelining和事务功能?使用的场景多不多?
综合上面几点考虑,如果单台主机的可用物理内存完全足以支撑对Redis的容量需求,且Redis面临的并发写压力距离Benchmark值还尚有距离,建议采用主从复制的架构,可以省去很多不必要的麻烦。同时,如果应用中大量使用pipelining和事务,也建议尽可能选择主从复制架构,可以减少设计和开发时的复杂度。
Redis Java客户端的选择
Redis的Java客户端很多,官方推荐的有三种:Jedis、Redisson和lettuce。
在这里对Jedis和Redisson进行对比介绍
Jedis:
- 轻量,简洁,便于集成和改造
- 支持连接池
- 支持pipelining、事务、LUA Scripting、Redis Sentinel、Redis Cluster
- 不支持读写分离,需要自己实现
- 文档差(真的很差,几乎没有……)
Redisson: - 基于Netty实现,采用非阻塞IO,性能高
- 支持异步请求
- 支持连接池
- 支持pipelining、LUA Scripting、Redis Sentinel、Redis Cluster
- 不支持事务,官方建议以LUA Scripting代替事务
- 支持在Redis Cluster架构下使用pipelining
- 支持读写分离,支持读负载均衡,在主从复制和Redis Cluster架构下都可以使用
- 内建Tomcat Session Manager,为Tomcat 6/7/8提供了会话共享功能
- 可以与Spring Session集成,实现基于Redis的会话共享
- 文档较丰富,有中文文档
缓存穿透、缓存雪崩和缓存击穿
转载:https://baijiahao.baidu.com/s?id=1619572269435584821&wfr=spider&for=pc
缓存穿透
是指查询一个数据库一定不存在的数据。一般只会缓存数据库存在的值,所以会造成缓存穿透。大量的访问到达,直接穿透到数据库。
解决方法:
- 布隆过滤。对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃。还有最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
- 缓存空值,可以为其设置较短的过期时间。缓存空对象会有两个问题:
第一,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间 ( 如果是攻击,问题更严重 ),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
第二,缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为 5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。
布隆过滤
- 首先需要k个hash函数,每个函数可以把key散列成为1个整数
- 初始化时,需要一个长度为n比特的数组,每个比特位初始化为0
- 某个key加入集合时,用k个hash函数计算出k个散列值,并把数组中对应的比特位置为1
- 判断某个key是否在集合时,用k个hash函数计算出k个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为在集合中。
缓存雪崩(热点数据集中失效)
是指在某一个时间段,缓存集中过期失效;或者是缓存服务器宕机。
解决方法:
1、加锁或者队列来控制读数据库写缓存的线程数量
2、为失效时间添加一个随机因子;
3、主从复制或者集群
4、做二级缓存,或者双缓存策略。
缓存击穿
缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
解决方法:对于热点数据不设置过期时间