@songhanshi
2020-12-14T11:24:04.000000Z
字数 47008
阅读 694
Java学习
书籍资料
redis特性
1)速度快
2)基于键值对的数据结构服务器
3)丰富的功能
4)简单稳定
5)客户端语言多
6)持久化
7)主从复制
8)高可用和分布式
读写速度?
忽略机器性能上的差异,官方给出的数字是读写性能可以达到10万/秒。
Redis速度非常快的原因?
5种数据结构
除了5种数据结构,Redis还提供了许多额外的功能?
Redis可以做什么
1.缓存
缓存机制几乎在所有的大型网站都有使用,合理地使用缓存不仅可以加 快数据的访问速度,而且能够有效地降低后端数据源的压力。Redis提供了 键值过期时间设置,并且也提供了灵活控制最大内存和内存溢出后的淘汰策 略。可以这么说,一个合理的缓存设计能够为一个网站的稳定保驾护航。第 11章将对缓存的设计与使用进行详细说明。
2.排行榜系统
排行榜系统几乎存在于所有的网站,例如按照热度排名的排行榜,按照 发布时间的排行榜,按照各种复杂维度计算出的排行榜,Redis提供了列表 和有序集合数据结构,合理地使用这些数据结构可以很方便地构建各种排行 榜系统。
3.计数器应用
计数器在网站中的作用至关重要,例如视频网站有播放数、电商网站有 浏览数,为了保证数据的实时性,每一次播放和浏览都要做加1的操作,如 果并发量很大对于传统关系型数据的性能是一种挑战。Redis天然支持计数 功能而且计数的性能也非常好,可以说是计数器系统的重要选择。
4.社交网络
赞/踩、粉丝、共同好友/喜好、推送、下拉刷新等是社交网站的必备功 38
能,由于社交网站访问量通常比较大,而且传统的关系型数据不太适合保存 这种类型的数据,Redis提供的数据结构可以相对比较容易地实现这些功 能。
5.消息队列系统
消息队列系统可以说是一个大型网站的必备基础组件,因为其具有业务 解耦、非实时业务削峰等特性。Redis提供了发布订阅功能和阻塞队列的功 能,虽然和专业的消息队列比还不够足够强大,但是对于一般的消息队列功 能基本可以满足。
redis为什么那么快?
正常情况下,Redis执行命令的速度非常快,官方给出的数字是读写性 能可以达到10万/秒,当然这也取决于机器的性能,但这里先不讨论机器性 能上的差异,只分析一下是什么造就了Redis除此之快的速度,可以大致归 纳为以下四点:
·Redis的所有数据都是存放在内存中的,表1-1是谷歌公司2009年给出的 各层级硬件执行速度,所以把数据放在内存中是Redis速度快的最主要原 因。
·Redis是用C语言实现的,一般来说C语言实现的程序“距离”操作系统更 近,执行速度相对会更快。
·Redis使用了单线程架构,预防了多线程可能产生的竞争问题。
·作者对于Redis源代码可以说是精打细磨,曾经有人评价Redis是少有的 集性能和优雅于一身的开源代码。
通过多个客户端命令调用的例子说明Redis单线程命令处理
机制,接着分析Redis单线程模型为什么性能如此之高。
多个客户端命令调用的例子说明Redis单线程命令处理机制?
开启了三个redis-cli客户端同时执行命令。
// 客户端1设置一个字符串键值对:> set hello world// 客户端2对counter做自增操作:> incr counter// 客户端3对counter做自增操作:> incr counter
每次客户端调用都经历了发送命令、执行命令、返回结果三个过程。Redis客户端与服务端的模型可以简化如图:

为什么单线程还能这么快?
第一,纯内存访问,Redis将所有数据放在内存中,内存的响应时长大 约为100纳秒,这是Redis达到每秒万级别访问的重要基础。
第二,非阻塞I/O,Redis使用epoll作为I/O多路复用技术的实现,再加上 Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。如下图
第三,单线程避免了线程切换和竞态产生的消耗。
非阻塞I/O

单线程的优缺点?


