当前位置 : 首页 » 文章分类 :  开发  »  Java面试准备-(06)Redis和缓存

Java面试准备-(06)Redis和缓存

面试准备之redis和缓存


Redis

Redis 命令参考 - redisfans
http://doc.redisfans.com/

Redis中文官网 - redis文档中心
http://www.redis.cn/documentation.html

redis中文网 - redis教程
http://www.redis.net.cn/tutorial/3501.html

随笔分类 - redis
http://www.cnblogs.com/xiaoxi/category/961351.html

redis面试总结
https://www.cnblogs.com/jiahaoJAVA/p/6244278.html

redis是一个高性能的key-value非关系数据库,它可以存键(key)与5种不同类型的值(value)之间的映射(mapping),支持存储的value类型包括:String(字符串)、list(链表)、set(集合)、zset(有序集合)和hash(散列表)

这么说吧,redis配置文件中的每条命令,redis的每条操作指令,都可以衍生出一系列问题:
1、这个配置项/指令是干什么的?
2、内部实现机制是什么?
3、有哪些可选的参数?
4、分别是什么意思?
5、不同配置参数的优劣?在什么情况下使用?


Redis数据类型

列表list(双向链表,有序)

列表(list)类型是用来存储多个字符串,元素从左到右组成一个有序的集合.列表中的每个字符串被称为元素(element),一个列表最多可以存储(2的32次方)-1个元素.在redis中,可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定所有下标的元素等.

列表类型有两个特点:
1、列表中的元素是有序的,这就意味着可以通过索引下标获取某个元素或者某个范围内的元素列表.
2、列表中的元素可以是重复的.

操作命令

插入

从右边插入元素.  rpush key value [value…]
从左边插入元素.  lpush key value [value….]  使用方法与rpush一样,从左侧插入.   

查询

(1) 查询指定范围内的元素列表  lrange key start end  
lrange操作会获取列表指定索引范围所有的元素.索引下标有两个特点:第一,索引下标从左到右分别是0到N-1,但是从右到左分别是-1到-N.第二,lrange中的end选项包含了自身.

(2) 获取列表指定索引下的元素  lindex key index

(3) 获取列表长度  llen key

删除

(1) 从列表左侧或右侧弹出元素.  lpop key  rpop key  将列表最左侧与右侧的元素弹出来.

(2) 删除指定元素  lrem key count value
lrem命令会从列表中找到等于value的元素进行删除,根据count的不同分为三种:
count>0,从列表中删除指定数量(count)的元素.
count<0,从列表中删除count绝对值数量的元素.
count=0,删除所有.

(3) 按照索引范围修剪列表  ltrim key start end

修改

修改指定索引下标的元素:  lset key index value

阻塞操作

阻塞式弹出:   blpop key [key…] timeout  brpop key [key…] timeout
blpop与brpop命令是lpop和rpop命令的阻塞版本,他除了弹出方向不同,使用方法基本相同,所以下面以brpop命令进行说明,
brpop命令包含两个参数:
1)列表为空:如果timeout等于3,那么客户端等到三秒后返回,如果timeout=0,那么客户端将一直阻塞,直到弹出成功.
2)列表不为空:客户端会立刻返回.

在使用阻塞弹出命令时,有两点需要注意.
第一点:如果是多个键,那么会从左到右遍历键,一旦有一个键能弹出元素客户端就会立刻返回.
第二点:如果多个客户端同时对一个键进行操作,那么最先执行命令的客户端可以获取到值.

内部数据结构

列表类型的内部编码有两种:
1、ziplist(压缩列表):当列表的元素个数大于list-max-ziplist-entries配置(默认为512个),同时列表中每个元素的长度小于list-max-ziplist-value配置(默认为64字节).

2、linkedlist(链表):当列表的长度或值得大小不满足ziplist的要求,redis会采用linkedlist为列表的内部实现编码.

使用场景(阻塞队列)

1、消息队列:redis的lpush-brpop命令组合即可实现阻塞队列,生产者客户端使用lpush命令向列表插入元素.消费者客户端使用brpop命令阻塞式的”抢”列表中的尾部元素.多个客户端保证消息的负载均衡与可用性.

2、文章列表:每个用户都有属于自己的文章列表.此时可以考虑使用列表,因为列表不但是有序的,同时支持使用lrange按照索引范围获取多个元素.

3、开发提示:列表的使用场景有很多如: lpush+lpop=Stack(栈)、lpush+rpop=queue(队列)、lpush+brpop=message queue、lpush+ltrim=Capped Collection(有限集合)

redis有序集合性能 列表、集合、有序集合
https://blog.csdn.net/ttomqq/article/details/78548489


集合set(无序,不可重复)

集合(set)类型也是用来保存多个的字符串元素,但和列表不同的是:它的元素是无序且不可重复的,不能通过索引获取元素

操作命令

集合内操作

(1) 添加元素  sadd key value [value…]  返回结果为添加成功的元素数量.

(2) 删除元素  srem key value [value…]  返回结果为删除成功的元素数量.

(3) 获取元素个数  scard key

(4) 判断元素是否在集合中  sismember key value

(5) 随机从集合中返回指定个数元素  srandmember key [count]  [count]是可选参数,如果不写默认为:1.

(6) 从集合中随机弹出元素  spop key  spop操作可以从集合中随机弹出一个元素.

(7) 获取集合的所有元素  smembers key  获取集合所有元素,且返回结果是无序的.

集合间操作

(1) 求多个集合的交集  sinter key [key…]

(2) 求多个集合的并集  sunion key [key…]

(3) 求多个集合的差集  sdiff key [key…]

(4) 将交集、并集、差集的结果保存.
sinterstore storeKey key [key…]
sunionstore storeKey key [key…]
sdiffstore storeKey key [key…]
集合间的运算在元素比较多的情况下会比较耗时,所以redis提供了上面三个命令(原命令+store)将集合间交集、并集、差集的结果保存到storeKey中,例如将user:1:follows和user:2:follows两个集合之间的交集结果保存到user:1_2:follows中

内部数据结构

集合类型的内部编码有两种:
1、intset(整数集合)
当集合中的元素全是整数,且长度不超过set-max-intset-entries(默认为512个)时,redis会选用intset作为内部编码.

2、hashtable(哈希表)
当集合无法满足intset的条件时,redis会使用hashtable作为内部编码.

使用场景(用户标签)

集合类型比较典型的使用场景是标签(tag).例如一个用户可能对音乐感兴趣,另一个用户对新闻感兴趣,这些想去点就是标签.有了这些数据就可以获得喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于用户体验来说比较重要.

redis有序集合性能 列表、集合、有序集合
https://blog.csdn.net/ttomqq/article/details/78548489


有序集合zset(按分值score排序)

有序集合相对于哈希、列表、集合来说会有一点陌生,但既然叫有序集合.那么它和集合必然是有着联系,它保留了集合不能重复元素的特性.但不同的是,有序集合是可排序的.但是他和列表使用索引下标进行排序依据不同的是,它给每个元素设置一个分数(score)作为排序的依据

集合内操作命令

添加成员zadd key score mem(logn复杂度)

(1) 添加成员  zadd key score member [score member …]
有关zadd命令有两点需要注意:  Redis 3.2为zadd命令添加了nx、xx、ch、incr四个选项:
nx:member必须不存在,才可以设置成功,用于添加.
xx:member必须存在,才可以设置成功,用于添加.
ch:返回此次操作后,有序集合元素和分数发生变化的个数.
incr: 对score进行添加操作,相当于后面介绍的zincrby.

有序集合相比集合提供了排序字段,但是也产生了代价,zadd的时间复杂度是O(log(n)),sadd的时间复杂度为O(1).

(2) 获取成员个数  zcard key

获取成员分数zscore key mem

(3) 获取某个成员的分数  zscore key member

获取成员排名zrank key mem

(4) 获取成员排名  zrank key member  zrevrank key member

(5) 删除成员  zrem key member [member…]  

(6) 增加成员分数  zincrby key score member

获取指定范围元素 zrange key start end

(7) 获取指定范围的元素  zrange key start end [withscores]  zrevrange key start end [withscores]
有序集合是按照分值排名的,zrange是由低到高返回,zrevrange反之,查询全部:zrange user:ranking 0 -1,加上withscores参数显示分数.

获取指定分值范围元素 zrangebyscore key min max

(8) 返回指定分数范围的成员  zrangebyscore key min max [withscores] [limit offset count]  zrevrangebyscore key min max [withscores] [limit offset count]

(9) 返回指定分数范围成员个数  zcount key min max

(10) 删除指定排名内的升序元素  zremrangebyrank key start end

