一、介绍
- 基于内存的key-value数据库,消除了磁盘I/0的影响
- 定期异步操作把数据库数据flush到硬盘上进行保存。每秒可以处理超过 10万次读写;
- redis是单进程单线程的
- 单个value的最大限制是1GB ,memcached的value最大为1M
二、优缺点
- 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
- 支持丰富数据类型,支持string,list,set,sorted set,hash
- 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行
- 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除
缺点:数据库容量受到物理内存的限制,不能用作海量数据的高性能读写
三、与memcached的区别
- 存储方式: Memecache把数据全部存在内存之中,断电后会挂掉。 Redis有部份存在硬盘上,这样能保证数据的持久性。
- 数据类型: Memcache对数据类型支持相对简单。 Redis有复杂的数据类型。
- 使用底层模型不同 它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。 Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求
四、常见问题及解决方法
-
Master不要做持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务。如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。Master 进行AOF持久化,AOF文件会不断增大,AOF文件过大会影响Master重启的恢复速度。
-
Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象
-
Redis主从复制的性能问题,为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内
五、影响key生存时间的操作
- DEL 命令:删除key
- SET 和 GETSET 命令覆盖原来的数据
- PERSIST命令可以在不删除 key 的情况下,移除 key 的生存时间,让 key 重新成为一个persistent key
- EXPIRE命令,指定生存时间。设置成功返回 1;当 key 不存在或者不能为 key 设置生存时间时,返回 0 。TTL可以查看key的当前生存时间
注意事项:
1)RENAME对一个 key 进行改名,那么改名后的 key的生存时间和改名前一样。
2)对一个 key 执行INCR命令,对一个列表进行LPUSH命令,或者对一个哈希表执行HSET命令,这类操作都不会修改 key 本身的生存时间
六、保证存储的数据是热点数据的方法
redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略(回收策略),包括6中策略:
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-enviction(驱逐):禁止驱逐数据
使用策略规则:
1、如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用allkeys-lru
2、如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用allkeys-random
七、Redis的并发竞争问题的解决
Redis为单进程单线程模式,采用队列模式将并发访问变为串行访问。Redis对于多个客户端连接并不存在竞争,但是在Jedis客户端对Redis进行并发访问时会发生连接超时、数据转换错误、阻塞、客户端关闭连接等问题,这些问题均是由于客户端连接混乱造成。对此有2种解决方法:
1.客户端角度,为保证每个客户端间正常有序与Redis进行通信,对连接进行池化,同时对客户端读写Redis操作采用内部锁synchronized或者lock。
2.服务器角度,利用setnx命令(SET if Not eXists)实现分布式锁。
setnx实现锁需要注意的问题:
- 需要expire设置过期时间,以免意外导致锁没有删除,程序一直在执行锁住的内容,比如一直在刷新操作,达不到缓存实现。同时这两个操作要用Multi/Exec 包裹起来,确保原子性操作。
- 但当同时多个请求时,以上操作也可能存在问题,SetNX只有一个请求能成功,但expire会被多个请求执行,导致不会过期,这时可以有条件地执行expire操作。以上操作,可以通过set来完成。从2.6.12 起,SET 涵盖了 SETEX 的功能,并且 SET 本身已经包含了设置过期时间的功能
- 通过set操作,也要注意,比如更新缓存的时间比较长,超过锁的有效期,导致在缓存更新过程中,锁就失效了。可以通过set创建锁的时候需要引入一个随机值解决
八、Redis的事务特征
-
redis事务通过MULTI/EXEC/DISCARD/WATCH四个命令实现,通过MULTI命令开启一个事务,关系型数据库中可理解为"BEGIN TRANSACTION"语句。结束事务通过执行EXEC/DISCARD命令来提交/回滚该事务内的所有操作。等同于关系型数据库中的COMMIT/ROLLBACK语句。WATCH命令可用于提供CAS(check-and-set)乐观锁功能。通过WATCH命令在事务执行之前监控了多个Keys,倘若在WATCH之后有任何Key的值发生了变化,EXEC命令执行的事务都将被放弃,同时返回Null multi-bulk(空多条批量回复)应答以通知调用者事务执行失败。
-
在事务开启之前,如果客户端与服务器之间出现通讯故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。然而如果网络中断事件是发生在客户端执行EXEC命令之后,那么该事务中的所有命令都会被服务器执行
-
事务执行期间,Redis不会再为其它客户端的请求提供任何服务,从而保证了事物中的所有命令被原子的执行。在事务中的所有命令都将会被串行化的顺序执行
-
和关系型数据库中的事务相比,在Redis事务中如果有某一条命令执行失败,其后的命令仍然会被继续执行。
九、乐观锁与悲观锁
乐观锁:就是利用版本号比较机制,只是在读数据的时候,将读到的数据的版本号一起读出来,当对数据的操作结束后,准备写数据的时候,再进行一次数据版本号的比较,若版本号没有变化,即认为数据是一致的,没有更改,可以直接写入,若版本号有变化,则认为数据被更新,不能写入,防止脏写
悲观锁:整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
十、Redis持久化的方式
1、快照(snapshots):默认方式。将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。
save 900 1 #900秒内如果超过1个key被修改,则发起快照保存
save 300 10 #300秒内容如超过10个key被修改,则发起快照保存
save 60 10000
自动实现快照:
1、redis使用fork函数生成一个子进程
2、父进程继续接收并处理客户端发来的命令,而子进程开始将内存中的数据写入硬盘中的临时文件
3、当子进程写入完所有数据后会用该临时文件替换旧的RDB文件,至此,一次快照操作完成
手动实现快照:
1、执行bgsave命令(此时redis会fork一个子进程,子进程负责生成硬盘文件,父进程负责继续接受命令)
2、执行save命令(和bgsave命令不同,发送save命令后,到系统创建快照完成之前系统不会再接收新的命令,换句话说save命令会阻塞后面的命令,而bgsave不会)
注意事项:
1)每次快照持久化都是将内存数据完整写入到磁盘一次,并不是增量的只同步脏数据。
2)另外由于快照方式是在一定间隔时间做一次的,所以如果redis意外down掉的话,就会丢失最后一次快照后的所有修改。如果应用要求不能丢失任何修改的话,可以采用aof持久化方式
rdb的优缺点:
优点:由于存储的有数据快照文件,恢复数据很方便。
缺点:会丢失最后一次快照以后更改的所有数据
2、AOF:redis会将每一个收到的写命令都通过write函数追加到文件中(默认是appendonly.aof)。当redis重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。当然由于os会在内核中缓存 write做的修改,所以可能不是立即写到磁盘上。这样aof方式的持久化也还是有可能会丢失部分修改。不过可以通过配置文件告诉redis我们想要通过fsync函数强制os写入到磁盘的时机。有如下三种方式
appendonly yes #启用aof持久化方式
#appendfsync always #每次收到写命令就立即强制写入磁盘,最慢的,但是保证完全的持久化,不推荐使用
#appendfsync everysec #默认方式。每秒钟强制写入磁盘一次,在性能和持久化方面做了很好的折中,推荐
#appendfsync no #完全依赖os,性能最好,持久化没保证
注意事项:
1)aof 的方式会导致持久化文件会变的越来越大。例如我们调用incr test命令100次,文件中必须保存全部的100条命令,其实有99条都是多余的。因为要恢复数据库的状态其实文件中保存一条set test 100就够了。
2) 为了压缩aof的持久化文件。redis提供了bgrewriteaof命令。收到此命令redis将使用与快照类似的方式将内存中的数据以命令的方式保存到临时文件中,最后替换原来的文件。
3)重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似
4)使用Append-Only模式时,Redis会通过调用系统函数write将该事务内的所有写操作在本次调用中全部写入磁盘。然而如果在写入的过程中出现系统崩溃,如电源故障导致的宕机,那么此时也许只有部分数据被写入到磁盘,而另外一部分数据却已经丢失。Redis服务器会在重新启动时执行一系列必要的一致性检测,一旦发现类似问题,就会立即退出并给出相应的错误提示。此时,我们就要充分利用Redis工具包中提供的redis-check-aof工具,该工具可以帮助我们定位到数据不一致的错误,并将已经写入的部分数据进行回滚。修复之后我们就可以再次重新启动Redis服务器
3、虚拟内存:使用vm把不经常访问的数据从内存交换到磁盘中,从而腾出宝贵的内存空间用于其他需要访问的数据。
1)Redis没有使用os提供的虚拟内存机制而是实现了自己的虚拟内存机制的原因:
- os 的虚拟内存是已4k页面为最小单位进行交换的。而redis的大多数对象都远小于4k,所以一个os页面上可能有多个redis对象。而redis规定同一个页面只能保存一个对象
- os认为所有os页面是活跃的,redis的集合对象类型如list,set可能存在与多个os页面上,这些对象可能很少被访问到,这样只有内存真正耗尽时os才会交换页面。
- 使用redis的vm可以将被交换到磁盘的对象进行压缩,去除指针和对象元数据信息,这样redis的vm会比os vm少做很多io操作
2)VM相关配置:
slaveof 192.168.1.1 6379 #指定master的ip和端口
vm-enabled yes #开启vm功能
vm-swap-file /tmp/redis.swap #交换出来的value保存的文件路径/tmp/redis.swap
vm-max-memory 1000000 #redis使用的最大内存上限,超过上限后redis开始交换value到磁盘文件中
vm-page-size 32 #每个页面的大小32个字节
vm-pages 134217728 #最多使用在文件中使用多少页面,交换文件的大小 = vm-page-size * vm-pages
vm-max-threads 4 #用于执行value对象换入换出的工作线程数量,0表示不使用工作线程(后面介绍)
- vm-max-memory:在redis使用的内存没超过vm-max-memory之前是不会交换任何value的。当超过最大内存限制后,redis会选择较老的对象。如果两个对象一样老会优先交换比较大的对象,精确的公式swappability = age*log(size_in_memory)
- vm-page-size:redis规定同一个页面只能保存一个对象。但是一个对象可以保存在多个页面中,将页面的大小设置为可以容纳大多数对象的大小。太大了会浪费磁盘空间,太小了会造成交换文件出现碎片
- vm-pages:redis会在内存中对应一个1bit值来记录页面的空闲状态。所以像上面配置中页面数量(vm-pages 134217728 )会占用16M内存用来记录页面空闲状态
- vm-max-threads:用做交换任务的线程数量。如果大于0推荐设为服务器的cpu核数。如果是0则交换过程在主线程进行
当vm-max-threads设为0时(Blocking VM)
换出:主线程定期检查发现内存超出最大上限后,会直接已阻塞的方式,将选中的对象保存到swap文件中,并释放对象占用的内存,此过程会一直重复直到下面条件满足:
1.内存使用降到最大限制以下 2.swap文件满了 3.几乎全部的对象都被交换到磁盘了
换入:当有client请求value被换出的key时。主线程会以阻塞的方式从文件中加载对应的value对象,加载时此时会阻塞所有client。然后处理client的请求
当vm-max-threads大于0(Threaded VM)
换出:当主线程检测到使用内存超过最大上限,会将选中的要交换的对象信息放到一个队列中交由工作线程后台处理,主线程会继续处理client请求。
换入:如果有client请求的key被换出了,主线程先阻塞发出命令的client,然后将加载对象的信息放到一个队列中,让工作线程去加载。加载完毕后工作线程通知主线程。主线程再执行client的命令。这种方式只阻塞请求value被换出key的client
3)在Redis-2.4后虚拟内存功能已经不被支持了,原因如下:
1)重启太慢 2)保存数据太慢
3)上面两条导致 replication 太慢 4)代码过于复杂
4.diskstore:放弃了虚拟内存方式后选择的一种新的实现方式,也就是传统的B-tree的方式,目前还处于alpha版本
- 读操作,使用read through以及LRU方式。内存中不存在的数据从磁盘拉取并放入内存,内存中放不下的数据采用LRU淘汰
- 写操作,采用另外spawn一个线程单独处理,写线程通常是异步的,当然也可以把cache-flush-delay配置设成0,Redis尽量保证即时写入。但是在很多场合延迟写会有更好的性能,比如一些计数器用Redis存储,在短时间如果某个计数反复被修改,Redis只需要将最终的结果写入磁盘。由于写入会按key合并,因此和snapshot还是有差异,disk store并不能保证时间一致性。由于写操作是单线程,即使cache-flush-delay设成0,多个client同时写则需要排队等待,如果队列容量超过cache-max-memory Redis设计会进入等待状态,造成调用方卡住
- rdb 和新 diskstore 格式关系:rdb是传统Redis内存方式的存储格式,通过工具可以将rdb格式转换成diskstore格式。通过BGSAVE可以随时将diskstore格式另存为rdb格式,而且rdb格式还用于Redis复制以及不同存储方式之间的中间格式
十一、Redis的同步机制
Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将rdb文件全量同步到复制节点,复制节点接受完成后将rdb镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。
十二、Redis集群的原理
Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。
Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。
十三、应用场合
- 会话缓存:Redis提供持久化,一定程度不会数据丢失
- 全页缓存:因为有磁盘的持久化,用户也不会看到页面加载速度的下降
- 队列:Redis提供 list 和 set 操作
- 排行榜/计数器:Redis提供集合(Set)和有序集合(Sorted Set)
十四、问题总结
1)Redis时如何做持久化的?
bgsave做镜像全量持久化,aof做增量持久化。因为bgsave会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要aof来配合使用。在redis实例重启时,会使用bgsave持久化文件重新构建内存,再使用aof重放近期的操作指令来实现完整恢复重启之前的状态。但是当机器突然掉电会怎样?取决于aof日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。
2)bgsave的原理
fork和cow。fork是指redis通过创建子进程来进行bgsave操作,cow指的是copy on write
cow:在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程
3)Pipeline的好处
可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。但是注意,如果使用Pipeline。当节点个数扩充后,会导致长连接数目成倍数上涨。使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。
4)假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?
使用keys指令可以扫出指定模式的key列表,由于redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。
5)如何使用Redis做异步队列?
一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。
在不使用sleep时,list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。
6)如何生产一次消息消费多次?
使用pub/sub主题订阅者模,可以实现1:N的消息队列。但这种模式会导致在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等
7)redis如何实现延时队列?
使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理
8)如果有大量的key需要设置同一时间过期,需要注意什么?
如果大量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。一般需要在时间上加一个随机值,使得过期时间分散一些。
9)aof重写
是指把内存中的数据,逆化成命令,写入到.aof日志里.以解决 aof日志过大的问题.
10)恢复时rdb和aof哪个恢复的快
rdb快,因为其是数据的内存映射,直接载入到内存,而aof是命令,需要逐条执行,如果rdb和aof文件都在,优先使用aof恢复数据
11) 在dump rdb过程中,aof如果停止同步,会不会丢失?
不会,所有的操作缓存在内存的队列里,dump完成后统一操作