|C字符串|SDS|SDS作用|SDS实现|和C的区别|为什么用SDS|||
关于C语言的字符串?
redisLog(REDIS_WARNING,"Redis is now ready to exit, bye bye...");
redis构建的字符串?
SDS的应用?
SDS的实现?
struct sdshdr {// 记录buf数组中已使用字节的数量等于SDS所保存字符串的长度int len;// 记录buf数组中未使用字节的数量int free;// 字节数组,用于保存字符串char buf[];};
2)示例
·free属性的值为0,表示这个SDS没有分配任何未使用空间。
·len属性的值为5,表示这个SDS保存了一个五字节长的字符串。
·buf属性是一个char类型的数组,数组的前五个字节分别保存了'R'、'e'、'd'、'i'、's'五个字符,而最后一个字节则保存了空字符'\0'。
-- SDS遵循C字符串以空字符结尾的惯例,保存空字符的1字节空间不计算在SDS的len属性里面,并且为空字符分配额外的1字节空间,以及添加空字符到字符串末尾等操作,都是由SDS函数自动完成的,所以这个空字符对于SDS的使用者来说是完全透明的。遵循空字符结尾这一惯例的好处是,SDS可以直接重用一部分C字符串函数库里面的函数。
-- 上两个图展示的SDS的区别在于,这个SDS为buf数组分配了五字节未使用空间,所以它的free属性的值为5(图中使用五个空格来表示五字节的未使用空间)。
SDS与C字符串的区别?