(11) 删除指定分数范围的成员  zremrangebyscore key min max

集合间的操作命令

(1) 交集
zinterstore storeKey keyNum key [key …] [weights weight [weight…]] [aggregate sum|min|max]  
参数说明:
storeKey:交集计算结果保存到这个键下.
keyNum:需要做交集的键的个数.
key[key …]:需要做交集的键.
weights weight [weight…]:每个键的权重,在做交集计算时,每个键中的每个member的分值会和这个权重相乘,每个键的权重默认为1.
aggregate sum|min|sum:计算成员交集后,分值可以按照sum(和)、min(最小值)、max(最大值)做汇总.默认值为sum.

(2) 并集  
zunionstore storeKey keyNum key [key…] [weights weight [weight…]] [aggregate sum|min|max]  
该命令的所有参数和zinterstore是一致的,只不过做的是并集计算

内部数据结构(跳跃表skiplist)

1、ziplist(压缩列表)
当有序集合的元素小于zset-max-ziplist-entries配置(默认是128个),同时每个元素的值都小于zset-max-ziplist-value(默认是64字节)时,Redis会用ziplist来作为有序集合的内部编码实现,ziplist可以有效的减少内存的使用

2、skiplist(跳跃表)    
当ziplist的条件不满足时,有序集合将使用skiplist作为内部编码的实现,来解决此时ziplist造成的读写效率下降的问题.

redis有序集合性能 列表、集合、有序集合
https://blog.csdn.net/ttomqq/article/details/78548489


redis各数据类型的使用场景

string
计数器应用

list
取最新n个数据的操作
消息队列
删除与过滤
实时分析正在发生的情况,用于数据统计与防垃圾邮件

set
unique操作,获取某段时间说有数据的排重值
实时系统,反垃圾系统
共同好友,二度好友
利用唯一性,可以统计访问法网站的所有独立IP
好友推荐的时候,根据tag求交集,大于某个threshold就可推荐

hash
存储、读取、修改用户属性

sorted set
排行榜应用,取top n操作
需要精准设定过期时间的应用(时间戳作为score)
带有权重的元素,比如一个游戏的用户得分排行榜
过期项目处理,按照时间排序

redis有序集合性能 列表、集合、有序集合
https://blog.csdn.net/ttomqq/article/details/78548489


Redis内部数据结构

Redis 作为一个基于key/value的内存数据库,使用ANSI C语言实现,以其高性能和支持丰富的数据结构闻名于世,而其数据结构也是其高性能的基础。
在Redis内部,有非常多的数据结构:sds(简单动态字符串),list,intset(整数集合),hash(字典),zskiplist(跳跃表),ziplist(压缩表)等。
以redis3.2的正式版源码分析

sds简单动态字符串

Redis采用动态字符串的形式,用len记录长度,这样可以在O(1)的复杂度内获取字符串长度;根据不同的类型和字符串的长短,分别用不同类型的sdshdr,可以节约不少空间;将alloc和len分离,可以在一定的范围内节省分配内存所用的时间;在Redis中,运用了大量的指针移动技巧来获取void*对象,也提高了程序的运行效率。

list双向链表

Redis中,list的实现是一个双端链表,这样可以方便的获取其前后的节点值,方便之后对节点的查找
链表结点:

typedef struct listNode { /*节点*/
struct listNode *prev;
struct listNode *next;
void *value; /*value用函数指针类型,决定了value可以是sds,list,set,dict等类型*/
} listNode;

链表结构:

typedef struct list { /*链表结构*/
listNode *head; /*头节点*/
listNode *tail; /*尾节点*/
void *(*dup)(void *ptr); /*复制节点*/
void (*free)(void *ptr); /*释放节点*/
int (*match)(void *ptr, void *key);  /* 匹配节点,返回key值的index */
unsigned long len; /*记录链表的长度*/
} list;

intset整数集合

当一个集合元素只有整数并且数量元素不多的时候,可以选择用整数集合来作为其底层实现。

typedef struct intset { /*整数集合的数据结构*/
uint32_t encoding; //编码方式
uint32_t length;
int8_t contents[];
} intset;

contents数组,它存储集合中的内容,并且以从小到大的顺序排列,并保证其没有重复的元素。

Redis内部数据结构的实现
https://blog.csdn.net/a6833916180/article/details/51596013


dict字典(哈希表)

字典结构是整个Redis的核心数据结构,基本上是其内部结构的缩影。

typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;

dictEntry是最核心的字典结构的节点结构,它保存了key和value的内容;另外,next指针是为了解决hash冲突,字典结构的hash冲突解决方法是拉链法,对于hashcode重复的节点以链表的形式存储。

typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask; /*hash表的掩码,总是size-1,用于计算hash表的索引值*/
unsigned long used;
} dictht;

dictht是节点dictEntry的持有者,将dictEntry结构串起来,table就是hash表,其实dictEntry *table[]这样的书写方式更容易理解些,size就是table数组的长度,used标志已有节点的数目。

typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
int iterators; /* number of iterators currently running */
} dict;

dict是最外层的字典结构的接口形式,type标志类型,privdata标志其私有数据,dict持有两个dictht结构,一个用来存储数据,一个用来在rehash时使用,rehashidx标志是否正在rehash(因为Redis中rehash是一个渐近的过程,正在rehash的时候rehashidx记录rehash的阶段,否则为-1)。
注:rehash是一个为了让负载因子(load_factor=used/size)控制在一个合理的范围内而重新分配内存和扩展结构的过程。
iterators是一个迭代器,用于记录当前迭代的数目。

Redis内部数据结构的实现
https://blog.csdn.net/a6833916180/article/details/51596013


zskiplist跳跃表

跳表是一种实现起来很简单,单层多指针的链表,它查找效率很高,堪比优化过的二叉平衡树,且比平衡树的实现,简单的多的多。

跳表在Redis中仅仅作为zset(有序集合)的底层实现出现

zadd的时间复杂度是O(log(n))

ziplist压缩表

ziplist是一个编码后的列表,是由一系列特殊编码的连续内存块组成的顺序型数据结构,特殊的设计使得内存操作非常有效率,此列表可以同时存放字符串和整数类型,列表可以在头尾各边支持推加和弹出操作在O(1)常量时间,但是,因为每次操作涉及到内存的重新分配释放,所以加大了操作的复杂性 。

typedef struct zlentry {
//prevrawlen为上一个数据结点的长度,prevrawlensize为记录该长度数值所需要的字节数
unsigned int prevrawlensize, prevrawlen;
//len为当前数据结点的长度,lensize表示表示当前长度表示所需的字节数
unsigned int lensize, len;
//数据结点的头部信息长度的字节数
unsigned int headersize;
//编码的方式
unsigned char encoding;
//数据结点的数据(已包含头部等信息),以字符串形式保存
unsigned char *p;
} zlentry;

压缩表之所以成为压缩表,是因为它起到了一定的压缩功能,对于其他的数据结构为了快速定位,使用了大量的指针结构,这样对于长度较大的数据优势明显,但是对于长度非常小的数据,比如说一个表里的每一个数据长度都很短,但是数据量并不小,这样的话,就会出现大量的指针结构,造成内存浪费,而压缩表则分配了一块连续内存来存储,就避免了大量的指针结构,节省了内存。另外,ziplist也使用了动态分配内存的方法,也一定程度上避免了内存的浪费。

Redis内部数据结构的实现
https://blog.csdn.net/a6833916180/article/details/51596013


单进程单线程

Redis采用的是基于内存的采用的是单进程单线程模型的KV数据库,由C语言编写。官方提供的数据是可以达到100000+的qps。这个数据不比采用单进程多线程的同样基于内存的KV数据库Memcached差。
Redis快的主要原因是:
完全基于内存
数据结构简单,对数据操作也简单
使用多路 I/O 复用模型

多路复用io模型epoll

多路 I/O 复用模型是利用select、poll、epoll可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。

和Memcached不同,Redis并没有直接使用Libevent,而是自己完成了一个非常轻量级的对select、epoll、evport、kqueue这些通用的接口的实现。在不同的系统调用选用适合的接口,linux下默认是epoll。因为Libevent比较重更通用代码量也就很庞大,拥有很多Redis用不上的功能,Redis为了追求“轻巧”并且去除依赖,就选择自己去封装了一套。

Redis为什么是单线程的?
因为CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存或者网络带宽,既然单线程容易实现,而且CPU不会成为瓶颈,那就顺便成章的采用单线程的方案

如果万一CPU成为你的Redis瓶颈了,或者,你就是不想让服务器其他核闲置,那怎么办?
那也很简单,你多起几个Redis进程就好了。Redis是keyvalue数据库,又不是关系数据库,数据之间没有约束。只要客户端分清哪些key放在哪个Redis进程上就可以了。redis-cluster可以帮你做的更好

Redis为什么使用单进程单线程方式也这么快
https://www.cnblogs.com/syyong/p/6231326.html

Redis单线程架构
https://www.cnblogs.com/roxy/p/8094198.html


redis集群

redis集群分为服务端集群和客户端分片,redis3.0以上版本实现了集群机制,即服务端集群,3.0以下使用客户端分片(Sharding)。

redis3.0官方集群方案:服务端哈希槽

Redis Cluster是一种服务器Sharding技术,3.0版本开始正式提供。

Redis Cluster中,Sharding采用slot(槽)的概念,一共分成16384个槽,这有点儿类pre sharding思路。对于每个进入Redis的键值对,根据key进行散列,分配到这16384个slot中的某一个中。使用的hash算法也比较简单,就是CRC16后16384取模。

Redis集群中的每个node(节点)负责分摊这16384个slot中的一部分,也就是说,每个slot都对应一个node负责处理。当动态添加或减少node节点时,需要将16384个槽做个再分配,槽中的键值也要迁移。当然,这一过程,在目前实现中,还处于半自动状态,需要人工介入。

Redis集群,要保证16384个槽对应的node都正常工作,如果某个node发生故障,那它负责的slots也就失效,整个集群将不能工作。

为了增加集群的可访问性,官方推荐的方案是将node配置成主从结构,即一个master主节点,挂n个slave从节点。这时,如果主节点失效,Redis Cluster会根据选举算法从slave节点中选择一个上升为主节点,整个集群继续对外提供服务。这非常类似前篇文章提到的Redis Sharding场景下服务器节点通过Sentinel监控架构成主从结构,只是Redis Cluster本身提供了故障转移容错的能力。

Redis Cluster的新节点识别能力、故障判断及故障转移能力是通过集群中的每个node都在和其它nodes进行通信,这被称为集群总线(cluster bus)。它们使用特殊的端口号,即对外服务端口号加10000。例如如果某个node的端口号是6379,那么它与其它nodes通信的端口号是16379。nodes之间的通信采用特殊的二进制协议。

对客户端来说,整个cluster被看做是一个整体,客户端可以连接任意一个node进行操作,就像操作单一Redis实例一样,当客户端操作的key没有分配到该node上时,Redis会返回转向指令,指向正确的node,这有点儿像浏览器页面的302 redirect跳转。

Redis Cluster是Redis 3.0以后才正式推出,时间较晚,目前能证明在大规模生产环境下成功的案例还不是很多,需要时间检验。

Redis集群方案应该怎么做?
https://www.zhihu.com/question/21419897

集群教程(redis3.0官方集群方案)
http://doc.redisfans.com/topic/cluster-tutorial.html

redis3.0之前的集群方案:客户端sharding

Redis 3正式推出了官方集群技术,解决了多Redis实例协同服务问题。Redis Cluster可以说是服务端Sharding分片技术的体现,即将键值按照一定算法合理分配到各个实例分片上,同时各个实例节点协调沟通,共同对外承担一致服务。多Redis实例服务,比单Redis实例要复杂的多,这涉及到定位、协同、容错、扩容等技术难题。

redis3.0之前一般使用客户端分片(Sharding)来实现集群。

Redis Sharding可以说是Redis Cluster出来之前,业界普遍使用的多Redis实例集群方法。其主要思想是采用哈希算法将Redis数据的key进行散列,通过hash函数,特定的key会映射到特定的Redis节点上。这样,客户端就知道该向哪个Redis节点操作数据。

庆幸的是,java redis客户端驱动jedis,已支持Redis Sharding功能,即ShardedJedis以及结合缓存池的ShardedJedisPool。
Jedis的Redis Sharding实现具有如下特点:
1、采用一致性哈希算法(consistent hashing),将key和节点name同时hashing,然后进行映射匹配,采用的算法是MURMUR_HASH。采用一致性哈希而不是采用简单类似哈希求模映射的主要原因是当增加或减少节点时,不会产生由于重新匹配造成的rehashing。一致性哈希只影响相邻节点key分配,影响量小。
2.为了避免一致性哈希只影响相邻节点造成节点分配压力,ShardedJedis会对每个Redis节点根据名字(没有,Jedis会赋予缺省名字)会虚拟化出160个虚拟节点进行散列。根据权重weight,也可虚拟化出160倍数的虚拟节点。用虚拟节点做映射匹配,可以在增加或减少Redis节点时,key在各Redis节点移动再分配更均匀,而不是只有相邻节点受影响。
3.ShardedJedis支持keyTagPattern模式,即抽取key的一部分keyTag做sharding,这样通过合理命名key,可以将一组相关联的key放入同一个Redis节点,这在避免跨节点访问相关数据时很重要。

客户端sharding扩容方案-presharding

Redis Sharding采用客户端Sharding方式,服务端Redis还是一个个相对独立的Redis实例节点,没有做任何变动。同时,我们也不需要增加额外的中间处理组件,这是一种非常轻量、灵活的Redis多实例集群方法。

当然,Redis Sharding这种轻量灵活方式必然在集群其它能力方面做出妥协。比如扩容,当想要增加Redis节点时,尽管采用一致性哈希,毕竟还是会有key匹配不到而丢失,这时需要键值迁移。

作为轻量级客户端sharding,处理Redis键值迁移是不现实的,这就要求应用层面允许Redis中数据丢失或从后端数据库重新加载数据。但有些时候,击穿缓存层,直接访问数据库层,会对系统访问造成很大压力。有没有其它手段改善这种情况?

Redis作者给出了一个比较讨巧的办法–presharding,即预先根据系统规模尽量部署好多个Redis实例,这些实例占用系统资源很小,一台物理机可部署多个,让他们都参与sharding,当需要扩容时,选中一个实例作为主节点,新加入的Redis节点作为从节点进行数据复制。数据同步后,修改sharding配置,让指向原实例的Shard指向新机器上扩容后的Redis节点,同时调整新Redis节点为主节点,原实例可不再使用。

presharding是预先分配好足够的分片,扩容时只是将属于某一分片的原Redis实例替换成新的容量更大的Redis实例。参与sharding的分片没有改变,所以也就不存在key值从一个区转移到另一个分片区的现象,只是将属于同分片区的键值从原Redis实例同步到新Redis实例。

Redis集群方案应该怎么做?
https://www.zhihu.com/question/21419897

redis集群(Sharding)和在线扩容(Pre-Sharding)
https://blog.csdn.net/rosanu_blog/article/details/68066756

redis代理中间件twemproxy

Redis代理中间件twemproxy就是这样一种利用中间件做sharding的技术。

twemproxy处于客户端和服务器的中间,将客户端发来的请求,进行一定的处理后(如sharding),再转发给后端真正的Redis服务器。也就是说,客户端不直接访问Redis服务器,而是通过twemproxy代理中间件间接访问。


主从复制

Redis 支持简单且易用的主从复制(master-slave replication)功能, 该功能可以让从服务器(slave server)成为主服务器(master server)的精确复制品。

一个主服务器可以有多个从服务器。
不仅主服务器可以有从服务器, 从服务器也可以有自己的从服务器, 多个从服务器之间可以构成一个图状结构。
复制功能不会阻塞主服务器: 即使有一个或多个从服务器正在进行初次同步, 主服务器也可以继续处理命令请求。

Redis主从同步策略
主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。

slave启动时全量复制

Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:
1)从服务器连接主服务器,发送SYNC命令;
2)主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
3)主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
4)从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
5)主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
6)从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
完成上面几个步骤后就完成了从服务器数据初始化的所有操作,从服务器此时可以接收来自用户的读请求。

即使有多个从服务器同时向主服务器发送 SYNC , 主服务器也只需执行一次 BGSAVE 命令, 就可以处理所有这些从服务器的同步请求。

slave运行中增量同步

Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。

Redis主从复制原理
https://www.cnblogs.com/hepingqingfeng/p/7263782.html

部分重同步(2.8之后,psync)

从服务器可以在主从服务器之间的连接断开时进行自动重连, 在 Redis 2.8 版本之前, 断线之后重连的从服务器总要执行一次完整重同步(full resynchronization)操作, 但是从 Redis 2.8 版本开始, 从服务器可以根据主服务器的情况来选择执行完整重同步还是部分重同步(partial resynchronization)。

从 Redis 2.8 开始, 在网络连接短暂性失效之后, 主从服务器可以尝试继续执行原有的复制进程(process), 而不一定要执行完整重同步操作。