未使用空间,SDS实现的空间预分配和惰性空间释放两种优化策略? - 减少修改字符串时带来的内存重分配次数
解释为什么Redis要使用SDS而不是C字符串?
链表基本知识?
链表和链表节点的实现?
typedef struct listNode {// 前置节点struct listNode * prev;// 后置节点struct listNode * next;// 节点的值void * value;}listNode;

typedef struct list {// 表头节点listNode * head;// 表尾节点listNode * tail;// 链表所包含的节点数量unsigned long len;// 节点值复制函数void *(*dup)(void *ptr);// 节点值释放函数void (*free)(void *ptr);// 节点值对比函数int (*match)(void *ptr,void *key);} list;

Redis的链表实现的特性可以总结如下:
字典的基本理论?
字典的实现?
Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。
接下来的三个小节将分别介绍Redis的哈希表、哈希表节点以及字典的实现。
typedef struct dictht {// 哈希表数组dictEntry **table;// 哈希表大小unsigned long size;// 哈希表大小掩码,用于计算索引值总是等于size-1unsigned long sizemask;// 该哈希表已有节点的数量unsigned long used;} dictht;

typedef struct dictEntry {// 键void *key;// 值union{void *val;uint64_tu64;int64_ts64;} v;// 指向下个哈希表节点,形成链表struct dictEntry *next;} dictEntry;

typedef struct dict {// 类型特定函数dictType *type;// 私有数据void *privdata;// 哈希表dictht ht[2];// rehash索引//当rehash不在进行时,值为-1in trehashidx; /* rehashing not in progress if rehashidx == -1 */} dict;// == type属性 ==typedef struct dictType {// 计算哈希值的函数unsigned int (*hashFunction)(const void *key);// 复制键的函数void *(*keyDup)(void *privdata, const void *key);// 复制值的函数void *(*valDup)(void *privdata, const void *obj);// 对比键的函数int (*keyCompare)(void *privdata, const void *key1, const void *key2);// 销毁键的函数void (*keyDestructor)(void *privdata, void *key);// 销毁值的函数void (*valDestructor)(void *privdata, void *obj);} dictType;

哈希算法?
当要将一个新的键值对添加到字典里面时,程序需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。
#使用字典设置的哈希函数,计算键key的哈希值hash = dict->type->hashFunction(key);#使用哈希表的sizemask属性和哈希值,计算出索引值#根据情况不同,ht[x]可以是ht[0]或者ht[1]index = hash & dict->ht[x].sizemask;
空字典
当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash2算法来计算键的哈希值。
解决键冲突?
当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时,我们称这些键发生了冲突(collision)。
Redis的哈希表使用链地址法(separate chaining)来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这就解决了键冲突的问题。

rehash?
rehash的步骤?
举个例子,假设程序要对图4-8所示字典的ht[0]进行扩展操作,那么程序将执行以下步骤:
-- 执行rehash之前的字典:




哈希表的扩展与收缩?
其中哈希表的负载因子可以通过公式计算得出。
# 负载因子= 哈希表已保存节点数量/ 哈希表大小load_factor = ht[0].used / ht[0].size
例如,对于一个大小为4,包含4个键值对的哈希表来说,这个哈希表的负载因子为:
load_factor = 4 / 4 = 1
渐进式rehash?
一次完整的渐进式rehash过程?






渐进式rehash执行期间的哈希表操作?
跳跃表基本理论?
跳跃表的实现?

zskiplistNode和zskiplist两个结构-zskiplistNode?
跳跃表节点的实现由redis.h/zskiplistNode结构定义:
typedef struct zskiplistNode {// 层struct zskiplistLevel {// 前进指针struct zskiplistNode *forward;// 跨度unsigned int span;} level[];// 后退指针struct zskiplistNode *backward;// 分值double score;// 成员对象robj *obj;} zskiplistNode;





zskiplistNode和zskiplist两个结构-zskiplist?

typedef struct zskiplist {// 表头节点和表尾节点structz skiplistNode *header, *tail;// 表中节点的数量unsigned long length;// 表中层数最大的节点的层数int level;} zskiplist;
-- header和tail指针分别指向跳跃表的表头和表尾节点,通过这两个指针,程序定位表头节点和表尾节点的复杂度为O(1)。
-- 通过使用length属性来记录节点的数量,程序可以在O(1)复杂度内返回跳跃表的长度。
-- level属性则用于在O(1)复杂度内获取跳跃表中层高最大的那个节点的层数量,注意表头节点的层高并不计算在内。
整数集的基本知识?
整数集合的实现
typedef struct intset {// 编码方式uint32_t encoding;// 集合包含的元素数量uint32_t length;// 保存元素的数组int8_t contents[];} intset;
-- contents数组:contents数组是整数集合的底层实现:整数集合的每个元素都是contents数组的一个数组项(item),各个项在数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项。
-- length属性:length属性记录了整数集合包含的元素数量,也即是contents数组的长度。
升级?
压缩列表的基本知识?
压缩列表的构成?


压缩列表的构成-示例?
1)三个节点的
·列表zlbytes属性的值为0x50(十进制80),表示压缩列表的总长为80字节。
·列表zltail属性的值为0x3c(十进制60),这表示如果我们有一个指向压缩列表起始地址的指针p,那么只要用指针p加上偏移量60,就可以计算出表尾节点entry3的地址。
·列表zllen属性的值为0x3(十进制3),表示压缩列表包含三个节点。
2)五个节点的
·列表zlbytes属性的值为0xd2(十进制210),表示压缩列表的总长为210字节。
·列表zltail属性的值为0xb3(十进制179),这表示如果我们有一个指向压缩列表起始地址的指针p,那么只要用指针p加上偏移量179,就可以计算出表尾节点entry5的地址。
·列表zllen属性的值为0x5(十进制5),表示压缩列表包含五个节点。

压缩列表节点的构成?

概述
简化字符串对象,以下统一使用

对象的表示?
typedef struct redisObject {// 类型unsigned type:4;// 编码unsigned encoding:4;// 指向底层实现数据结构的指针void *ptr;// ...} robj;
type属性
ptr指针和encoding属性


string对象编码?
字符串对象编码?



embstr编码?

embstr编码的字符串对象来保存短字符串值的好处?
保存long double类型表示的浮点数
总结并列出了字符串对象保存各种不同类型的值所使用的编码方式。

编码转换?
列表对象的编码?


对象嵌套?
编码转换?
哈希对象的编码?


编码转换?
集合对象的编码
集合对象的编码可以是intset或者hashtable。


编码转换?
有序集合的编码

typedef struct zset {zskiplist *zsl;dict *dict;} zset;
有序集合元素同时被保存在字典和跳跃表中(上图)
为了展示方便,图在字典和跳跃表中重复展示了各个元素的成员和分值,但在实际中,字典和跳跃表会共享元素的成员和分值,所以并不会造成任何数据重复,也不会因此而浪费任何内存。
为什么有序集合需要同时使用跳跃表和字典来实现?
编码转换?
string类型应用场景?
1)缓存功能
比较典型的缓存使用场景,如图
-- 其中,Redis作为缓存层,MySQL作为存储层,绝大部分请求的数据都是从Redis中获取。由于Redis具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。
//1)该函数用于获取用户的基础信息:UserInfo getUserInfo(long id){...}//2)首先从Redis获取用户信息:// 定义键userRedisKey = "user:info:" + id;// 从Redis获取值value = redis.get(userRedisKey);if (value != null) {// 将值进行反序列化为UserInfo并返回结果userInfo = deserialize(value);return userInfo;}//3)如果没有从Redis获取到用户信息,需要从MySQL中进行获取,并将结果回写到Redis,添加1小时(3600秒)过期时间:// 从MySQL获取用户信息userInfo = mysql.get(id);// 将userInfo序列化,并存入Redisredis.setex(userRedisKey, 3600, serialize(userInfo));// 返回结果return userInfo//总结:整个功能的伪代码如下:UserInfo getUserInfo(long id){userRedisKey = "user:info:" + idvalue = redis.get(userRedisKey);UserInfo userInfo;if (value != null) {userInfo = deserialize(value);} else {userInfo = mysql.get(id);if (userInfo != null)redis.setex(userRedisKey, 3600, serialize(userInfo));}return userInfo;}
2)计数
-- Redis作为计数的基础工具,可以实现快速计数、查询缓存的功能,同时数据可以异步落地到其他数据源。
-- 例如笔者所在团队的视频播放数系统就是使用Redis作为视频播放数计数的基础组件,用户每播放一次视频,相应的视频播放数就会自增1:
long incrVideoCounter(long id) {key = "video:playCount:" + id;return redis.incr(key);}
-- 应用:实际上一个真实的计数系统要考虑的问题会很多:防作弊、按照不同维度计数,数据持久化到底层数据源等。

//伪代码给出了基本实现思路:phoneNum = "138xxxxxxxx";key = "shortMsg:limit:" + phoneNum;// SET key value EX 60 NXisExists = redis.set(key,1,"EX 60","NX");if(isExists != null || redis.incr(key) <=5){// 通过}else{// 限速}
哈希应用场景?
| id | name | age | city |
|---|---|---|---|
| 1 | tom | 23 | beijng |
| 2 | mike | 30 | tianjin |

相比于使用字符串序列化缓存用户信息,哈希类型变得更加直观,并且在更新操作上会更加便捷。可以将每个用户的id定义为键后缀,多对fieldvalue对应每个用户的属性,类似如下伪代码:
UserInfo getUserInfo(long id){// 用户id作为key后缀userRedisKey = "user:info:" + id;// 使用hgetall获取所有用户信息映射关系userInfoMap = redis.hgetAll(userRedisKey);UserInfo userInfo;if (userInfoMap != null) {// 将映射关系转换为UserInfouserInfo = transferMapToUserInfo(userInfoMap);} else {// 从MySQL中获取用户信息userInfo = mysql.get(id);// 将userInfo变为映射关系使用hmset保存到Redis中redis.hmset(userRedisKey, transferUserInfoToMap(userInfo));// 添加过期时间redis.expire(userRedisKey, 3600);}return userInfo;}
不同
但是需要注意的是哈希类型和关系型数据库有两点不同之处:
-- 哈希类型是稀疏的,而关系型数据库是完全结构化的,例如哈希类型
每个键可以有不同的field,而关系型数据库一旦添加新的列,所有行都要为其设置值(即使为NULL)
-- 关系型数据库可以做复杂的关系查询,而Redis去模拟关系型复杂查询开发困难,维护成本高。
三种缓存方法-方案的实现方法和优缺点分析。
缓存用户信息:
1)原生字符串类型:每个属性一个键。
set user:1:name tomset user:1:age 23set user:1:city beijing
优点:简单直观,每个属性都支持更新操作。
缺点:占用过多的键,内存占用量较大,同时用户信息内聚性比较差,
所以此种方案一般不会在生产环境使用。
2)序列化字符串类型:将用户信息序列化后用一个键保存。
set user:1 serialize(userInfo)
优点:简化编程,如果合理的使用序列化可以提高内存的使用效率。
缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全
部数据取出进行反序列化,更新后再序列化到Redis中。
3)哈希类型:每个用户属性使用一对field-value,但是只用一个键保
存。
hmset user:1 name tomage 23 city beijing
优点:简单直观,如果使用合理可以减少内存空间的使用。
缺点:要控制哈希在ziplist和hashtable两种内部编码的转换,hashtable会消耗更多内存。
消息队列
lpush+brpop命令组合即可实现阻塞队列,生产者客户端使用lrpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。

文章列表
每个用户有属于自己的文章列表,现需要分页展示文章列表。此时可以
考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。
1|每篇文章使用哈希结构存储,例如每篇文章有3个属性title、timestamp、content:
hmset acticle:1 title xx timestamp 1476536196 content xxxx...hmset acticle:k title yy timestamp 1476512536 content yyyy...
2|向用户文章列表添加文章,user:{id}:articles作为用户文章列表的键(key):
lpush user:1:acticles article:1 article3...lpush user:k:acticles article:5...
3|分页获取用户文章列表,例如下面伪代码获取用户id=1的前10篇文
章:
articles = lrange user:1:articles 0 9for article in {articles}hgetall {article}
使用列表类型保存和获取文章列表会存在两个问题。
第一,如果每次分页获取的文章个数较多,需要执行多次hgetall操作,此时可以考虑使用Pipeline批量获取,或者考虑将文章数据序列化为字符串类型,使用mget批量获取。
第二,分页获取文章列表时,lrange命令在列表两端性能较好,但是如果列表较大,获取列表中间范围的元素性能会变差,此时可以考虑将列表做二级拆分,或者使用Redis3.2的quicklist内部编码实现,它结合ziplist和linkedlist的特点,获取列表中间范围的元素时也可以高效完成。
扩展?
实际上列表的使用场景很多,在选择时可以参考以下口诀:
标签?
实现标签功能?
下面使用集合类型实现标签功能的若干功能。
1|给用户添加标签
sadd user:1:tags tag1 tag2 tag5sadd user:2:tags tag2 tag3 tag5...sadd user:k:tags tag1 tag2 tag4...
2|给标签添加用户
sadd tag1:users user:1 user:3sadd tag2:users user:1 user:2 user:3...sadd tagk:users user:1 user:2...
开发注意问题:开发提示1:
用户和标签的关系维护应该在一个事务内执行,防止部分命令失败造成的数据不一致,有关如何将两个命令放在一个事务,参考事务以及Lua的使用方法。
3|删除用户下的标签
srem user:1:tags tag1 tag5...
4|删除标签下的用户
srem tag1:users user:1srem tag5:users user:1...
3|和4|也是尽量放在一个事务执行。
sinter user:1:tags user:2:tags
zset的应用场景?
使用赞数这个维度,记录每天用户上传视频的排行榜。
主要需要实现以下4个功能:
(1)添加用户赞数
例如用户mike上传了一个视频,并获得了3个赞,可以使用有序集合的zadd和zincrby功能:
zadd user:ranking:2016_03_15 mike 3
如果之后再获得一个赞,可以使用zincrby:
zincrby user:ranking:2016_03_15 mike 1
(2)取消用户赞数
由于各种原因(例如用户注销、用户作弊)需要将用户删除,此时需要
将用户从榜单中删除掉,可以使用zrem。
例如删除成员tom:
zrem user:ranking:2016_03_15 mike
(3)展示获取赞数最多的十个用户
此功能使用zrevrange命令实现:
zrevrangebyrank user:ranking:2016_03_15 0 9
(4)展示用户信息以及用户分数
此功能将用户名作为键后缀,将用户信息保存在哈希类型中,至于用户
的分数和排名可以使用zscore和zrank两个功能:
hgetall user:info:tomzscore user:ranking:2016_03_15 mikezrank user:ranking:2016_03_15 mike
手动触发--save命令?
* DB saved on disk
手动触发--bgsave命令?
* Background saving started by pid 3151* DB saved on disk* RDB: 0 MB of memory used by copy-on-write* Background saving terminated with success
RDB的优点?
RDB的缺点?
简介?
使用?
AOF的工作流程?
AOF的工作流程操作:命令写入(append)、文件同步(sync)、文件重写(rewrite)、重启加载(load)。
流程如下:
1)所有的写入命令会追加到aof_buf(缓冲区)中。
2)AOF缓冲区根据对应的策略向硬盘做同步操作。
3)随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。
4)当Redis服务器重启时,可以加载AOF文件进行数据恢复。
AOF工作流-1命令写入?
// 如set hello world,在AOF缓冲区会追加如下文本:*3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n
AOF为什么直接采用文本协议格式?
AOF为什么把命令追加到aof_buf中?
AOF工作流-2文件同步?