这个特性需要主服务器为被发送的复制流创建一个内存缓冲区(in-memory backlog), 并且主服务器和所有从服务器之间都记录一个复制偏移量(replication offset)和一个主服务器 ID (master run id), 当出现网络连接断开时, 从服务器会重新连接, 并且向主服务器请求继续执行原来的复制进程:

  • 如果从服务器记录的主服务器 ID 和当前要连接的主服务器的 ID 相同, 并且从服务器记录的偏移量所指定的数据仍然保存在主服务器的复制流缓冲区里面, 那么主服务器会向从服务器发送断线时缺失的那部分数据, 然后复制工作可以继续执行。
  • 否则的话, 从服务器就要执行完整重同步操作。

Redis 2.8 的这个部分重同步特性会用到一个新增的 PSYNC 内部命令, 而 Redis 2.8 以前的旧版本只有 SYNC 命令, 不过, 只要从服务器是 Redis 2.8 或以上的版本, 它就会根据主服务器的版本来决定到底是使用 PSYNC 还是 SYNC :
如果主服务器是 Redis 2.8 或以上版本,那么从服务器使用 PSYNC 命令来进行同步。
如果主服务器是 Redis 2.8 之前的版本,那么从服务器使用 SYNC 命令来进行同步。

复制(Replication)
http://doc.redisfans.com/topic/replication.html

redis不保证主从的强一致性

Redis采用了乐观复制(optimistic replication)的复制策略,容忍在一定时间内主从库的内容是不同的,但是两者的数据会最终同步。具体来说,Redis在主从库之间复制数据的过程本身是异步的,这意味着,主库执行完客户端请求的命令后会立即将命令在主库的执行结果返回给客户端,并异步地将命令同步给从库,而不会等待从库接收到该命令后再返回给客户端。

这一特性保证了启用复制后主库的性能不会受到影响,但另一方面也会产生一个主从库数据不一致的时间窗口,当主库执行了一条写命令后,主库的数据已经发生的变动,然而在主库将该命令传送给从库之前,如果两个数据库之间的网络连接断开了,此时二者之间的数据就会是不一致的。

从这个角度来看,主库是无法得知某个命令最终同步给了多少个从库的,不过 Redis 提供了两个配置选项,来限制只有当数据至少同步给指定数量的从库时,主库才是可写的:
min-slaves-to-write 3
min-slaves-max-lag 10

min-slaves-to-write 表示只有当3个(或以上)的从库连接到主库时,主库才是可写的,否则会返回错误:
min-slaves-max-lag 表示允许从库最长失去连接的时间,如果从库最后与主库联系(即发送“replconf ack”命令)的时间小于这个值,则认为从库还在保持与主库的连接。

举个例子,按上面的配置,假设主库与3个从库相连,其中一个从库上一次与主库联系是 9 秒前,这时主库可以正常接受写入,一旦1秒过后这台从库依旧没有活动,则主库则认为目前连接的从库只有2个,从而拒绝写入。这一特性默认是关闭的,在分布式系统中,打开并合理配置该选项后可以降低主从架构中因为网络分区导致的数据不一致的问题。

07Redis入门指南笔记(主从复制、哨兵)
https://blog.csdn.net/gqtcgq/article/details/50273431


sentinel哨兵

Redis 的 Sentinel 系统用于管理多个 Redis 服务器(instance), 该系统执行以下三个任务:

  • 监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
  • 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
  • 自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。

启动sentinel

对于 redis-sentinel 程序, 你可以用以下命令来启动 Sentinel 系统:
redis-sentinel /path/to/sentinel.conf
对于 redis-server 程序, 你可以用以下命令来启动一个运行在 Sentinel 模式下的 Redis 服务器:
redis-server /path/to/sentinel.conf –sentinel

两种方法都可以启动一个 Sentinel 实例。
启动 Sentinel 实例必须指定相应的配置文件, 系统会使用配置文件来保存 Sentinel 的当前状态, 并在 Sentinel 重启时通过载入配置文件来进行状态还原。
如果启动 Sentinel 时没有指定相应的配置文件, 或者指定的配置文件不可写(not writable), 那么 Sentinel 会拒绝启动。

主观下线与客观下线

sentinel down-after-milliseconds mymaster 60000
down-after-milliseconds 选项指定了 Sentinel 认为服务器已经断线所需的毫秒数。
如果服务器在给定的毫秒数之内, 没有返回 Sentinel 发送的 PING 命令的回复, 或者返回一个错误, 那么 Sentinel 将这个服务器标记为主观下线(subjectively down,简称 SDOWN )。
不过只有一个 Sentinel 将服务器标记为主观下线并不一定会引起服务器的自动故障迁移: 只有在足够数量的 Sentinel 都将一个服务器标记为主观下线之后, 服务器才会被标记为客观下线(objectively down, 简称 ODOWN ), 这时自动故障迁移才会执行。
将服务器标记为客观下线所需的 Sentinel 数量由对主服务器的配置决定。

主观下线(Subjectively Down, 简称 SDOWN)指的是单个 Sentinel 实例对服务器做出的下线判断。
客观下线(Objectively Down, 简称 ODOWN)指的是多个 Sentinel 实例在对同一个服务器做出 SDOWN 判断, 并且通过 SENTINEL is-master-down-by-addr 命令互相交流之后, 得出的服务器下线判断。 (一个 Sentinel 可以通过向另一个 Sentinel 发送 SENTINEL is-master-down-by-addr 命令来询问对方是否认为给定的服务器已下线。)

主从切换后如何知道master的ip?

主从切换后,master ip变了,如何知道往哪个里面写入?
java后台使用jedis来操作redis,配置一个JedisSentinelPool,传入sentinel哨兵集群中每个节点的地址,JedisSentinelPool初始化时会与所有sentinel沟通,确定当前sentinel集群所监视的master是哪一个。主从切换后通过sentinel集群还能读出新的master的地址,所以写入不会出错。

sentinel如何知道其他sentinel的存在?(发布订阅)

一个 Sentinel 可以与其他多个 Sentinel 进行连接, 各个 Sentinel 之间可以互相检查对方的可用性, 并进行信息交换。

你无须为运行的每个 Sentinel 分别设置其他 Sentinel 的地址, 因为 Sentinel 可以通过发布与订阅功能来自动发现正在监视相同主服务器的其他 Sentinel , 这一功能是通过向频道 sentinel:hello 发送信息来实现的。

与此类似, 你也不必手动列出主服务器属下的所有从服务器, 因为 Sentinel 可以通过询问主服务器来获得所有从服务器的信息。

  • 每个 Sentinel 会以每两秒一次的频率, 通过发布与订阅功能, 向被它监视的所有主服务器和从服务器的 sentinel:hello 频道发送一条信息, 信息中包含了 Sentinel 的 IP 地址、端口号和运行 ID (runid)。
  • 每个 Sentinel 都订阅了被它监视的所有主服务器和从服务器的 sentinel:hello 频道, 查找之前未出现过的 sentinel (looking for unknown sentinels)。 当一个 Sentinel 发现一个新的 Sentinel 时, 它会将新的 Sentinel 添加到一个列表中, 这个列表保存了 Sentinel 已知的, 监视同一个主服务器的所有其他 Sentinel 。
  • Sentinel 发送的信息中还包括完整的主服务器当前配置(configuration)。 如果一个 Sentinel 包含的主服务器配置比另一个 Sentinel 发送的配置要旧, 那么这个 Sentinel 会立即升级到新配置上。
  • 在将一个新 Sentinel 添加到监视主服务器的列表上面之前, Sentinel 会先检查列表中是否已经包含了和要添加的 Sentinel 拥有相同运行 ID 或者相同地址(包括 IP 地址和端口号)的 Sentinel , 如果是的话, Sentinel 会先移除列表中已有的那些拥有相同运行 ID 或者相同地址的 Sentinel , 然后再添加新 Sentinel 。

redis持久化

Redis的所有数据都是保存在内存中,然后不定期的通过异步方式保存到磁盘上(这称为“半持久化模式”);也可以把每一次数据变化都写入到一个append only file(aof)里面(这称为“全持久化模式”)。

由于Redis的数据都存放在内存中,如果没有配置持久化,redis重启后数据就全丢失了,于是需要开启redis的持久化功能,将数据保存到磁 盘上,当redis重启后,可以从磁盘中恢复数据。

redis提供两种方式进行持久化:
一种是RDB持久化(原理是将Reids在内存中的数据库记录定时 dump到磁盘上的RDB持久化),
另外一种是AOF(append only file)持久化(原理是将Reids的操作日志以追加的方式写入文件)。

redis还可以同时使用AOF持久化和RDB持久化,在这种情况下,当redis重启时,它会有限使用AOF文件来还原数据集,因为AOF文件保存的数据集通常比RDB文件所保存的数据集更加完

RDB持久化(磁盘快照)

RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

RDB持久化配置
注意:save的两个条件都要满足才dump快照

save 900 1  #在900秒(15分钟)之后,如果至少有1个key发生变化,则dump内存快照。
save 300 10  #在300秒(5分钟)之后,如果至少有10个key发生变化,则dump内存快照。
save 60 10000  #在60秒(1分钟)之后,如果至少有10000个key发生变化,则dump内存快照。
dbfilename "dump.rdb"          #持久化文件名称
dir "/data/dbs/redis/6381"    #持久化数据文件存放的路径

配置文件修改需要重启redis服务,我们还可以在命令行里进行配置,即时生效,服务器重启后需重新配置

save(阻塞)和bgsave(异步非阻塞)

而RDB持久化也分两种:SAVE和BGSAVE
SAVE是阻塞式的RDB持久化,当执行这个命令时redis的主进程把内存里的数据库状态写入到RDB文件(即上面的dump.rdb)中,直到该文件创建完毕的这段时间内redis将不能处理任何命令请求。
BGSAVE属于非阻塞式的持久化,它会创建一个子进程专门去把内存中的数据库状态写入RDB文件里,同时主进程还可以处理来自客户端的命令请求。但子进程基本是复制的父进程,这等于两个相同大小的redis进程在系统上运行,会造成内存使用率的大幅增加。

bgsave命令
background save
在后台异步(Asynchronously)保存当前数据库的数据到磁盘。
BGSAVE 命令执行之后立即返回 OK ,然后 Redis fork 出一个新子进程,原来的 Redis 进程(父进程)继续处理客户端请求,而子进程则负责将数据保存到磁盘,然后退出。
客户端可以通过 LASTSAVE 命令查看相关信息,判断 BGSAVE 命令是否执行成功。


AOF持久化

AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

AOF相关配置项:

dir "/data/dbs/redis/6381"          #AOF文件存放目录
appendonly yes                      #开启AOF持久化,默认关闭
appendfilename "appendonly.aof"      #AOF文件名称(默认)
appendfsync no                      #AOF持久化策略,有三个选项:always、everysec和no
auto-aof-rewrite-percentage 100      #触发AOF文件重写的条件(默认)
auto-aof-rewrite-min-size 64mb      #触发AOF文件重写的条件(默认)

要弄明白上面几个配置就得从AOF的实现去理解,AOF的持久化是通过命令追加、文件写入和文件同步三个步骤实现的。当reids开启AOF后,服务端每执行一次写操作(如set、sadd、rpush)就会把该条命令追加到一个单独的AOF缓冲区的末尾,这就是命令追加;然后把AOF缓冲区的内容写入AOF文件里。看上去第二步就已经完成AOF持久化了那第三步是干什么的呢?这就需要从系统的文件写入机制说起:一般我们现在所使用的操作系统,为了提高文件的写入效率,都会有一个写入策略,即当你往硬盘写入数据时,操作系统不是实时的将数据写入硬盘,而是先把数据暂时的保存在一个内存缓冲区里,等到这个内存缓冲区的空间被填满或者是超过了设定的时限后才会真正的把缓冲区内的数据写入硬盘中。也就是说当redis进行到第二步文件写入的时候,从用户的角度看是已经把AOF缓冲区里的数据写入到AOF文件了,但对系统而言只不过是把AOF缓冲区的内容放到了另一个内存缓冲区里而已,之后redis还需要进行文件同步把该内存缓冲区里的数据真正写入硬盘上才算是完成了一次持久化。而何时进行文件同步则是根据配置的appendfsync来进行:

何时进行AOF持久化(always,everysec,no)

appendfsync有三个选项:always、everysec和no:

always(每个写操作)

1、选择always的时候服务器会在每执行一个事件就把AOF缓冲区的内容强制性的写入硬盘上的AOF文件里,可以看成你每执行一个redis写入命令就往AOF文件里记录这条命令,这保证了数据持久化的完整性,但效率是最慢的,却也是最安全的;

everysec(每秒)

2、配置成everysec的话服务端每执行一次写操作(如set、sadd、rpush)也会把该条命令追加到一个单独的AOF缓冲区的末尾,并将AOF缓冲区写入AOF文件,然后每隔一秒才会进行一次文件同步把内存缓冲区里的AOF缓存数据真正写入AOF文件里,这个模式兼顾了效率的同时也保证了数据的完整性,即使在服务器宕机也只会丢失一秒内对redis数据库做的修改;

no(由系统决定)

3、配置成no则意味redis数据库里的数据就算丢失你也可以接受,它也会把每条写命令追加到AOF缓冲区的末尾,然后写入文件,但什么时候进行文件同步真正把数据写入AOF文件里则由系统自身决定,即当内存缓冲区的空间被填满或者是超过了设定的时限后系统自动同步。这种模式下效率是最快的,但对数据来说也是最不安全的,如果redis里的数据都是从后台数据库如mysql中取出来的,属于随时可以找回或者不重要的数据,那么可以考虑设置成这种模式。

AOF重写(bgrewriteaof)

因为 AOF 的运作方式是不断地将命令追加到文件的末尾, 所以随着写入命令的不断增加, AOF 文件的体积也会变得越来越大。
举个例子, 如果你对一个计数器调用了 100 次 INCR , 那么仅仅是为了保存这个计数器的当前值, AOF 文件就需要使用 100 条记录(entry)。
然而在实际上, 只使用一条 SET 命令已经足以保存计数器的当前值了, 其余 99 条记录实际上都是多余的。
为了处理这种情况, Redis 支持一种有趣的特性: 可以在不打断服务客户端的情况下, 对 AOF 文件进行重建(rebuild)。
执行 BGREWRITEAOF 命令, Redis 将生成一个新的 AOF 文件, 这个文件包含重建当前数据集所需的最少命令。
Redis 2.2 需要自己手动执行 BGREWRITEAOF 命令; Redis 2.4 则可以自动触发 AOF 重写, 具体信息请查看 2.4 的示例配置文件。

RDB与AOF对比及合理选择持久化策略

RDB 和 AOF ,我应该用哪一个?
一般来说, 如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。
如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。
有很多用户都只使用 AOF 持久化, 但我们并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快, 除此之外, 使用 RDB 还可以避免之前提到的 AOF 程序的 bug 。
因为以上提到的种种原因, 未来我们可能会将 AOF 和 RDB 整合成单个持久化模型。 (这是一个长期计划。)

redis——持久化篇
https://www.cnblogs.com/dengtr/p/5085287.html

redis持久化的几种方式
https://www.cnblogs.com/AndyAo/p/8135980.html

redis两种持久化方式的优缺点
https://www.cnblogs.com/ssssdy/p/7132856.html


redis过期策略

redis过期时间设置(expire)与查看(ttl)

expire key second 为key设置过期时间
返回1表明设置成功,返回0表明key不存在或者不能成功设置过期时间。

此外还有如下几个命令可设置过期时间:
EXPIRE key seconds  //将key的生存时间设置为ttl秒
PEXPIRE key milliseconds  //将key的生成时间设置为ttl毫秒
EXPIREAT key timestamp  //将key的过期时间设置为timestamp所代表的的秒数的时间戳
PEXPIREAT key milliseconds-timestamp  //将key的过期时间设置为timestamp所代表的的毫秒数的时间戳

TTL key
以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)。

redis有3种过期策略:

每个key定时删除

含义:在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除
优点:保证内存被尽快释放
缺点:
若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,CPU不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些key
定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生),性能影响严重
没人用

惰性删除(访问key时再删除)

含义:key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。
优点:删除操作只发生在从数据库取出key的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,而且此时的删除是已经到了非做不可的地步(如果此时还不删除的话,我们就会获取到了已经过期的key了)
缺点:若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露(无用的垃圾占用了大量的内存)

对于惰性删除而言,并不是只有获取key的时候才会检查key是否过期,在某些设置key的方法上也会检查(eg.setnx key2 value2:该方法类似于memcached的add方法,如果设置的key2已经存在,那么该方法返回false,什么都不做;如果设置的key2不存在,那么该方法设置缓存key2-value2。假设调用此方法的时候,发现redis中已经存在了key2,但是该key2已经过期了,如果此时不执行删除操作的话,setnx方法将会直接返回false,也就是说此时并没有重新设置key2-value2成功,所以对于一定要在setnx执行之前,对key2进行过期检查)

周期性删除