AOF工作流-3文件重写?
重写后的AOF文件为什么可以变小?
1)进程内已经超时的数据不再写入文件。
2)旧的AOF文件含有无效命令,如del key1、hdel key2、srem keys、set a111、set a222等。重写使用进程内数据直接生成,这样新的AOF文件只保留最终数据的写入命令。
3)多条写命令可以合并为一个,如:lpush list a、lpush list b、lpush list c可以转化为:lpush list a b c。为了防止单条命令过大造成客户端缓冲区溢出,对于list、set、hash、zset等类型操作,以64个元素为界拆分为多条。
AOF重写过程可以手动触发和自动触发?
AOF重写运行流程?
流程说明:
1)执行AOF重写请求。
如果当前进程正在执行AOF重写,请求不执行并返回如下响应:
ERR Background append only file rewriting already in progress
如果当前进程正在执行bgsave操作,重写命令延迟到bgsave完成之后再
执行,返回如下响应:
Background append only file rewriting scheduled
2)父进程执行fork创建子进程,开销等同于bgsave过程。
3.1)主进程fork操作完成后,继续响应其他命令。所有修改命令依然写
入AOF缓冲区并根据appendfsync策略同步到硬盘,保证原有AOF机制正确
性。
3.2)由于fork操作运用写时复制技术,子进程只能共享fork操作时的内
存数据。由于父进程依然响应命令,Redis使用“AOF重写缓冲区”保存这部
分新数据,防止新AOF文件生成期间丢失这部分数据。
4)子进程根据内存快照,按照命令合并规则写入到新的AOF文件。每
次批量写入硬盘数据量由配置aof-rewrite-incremental-fsync控制,默认为32MB,防止单次刷盘数据过多造成硬盘阻塞。
5.1)新AOF文件写入完成后,子进程发送信号给父进程,父进程更新
统计信息,具体见info persistence下的aof_*相关统计。
5.2)父进程把AOF重写缓冲区的数据写入到新的AOF文件。
5.3)使用新AOF文件替换老文件,完成AOF重写。
AOF工作流-4重启加载?
* DB loaded from append only file: 5.841 seconds
2)AOF关闭或者AOF文件不存在时,加载RDB文件,打印如下日志:
* DB loaded from disk: 5.586 seconds
3)加载AOF/RDB文件成功后,Redis启动成功。
4)AOF/RDB文件存在错误时,Redis启动失败并打印错误信息。
文件校验
# Bad file format reading the append only file: make a backup of your AOF file,then use ./redis-check-aof --fix <filename>
| 序号 | 主要内容 | 状态 |
|---|---|---|
| 1 | ||
| 2 | ||
| 3 | ||
| 4 | 复制的使用方式:如何建立或断开复制、安全性、只读等 | |
| 5 | 复制可支持的拓扑结构,以及每个拓扑结构的适用场景。 | |
| 6 | 复制的原理,包括:建立复制、全量复制、部分复制、心跳等 | |
| 7 | 复制过程中常见的开发和运维问题:读写分离、数据不一致、规 |
避全量复制等 |
复制概念?
在分布式系统中为了解决单点问题,通常会把数据复制多个副本部署到其他机器,满足故障恢复和负载均衡等需求。Redis也是如此,它为我们提
供了复制功能,实现了相同数据的多个Redis副本。
复制简介?
全量复制|部分复制|复制偏移量|复制积压缓冲区|主节点运行id|psync命令
基本概念
psync命令运行需要以下组件支持?
·主从节点各自复制偏移量。
·主节点复制积压缓冲区。
·主节点运行id。
复制偏移量?

复制积压缓冲区?

主节点运行ID?
如何在不改变运行ID的情况下重启呢?
psync命令?
从节点使用psync命令完成部分复制和全量复制功能,命令格式:psync{runId}{offset},参数含义如下:
·runId:从节点所复制主节点的运行id。
·offset:当前从节点已复制的数据偏移量。
psync命令运行流程?
流程说明:
1)从节点(slave)发送psync命令给主节点,参数runId是当前从节点保存的主节点运行ID,如果没有则默认值为,参数offset是当前从节点保存的复制偏移量,如果是第一次参与复制则默认值为-1。
2)主节点(master)根据psync参数和自身数据情况决定响应结果:
·如果回复+FULLRESYNC{runId}{offset},那么从节点将触发全量复制流程。
·如果回复+CONTINUE,从节点将触发部分复制流程。
·如果回复+ERR,说明主节点版本低于Redis2.8,无法识别psync命令,从节点将发送旧版的sync命令触发全量复制流程。
全量复制?
显然,全量复制是一个非常耗时费力的操作。
部分复制?

基于复制的应用场景。
读写分离
| 序号 | 主要内容 | 状态 |
|---|---|---|
| 1 | 缓存的收益和成本分析 | |
| 2 | 缓存更新策略的选择和使用场景 | |
| 3 | 缓存粒度控制方法 | |
| 4 | 穿透问题优化 | |
| 5 | 无底洞问题优化 | × |
| 6 | 雪崩问题优化 | |
| 7 | 热点key重建优化 |