含义:每隔一段时间执行一次删除(在redis.conf配置文件设置hz,1s刷新的频率)过期key操作
优点:
通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用–处理”定时删除”的缺点
定期删除过期key–处理”惰性删除”的缺点
缺点
在内存友好方面,不如”定时删除”
在CPU时间友好方面,不如”惰性删除”
难点
合理设置删除操作的执行时长(每次删除执行多长时间)和执行频率(每隔多长时间做一次删除)(这个要根据服务器运行情况来定了)

定时删除和定期删除为主动删除:Redis会定期主动淘汰一批已过去的key
惰性删除为被动删除:用到的时候才会去检验key是不是已过期,过期就删除

惰性删除为redis服务器内置策略

定期删除可以通过:
第一、配置redis.conf 的hz选项,默认为10 (即1秒执行10次,100ms一次,值越大说明刷新频率越快,最Redis性能损耗也越大)
第二、配置redis.conf的maxmemory最大值,当已用内存超过maxmemory限定时,就会触发主动清理策略

Redis学习笔记–Redis数据过期策略详解
http://www.cnblogs.com/xuliangxing/p/7151812.html

关于Redis数据过期策略
https://www.cnblogs.com/chenpingzhao/p/5022467.html


redis内存淘汰策略

maxmemory bytes 配置限制使用的最大内存,也就是说配置maxmemory后会开启内存淘汰功能
maxmemory为0的时候表示我们对Redis的内存使用没有限制。

当我们程序达到最大值时, Redis使用了多种策略进行置换.Redis建议最大内存设置为物理内存的一半。

maxmemory-policy noeviction 配置内存淘汰策略:

Redis提供了下面几种淘汰策略供用户选择,其中默认的策略为noeviction策略:
noeviction:不淘汰,当内存使用达到阈值的时候,所有引起申请内存的命令会报错。
allkeys-lru:在主键空间中,优先移除最近未使用的key。
volatile-lru:在设置了过期时间的键空间中,优先移除最近未使用的key。
allkeys-random:在主键空间中,随机移除某个key。
volatile-random:在设置了过期时间的键空间中,随机移除某个key。
volatile-ttl:在设置了过期时间的键空间中,具有更早过期时间的key优先移除。

下面看看几种策略的适用场景:
allkeys-lru:如果我们的应用对缓存的访问符合幂律分布(也就是存在相对热点数据),或者我们不太清楚我们应用的缓存访问分布状况,我们可以选择allkeys-lru策略。
allkeys-random:如果我们的应用对于缓存key的访问概率相等,则可以使用这个策略。
volatile-ttl:这种策略使得我们可以向Redis提示哪些key更适合被eviction。
另外,volatile-lru策略和volatile-random策略适合我们将一个Redis实例既应用于缓存和又应用于持久化存储的时候,然而我们也可以通过使用两个Redis实例来达到相同的效果,值得一提的是将key设置过期时间实际上会消耗更多的内存,因此我们建议使用allkeys-lru策略从而更有效率的使用内存。

主键空间和过期键空间

主键空间和设置了过期时间的键空间,举个例子,假设我们有一批键存储在Redis中,则有那么一个哈希表用于存储这批键及其值,如果这批键中有一部分设置了过期时间,那么这批键还会被存储到另外一个哈希表中,这个哈希表中的值对应的是键被设置的过期时间。设置了过期时间的键空间为主键空间的子集。

Redis 内存淘汰机制
https://www.cnblogs.com/changbosha/p/5849982.html


Redis和数据库不一致问题

如果要“保证”数据的安全性,那么会带来开销的进一步提升,以至于使用redis带来的性能优势都会丧失。正确的做法是区分不同的业务,使得并不需要“保证”数据一致性的场合,可以使用redis优化。而敏感的场合依然使用mysql。

数据库和缓存之间一般不需要强一致性。
一般缓存是这样的:
读的顺序是先读缓存,后读数据库
写的顺序是先写数据库,然后写缓存
每次更新了相关的数据,都要把该缓存清理掉
为了避免极端条件下造成的缓存与数据库之间的数据不一致,缓存需要设置一个失效时间。时间到了,缓存自动被清理,达到缓存和数据库数据的“最终一致性”

为保证redis和数据库的数据一致性,写入策略应该是:
先使redis key失效(删除key),再写入数据库,等redis查询时自动去数据库更新。也就是缓存只做失效,不做更新。

数据库与缓存双写情况下导致数据不一致问题
场景一
当更新数据时,如更新某商品的库存,当前商品的库存是100,现在要更新为99,先更新数据库更改成99,然后删除缓存,发现删除缓存失败了,这意味着数据库存的是99,而缓存是100,这导致数据库和缓存不一致。

场景一解决方案
这种情况应该是先删除缓存,然后在更新数据库,如果删除缓存失败,那就不要更新数据库,如果说删除缓存成功,而更新数据库失败,那查询的时候只是从数据库里查了旧的数据而已,这样就能保持数据库与缓存的一致性。

场景二
在高并发的情况下,如果当删除完缓存的时候,这时去更新数据库,但还没有更新完,另外一个请求来查询数据,发现缓存里没有,就去数据库里查,还是以上面商品库存为例,如果数据库中产品的库存是100,那么查询到的库存是100,然后插入缓存,插入完缓存后,原来那个更新数据库的线程把数据库更新为了99,导致数据库与缓存不一致的情况

场景二解决方案
遇到这种情况,可以用队列的去解决这个问,创建几个队列,如20个,根据商品的ID去做hash值,然后对队列个数取摸,当有数据更新请求时,先把它丢到队列里去,当更新完后在从队列里去除,如果在更新的过程中,遇到以上场景,先去缓存里看下有没有数据,如果没有,可以先去队列里看是否有相同商品ID在做更新,如果有也把查询的请求发送到队列里去,然后同步等待缓存更新完成。

redis系列之数据库与缓存数据一致性解决方案
http://blog.csdn.net/simba_1986/article/details/77823309

mysql binlog

Mysql的binlog日志作用是用来记录mysql内部增删改查等对mysql数据库有更新的内容的记录(对数据库的改动),对数据库的查询select或show等不会被binlog日志记录;主要用于数据库的主从复制以及增量恢复。

使用阿里的同步工具canal,canal实现方式是模拟mysql slave和master的同步机制,监控DB bitlog的日志更新来触发缓存的更新,此种方法可以解放程序员双手,减少工作量,但在使用时有些局限性。


redis事务

redis的事务中,一次执行多条命令,本质是一组命令的集合,一个事务中所有的命令将被序列化,即按顺序执行而不会被其他命令插入

在redis中,事务的作用就是在一个队列中一次性、顺序性、排他性的执行一系列的命令。

常用的关于事务的命令有:

  1. MULTI:使用该命令,标记一个事务块的开始,通常在执行之后会回复OK,(但不一定真的OK),这个时候用户可以输入多个操作来代替逐条操作,redis会将这些操作放入队列中。
  2. EXEC:执行这个事务内的所有命令
  3. DISCARD:放弃事务,即该事务内的所有命令都将取消
  4. WATCH:监控一个或者多个key,如果这些key在提交事务(EXEC)之前被其他用户修改过,那么事务将执行失败,需要重新获取最新数据重头操作(类似于乐观锁)。
  5. UNWATCH:取消WATCH命令对多有key的监控,所有监控锁将会被取消。

redis事务不支持回滚

如下,事务中某条命令执行时错误:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key 1
QUEUED
127.0.0.1:6379> SADD key 2  //用集合命令sadd操作string类型key出现错误
QUEUED
127.0.0.1:6379> SET key 3  //错误之后的命令依然会继续执。
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) OK
127.0.0.1:6379> GET key //键值被错误命令之后的命令修改
"3"

但是提交事务的时候会发现,这条错误命令确实没有执行,但是其他正确的命令却执行,这是为什么的?
原因是在redis中,对于一个存在问题的命令,如果在入队的时候就已经出错,整个事务内的命令将都不会被执行(其后续的命令依然可以入队),如果这个错误命令在入队的时候并没有报错,而是在执行的时候出错了,那么redis默认跳过这个命令执行后续命令。
也就是说,redis只实现了部分事务。不支持回滚。

为什么redis事务不支持回滚?

如果你有使用关系式数据库的经验, 那么 “Redis 在事务失败时不进行回滚,而是继续执行余下的命令”这种做法可能会让你觉得有点奇怪。

以下是这种做法的优点:
Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。

为什么 Redis 不支持回滚(roll back)
http://doc.redisfans.com/topic/transaction.html#redis-roll-back

用watch命令实现乐观锁