redis的缓存穿透、缓存击穿、缓存雪崩原因现象和解决措施? |3
redis缓存穿透与解决措施? |3 (rky

String get(String key) {// 从缓存中获取数据String cacheValue = cache.get(key);// 缓存为空if (StringUtils.isBlank(cacheValue)) {// 从存储中获取String storageValue = storage.get(key);cache.set(key, storageValue);// 如果存储数据为空,需要设置一个过期时间(300秒)if (storageValue == null) {cache.expire(key, 60 * 5);}return storageValue;} else {// 缓存非空return cacheValue;}}
2)布隆过滤器拦截
► 如图,在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截。
► 场景:
例如:一个推荐系统有4亿个用户id,每个小时算法工程师会根据每个用户之前历史行为计算出推荐数据放到存储层中,但是最新的用户由于没有历史行为,就会发生缓存穿透的行为,为此可以将所有推荐数据的用户做成布隆过滤器。如果布隆过滤器认为该用户id不存在,那么就不会访问存储层,在一定程度保护了存储层。
► 实现:
有关布隆过滤器的相关知识,可以参考:https://en.wikipedia.org/wiki/Bloom_filter可以利用Redis的Bitmaps实现布隆过滤器,GitHub上已经开源了类似的方案,读者可以进行参考:https://github.com/erikdubbelboer/redis-lua-scaling-bloom-filter。
► 应用场景:
适用于数据命中不高、数据相对固定、实时性低(通常是数据集较大)的应用场景,代码维护较为复杂,但是缓存空间占用少。
► 两种解决方法的对比(实际上这个问题是一个开放问题,有很多解决方法)



缓存策略的问题?

解决?:
互斥锁(mutex key)解决缓存击穿?

String get(String key) {// 从Redis中获取数据String value = redis.get(key);// 如果value为空,则开始重构缓存if (value == null) {// 只允许一个线程重构缓存,使用nx,并设置过期时间exString mutexKey = "mutext:key:" + key;if (redis.set(mutexKey, "1", "ex 180", "nx")) {// 从数据源获取数据value = db.get(key);// 回写Redis,并设置过期时间redis.setex(key, timeout, value);// 删除key_mutexredis.delete(mutexKey);}// 其他线程休息50毫秒后重试else {Thread.sleep(50);get(key);}}return value;}
1)从Redis获取数据,如果值不为空,则直接返回值;否则执行下面的2.1)和2.2)步骤。
2.1)如果set(nx和ex)结果为true,说明此时没有其他线程重建缓存,那么当前线程执行缓存构建逻辑。
2.2)如果set(nx和ex)结果为false,说明此时已经有其他线程正在执行构建缓存的工作,那么当前线程将休息指定时间(例如这里是50毫秒,取决于构建缓存的速度)后,重新执行函数,直到获取到数据。
永远不过期接缓存击穿?

String get(final String key) {V v = redis.get(key);String value = v.getValue();// 逻辑过期时间long logicTimeout = v.getLogicTimeout();// 如果逻辑过期时间小于当前时间,开始后台构建if (v.logicTimeout <= System.currentTimeMillis()) {String mutexKey = "mutex:key:" + key;if (redis.set(mutexKey, "1", "ex 180", "nx")) {// 重构缓存threadPool.execute(new Runnable() {public void run() {String dbValue = db.get(key);redis.set(key, (dbvalue,newLogicTimeout));redis.delete(mutexKey);}});}}return value;}
缓存指标对比解决方案?