redis的锁CAS(check and set)类似于乐观锁,redis的实现原理是使用watch进行监视一个(或多个)数据,如果在事务提交之前数据发生了变化(估计使用了类似于乐观锁的标记),那么整个事务将提交失败。

我们可以举一个例子,我们开启两个终端,模拟两个人的操作,设置一条数据为count,初始时100,现在A对其进行监控,并且为count增加20。在没有提交之前,B也获取了这个count,为其减少50,那么这个时候A如果提交事务,会出现失败提示。可以看到,在A对数据的修改过程中,B对数据进行了修改,那么这条数据的“标记”就发生了变化,已经不是当初A取出数据的时候的标记了,这样,A的事务也就提交失败了。

Redis入门之浅谈redis事务
https://blog.csdn.net/candy_rainbow/article/details/52810440

Redis的事务功能详解
https://www.cnblogs.com/kyrin/p/5967620.html


keys模糊查询

可以通过Redis中keys命令进行获取key值,具体命令格式:keys pattern

redis中允许模糊查询的有3个通配符,分别是:*,?,[]

其中:
*:通配任意多个字符
?:通配单个字符
[]:通配括号内的某一个字符


发布订阅

SUBSCRIBE 、 UNSUBSCRIBE 和 PUBLISH 三个命令实现了发布与订阅信息泛型(Publish/Subscribe messaging paradigm), 在这个实现中, 发送者(发送信息的客户端)不是将信息直接发送给特定的接收者(接收信息的客户端), 而是将信息发送给频道(channel), 然后由频道将信息转发给所有对这个频道感兴趣的订阅者。

发送者无须知道任何关于订阅者的信息, 而订阅者也无须知道是那个客户端给它发送信息, 它只要关注自己感兴趣的频道即可。

比如说, 要订阅频道 foo 和 bar , 客户端可以使用频道名字作为参数来调用 SUBSCRIBE 命令:
redis> SUBSCRIBE foo bar

当有客户端发送信息到这些频道时, Redis 会将传入的信息推送到所有订阅这些频道的客户端里面。

正在订阅频道的客户端不应该发送除 SUBSCRIBE 和 UNSUBSCRIBE 之外的其他命令。 其中, SUBSCRIBE 可以用于订阅更多频道, 而 UNSUBSCRIBE 则可以用于退订已订阅的一个或多个频道。

SUBSCRIBE 和 UNSUBSCRIBE 的执行结果会以信息的形式返回, 客户端可以通过分析所接收信息的第一个元素, 从而判断所收到的内容是一条真正的信息, 还是 SUBSCRIBE 或 UNSUBSCRIBE 命令的操作结果。

发布与订阅(pub/sub)
http://doc.redisfans.com/topic/pubsub.html


缓存

缓存淘汰算法

LFU(Least Frequently Used 最不经常使用)

LFU(Least Frequently Used 最不经常使用)算法根据数据的历史访问频率来淘汰数据,其核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。
实现:LFU的每个数据块都有一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序。
具体实现如下:

  1. 新加入数据插入到队列尾部(因为引用计数为1);
  2. 队列中的数据被访问后,引用计数增加,队列重新排序;
  3. 当需要淘汰数据时,将已经排序的列表最后的数据块删除。

LRU(Least recently used 最近最少使用)

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
最常见的实现是使用一个链表保存缓存数据

  1. 新数据插入到链表头部;
  2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
  3. 当链表满的时候,将链表尾部的数据丢弃。

当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。


读缓存

读操作流程

有了数据库和缓存两个地方存放数据之后,每当需要读取相关数据时,操作流程一般是这样的:
(1)读取缓存中是否有相关数据
(2)如果缓存中有相关数据,则返回【这就是所谓的数据命中“hit”】
(3)如果缓存中没有相数据,则从数据库读取相关数据【这就是所谓的数据未命中“miss”】,放入缓存中,再返回
缓存的命中率 = 命中缓存的请求个数/总缓存访问请求个数 = hit/(hit+miss)


缓存穿透(查询不存在的数据)

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。

布隆过滤器

对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃。还有最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

缓存不存在的空值

也可以采用一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

缓存空对象会有两个问题:
第一,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间 ( 如果是攻击,问题更严重 ),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
第二,缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为 5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。

最佳实践 缓存穿透,瞬间并发,缓存雪崩的解决方法
https://blog.csdn.net/fei33423/article/details/79027790


缓存雪崩(缓存集中失效)

如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩。
这个没有完美解决办法,但可以分析用户行为,尽量让失效时间点均匀分布。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。

解决方法:
1、加锁排队
在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。

2、数据预热
可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存

3、设置不同过期时间
不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀

4、做二级缓存,或者双缓存策略
做二级缓存,或者双缓存策略。A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。

5、缓存永远不过期
这里的“永远不过期”包含两层意思:
(1) 从缓存上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。
(2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期.

从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。

最佳实践 缓存穿透,瞬间并发,缓存雪崩的解决方法
https://blog.csdn.net/fei33423/article/details/79027790

《redis学习》– 缓存穿透和缓存雪崩的预防和解决
https://blog.csdn.net/lizhi_java/article/details/68953014?locationNum=14&fps=1

缓存穿透,缓存击穿,缓存雪崩解决方案分析
https://blog.csdn.net/zeb_perfect/article/details/54135506


缓存更新策略

缓存的更新策略主要有两种:被动失效和主动更新
什么是更新缓存:数据不但写入数据库,还会写入缓存
什么是淘汰缓存:数据只会写入数据库,不会写入缓存,只会把数据淘汰掉

淘汰缓存(被动失效)

一般来说,缓存数据主要是服务读请求的,并设置一个过期时间;或者当数据库状态改变时,通过一个简单的delete操作,使数据失效掉;当下次再去读取时,如果发现数据过期了或者不存在了,那么就重新去持久层读取,然后更新到缓存中;这即是所谓的被动失效策略。

但是在被动失效策略中存在一个问题,就是从缓存失效或者丢失开始直到新的数据再次被更新到缓存中的这段时间,所有的读请求都将会直接落到数据库上;而对于一个大访问量的系统来说,这有可能会带来风险。所以我们换一种策略就是,当数据库更新时,主动去同步更新缓存,这样在缓存数据的整个生命期内,就不会有空窗期,前端请求也就没有机会去亲密接触数据库。

先淘汰缓存还是先写库?

当写操作发生时,假设淘汰缓存作为对缓存通用的处理方式,又面临两种抉择:
(1)先写数据库,再淘汰缓存
(2)先淘汰缓存,再写数据库

假设先写数据库,再淘汰缓存:第一步写数据库操作成功,第二步淘汰缓存失败,则会出现DB中是新数据,Cache中是旧数据,数据不一致。
假设先淘汰缓存,再写数据库:第一步淘汰缓存成功,第二步写数据库失败,则只会引发一次Cache miss。

结论:数据和缓存的操作时序,结论是清楚的:先淘汰缓存,再写数据库。

缓存架构设计二三事
https://blog.csdn.net/justinsause/article/details/51063631

主动更新缓存

前面我们提到主动更新主要是为了解决空窗期的问题,但是这同样会带来另一个问题,就是并发更新的情况;

在集群环境下,多台应用服务器同时访问一份数据是很正常的,这样就会存在一台服务器读取并修改了缓存数据,但是还没来得及写入的情况下,另一台服务器也读取并修改旧的数据,这时候,后写入的将会覆盖前面的,从而导致数据丢失;这也是分布式系统开发中,必然会遇到的一个问题。

主动更新缓存的数据一致问题

解决的方式主要有三种:
a、锁控制;这种方式一般在客户端实现(在服务端加锁是另外一种情况),其基本原理就是使用读写锁,即任何进程要调用写方法时,先要获取一个排他锁,阻塞住所有的其他访问,等自己完全修改完后才能释放;如果遇到其他进程也正在修改或读取数据,那么则需要等待;
锁控制虽然是一种方案,但是很少有真的这样去做的,其缺点显而易见,其并发性只存在于读操作之间,只要有写操作存在,就只能串行。

b、版本控制;这种方式也有两种实现,一种是单版本机制,即为每份数据保存一个版本号,当缓存数据写入时,需要传入这个版本号,然后服务端将传入的版本号和数据当前的版本号进行比对,如果大于当前版本,则成功写入,否则返回失败;这样解决方式比较简单;但是增加了高并发下客户端的写失败概率;

还有一种方式就是多版本机制,即存储系统为每个数据保存多份,每份都有自己的版本号,互不冲突,然后通过一定的策略来定期合并,再或者就是交由客户端自己去选择读取哪个版本的数据。很多分布式缓存一般会使用单版本机制,而很多NoSQL则使用后者。

大型web系统数据缓存设计
https://blog.csdn.net/yoko_luo/article/details/52571559


缓存的扩展性

缓存系统的扩展性是指在空间不足的性情况,能够通过增加机器等方式快速的在线扩容。这也是能够支撑业务系统快速发展的一个重要因素。


一致性哈希consistent hashing

一致性hash算法 - consistent hashing:http://blog.csdn.net/sparkliang/article/details/5279393
memcache的一致性hash算法使用:http://blog.csdn.net/kongqz/article/details/6695417

分布式缓存设计核心点:在设计分布式cache系统的时候,我们需要让key的分布均衡,并且在增加cache server后,cache的迁移做到最少。

首先求出机器节点的hash值(怎么算机器节点的hash?ip可以作为hash的参数吧),然后将其分布到0~2^32的一个圆环上(顺时针分布)
如果有一个写入缓存的请求,其中Key值为K,计算器hash值Hash(K), Hash(K) 对应于环中的某一个点,如果该点对应没有映射到具体的某一个机器节点,那么顺时针查找,直到第一次找到有映射机器的节点,该节点就是确定的目标节点

一致性哈希算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜问题。例如系统中只有两台服务器,此时必然造成大量数据集中到Node A上,而只有极少量会定位到Node B上。为了解决这种数据倾斜问题,一致性哈希算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器ip或主机名的后面增加编号来实现。例如上面的情况,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值,于是形成六个虚拟节点。
同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,例如定位到“Node A#1”、“Node A#2”、“Node A#3”三个虚拟节点的数据均定位到Node A上。这样就解决了服务节点少时数据倾斜的问题。在实际应用中,通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。

一致性哈希算法原理
https://www.cnblogs.com/lpfuture/p/5796398.html

一致性hash算法 - consistent hashing
http://blog.csdn.net/sparkliang/article/details/5279393

memcache的一致性hash算法使用
http://blog.csdn.net/kongqz/article/details/6695417


使用缓存带来的问题

我们不能否认缓存给我们带来诸多便利,同时,我们不能忽略缓存确实也给我们带来了不少困扰:
1、数据的一致性、实时性受影响。(需要对数据的一致性,时效性进行评估,进而确定是否要缓存或设定缓存的过期时间,比如个性化的数据是否值得缓存?)

2、缓存介质带来的不可靠性。(一般使用内存做缓存的话,若机器故障,如何保证缓存的高可用?可考虑对缓存进行分布式做成高可用,同时,需要接受这种不可靠不安全会给数据带来的问题,在异常情况下进行补偿处理,定期持久化等方式)

3、缓存的数据使得更难排查问题。因为缓存命中是随着访问随时变化的,缓存的行为难以重现,使得出现BUG很难排查。

4、进程内缓存可能会增加GC压力:在具有垃圾收集功能的语言中(如java),大量长寿命的缓存对象会增加垃圾收集的时间和次数。

性能优化–缓存篇
https://blog.csdn.net/liaoyulin0609/article/details/51762935


二级缓存(中心缓存+本地缓存)

ehcache+redis二级缓存
redis做中心缓存的问题
redis主备作为中心节点提供缓存,其他节点都向redis中心节点取数据。
但是,随着访问量增大,大量的缓存数据访问使得应用服务器和缓存服务器之间的网络I/O消耗越大。

那么要怎么处理呢?
所以两级缓存的思想诞生了,在redis的方案上做一步优化,在缓存到远程redis的同时,缓存一份到本地进程ehcache(此处的ehcache不用做集群,避免组播带来的开销),取缓存的时候会先取本地,没有会向redis请求,这样会减少应用服务器<–>缓存服务器redis之间的网络开销。

Spring+ehcache+redis两级缓存–缓存实战篇(1)
https://blog.csdn.net/liaoyulin0609/article/details/51787020


ehcache memcached redis对比

ehcache
纯java实现,缓存在内存中,可持久化到硬盘,效率高于memcache;但是缓存共享麻烦,集群分布式应用不方便。如果是单个应用或者对缓存访问要求很高的应用,用ehcache。
ehcache也有缓存共享方案,不过是通过RMI或者Jgroup多播方式进行广播缓存通知更新,缓存共享复杂,维护不方便;简单的共享可以,但是涉及到缓存恢复,大数据缓存,则不合适。

memcached
多种语言实现,K/V缓存,缓存在内存中,支持分布式缓存,可通过第三方实现持久化,基于libevent的事件处理。
memcache在客户端中实现分布式缓存,通过分布式算法指定目标数据的节点。

redis
K/V缓存,支持丰富的数据结构(list, set, hash),支持数据持久化,支持分布式缓存。
通过socket访问到缓存服务,效率比ehcache低,比数据库要快很多,处理集群和分布式缓存方便,有成熟的方案。如果是大型系统,存在缓存共享、分布式部署、缓存内容很大的,建议用redis。


上一篇 Java面试准备-(07)Spring

下一篇 Java面试准备-(05)数据库

阅读
21,461
阅读预计78分钟
创建日期 2018-04-08
修改日期 2018-12-13
类别
目录
  1. Redis
    1. Redis数据类型
      1. 列表list(双向链表,有序)
        1. 操作命令
          1. 插入
          2. 查询
          3. 删除
          4. 修改
          5. 阻塞操作
        2. 内部数据结构
        3. 使用场景(阻塞队列)
      2. 集合set(无序,不可重复)
        1. 操作命令
          1. 集合内操作
          2. 集合间操作
        2. 内部数据结构
        3. 使用场景(用户标签)
      3. 有序集合zset(按分值score排序)
        1. 集合内操作命令
          1. 添加成员zadd key score mem(logn复杂度)
          2. 获取成员分数zscore key mem
          3. 获取成员排名zrank key mem
          4. 获取指定范围元素 zrange key start end
          5. 获取指定分值范围元素 zrangebyscore key min max
        2. 集合间的操作命令
        3. 内部数据结构(跳跃表skiplist)
      4. redis各数据类型的使用场景
    2. Redis内部数据结构
      1. sds简单动态字符串
      2. list双向链表
      3. intset整数集合
      4. dict字典(哈希表)
      5. zskiplist跳跃表
      6. ziplist压缩表
    3. 单进程单线程
      1. 多路复用io模型epoll
    4. redis集群
      1. redis3.0官方集群方案:服务端哈希槽
      2. redis3.0之前的集群方案:客户端sharding
        1. 客户端sharding扩容方案-presharding
      3. redis代理中间件twemproxy
    5. 主从复制
      1. slave启动时全量复制
      2. slave运行中增量同步
      3. 部分重同步(2.8之后,psync)
      4. redis不保证主从的强一致性
    6. sentinel哨兵
      1. 启动sentinel
      2. 主观下线与客观下线
      3. 主从切换后如何知道master的ip?
      4. sentinel如何知道其他sentinel的存在?(发布订阅)
    7. redis持久化
      1. RDB持久化(磁盘快照)
        1. save(阻塞)和bgsave(异步非阻塞)
      2. AOF持久化
        1. 何时进行AOF持久化(always,everysec,no)
          1. always(每个写操作)
          2. everysec(每秒)
          3. no(由系统决定)
        2. AOF重写(bgrewriteaof)
      3. RDB与AOF对比及合理选择持久化策略
    8. redis过期策略
      1. redis过期时间设置(expire)与查看(ttl)
      2. 每个key定时删除
      3. 惰性删除(访问key时再删除)
      4. 周期性删除
    9. redis内存淘汰策略
      1. 主键空间和过期键空间
    10. Redis和数据库不一致问题
      1. mysql binlog
    11. redis事务
      1. redis事务不支持回滚
        1. 为什么redis事务不支持回滚?
      2. 用watch命令实现乐观锁
    12. keys模糊查询
    13. 发布订阅
  2. 缓存
    1. 缓存淘汰算法
      1. LFU(Least Frequently Used 最不经常使用)
      2. LRU(Least recently used 最近最少使用)
    2. 读缓存
      1. 读操作流程
      2. 缓存穿透(查询不存在的数据)
        1. 布隆过滤器
        2. 缓存不存在的空值
      3. 缓存雪崩(缓存集中失效)
    3. 缓存更新策略
      1. 淘汰缓存(被动失效)
        1. 先淘汰缓存还是先写库?
      2. 主动更新缓存
        1. 主动更新缓存的数据一致问题
    4. 缓存的扩展性
      1. 一致性哈希consistent hashing
    5. 使用缓存带来的问题
    6. 二级缓存(中心缓存+本地缓存)
    7. ehcache memcached redis对比
百度推荐