注:文中截图来自书籍《redis设计与实现(第二版)》;
Redis用作数据库,缓存(快速),分布式锁,消息代理。
Redis 用 C 语言实现。
高性能的 key-value 数据库。运行在内存。
Redis读的速度是11万次/s,写的速度是8.1万次/s 。
Redis不仅支持简单的key-value类型的数据,还提供list,set,zset,hash等数据结构的存储;
带有生存时间的 key 被称为易失的 (volatile);
Redis 持久化主要是为了数据备份。当redis重启后,可以从磁盘中恢复数据。支持计算集合的并交补等,还支持多种排序功能。可以看成是个数据结构服务器。
k-v缓存还有腾讯开源分布式 NoSQL 存储系统 DCache;Memcached 是内存中的键值存储,最初用于缓存目的。mongodb,redis,hbase 三者都是nosql数据库,不严谨地讲,Redis定位在"快",HBase定位于"大",mongodb定位在"灵活";
【持久化】
所有数据都是保存在内存中,
“半持久化模式”:通过异步方式定时dump到磁盘的一个dump.rdb;RDB
“全持久化模式”:每一次数据变化(查询不会记录)都同步写入到记录文件;append only file,AOF
当 Redis 重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
如果可以承受数分钟以内的数据丢失,那么可以只使用RDB持久化。定时生成RDB快照(snapshot)非常便于进行数据库备份, 并且RDB恢复数据集的速度比AOF快;
看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。
rdb更有些 eventually consistent的意思了。
RDB文件中存储的是安全的二进制内容,AOF方式向文件追加的是Redis操作命令,不是具体数据。
【redis的分布式锁】
Redis 的并发竞争 Key 问题就是多个系统同时对一个 key 进行操作;或保证多节点(计算机/进程)之间数据的一致性,进行同步;
在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用 Redis 自带的 SETNX
命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock
。
【redis的快速】
-
完全基于内存,绝大部分请求是纯粹的内存操作,非常快速
数据存在内存中,类似于HashMap,优势就是查找和操作的时间复杂度都是O(1);
-
数据结构简单,对数据操作也简单,Redis专门设计的数据结构;
-
采用单进程单线程
避免了不必要的上下文切换和竞争条件,也不存在多进程或多线程导致的切换而消耗 CPU
不用去考虑各种锁,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
-
使用多路 I/O 复用模型,非阻塞 IO
-
使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样
Redis 直接构建了自己的 VM 机制
因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
【作缓存】
对于缓存来说,一般都是用来支撑读高并发的
。
所以主从(master-slave)架构,一主多从;主负责写,读请求全部走从节点;【水平扩容】
主从架构 -> 主从复制
replication
、读写分离 -> 水平扩容支撑读高并发
也是利用 Redis 快速查找的特性,redis作查找表时的内容不能失效,而缓存的内容可以失效
因为缓存不作为可靠的数据来源。
问:MySQL里有2000w数据,redis中只存20w数据,如何保证redis中的数据都是热点数据?
将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。
【redis优点总结】
1.快速 2.可持久化 3.提供多种数据结构的存储
Redis运行在内存中但可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。
相比在磁盘上复杂的数据结构(二维表),在内存中操作数据起来非常简单,Redis可以做很多内部复杂性很强的事情。在磁盘格式方面是紧凑的以追加的方式产生的,因为并不需要进行随机访问。
【策略】
1.内存淘汰策略
(数据淘汰策略):
Redis存储超过内存限制时(内存数据集大小上升到一定大小,不足以容纳新写入数据),怎么处理需要新写入且需要额外空间的数据。
淘汰算法的本意是保留那些将来最有可能被再次访问的数据;
LRU算法只是预测最近被访问的数据将来最有可能被访问到;
Least Recently Used
LFU算法也就是最频繁被访问的数据将来最有可能被访问到;
Least Frequently Used
在设置了过期时间的键中:
volatile-lru:移除最近最少使用的key
volatile-lfu:删除最不频繁被访问的key
volatile-random:随机删除一个key
volatile-ttl:有更早过期时间的key优先移除
在键空间中:
allkeys-lru:移除最近最少使用的key(这个是最常用的)
allkeys-lfu:删除最不频繁被访问的key
allkeys-random:随机删除一个或者多个key
noeviction:新写入操作会报错
总结:【最近最少使用、最不频繁被访问、随机、will更早过期的优先、新写入操作报错】
2.缓存过期策略
(内存过期/失效策略):
当Redis中缓存的key过期了,Redis如何处理。
定时策略:每个expired key绑定一定时器,到期立即清除;定时器创建耗时,严重影响cpu性能
惰性策略:删除操作只发生在client请求key时,只删除当前key;may 内存泄露
定期策略:折中;隔一定时间扫描数据库expires字典中随机抽取一些key,清除一批已过期的
需要考虑过期的缓存是否会持久化(被写入磁盘)?如果写入又是怎么处理的?
RDB:持久化key之前检查是否过期,过期的不进入RDB文件。数据恢复到内存前对key先进行过期检查,过期的不导入数据库(主库情况)。
AOF:当key过期后,在发生删除操作时,
程序会向aof文件追加一条del命令
,在将来的以aof文件恢复数据时该过期的键就会被删掉。当key过期后,还没有被删除,此时进行执行持久化操作,该key是不会进入aof文件的。
【key 读miss】
key默认不过期,当配置中开启了‘超出最大内存限制就写磁盘
’的话,那么没有设置过期时间(默认的)key可能会被写到磁盘上,没修改设置的话,redis将使用LRU机制(‘最近最少使用’ 缓存机制),将内存中的老数据删除,并写入新数据 --> 被Redis主动地从实例中删除,从而产生读miss
的情况。
【常用指令】
shell开启redis服务:redis-server
客户端:
设置密码:
CONFIG SET requirepass "123happyday"
客户端登陆:
AUTH 123happyday
16个数据库(0-15)的选择:默认0;
SELECT 7
typedef struct redisObject{ unsigned type:4; //类型(key:sds/value:5种) unsigned encoding:4; //对象所使用的编码 void *ptr; //指向对象的底层实现数据结构 }robj; //ptr指针指向的数据结构由对象的encoding属性决定。 //encoding属性记录了对象所使用的编码, //即这个对象使用了什么数据结构作为对象的底层实现, //这个属性的值可以是列出的8个常量其中一个。
删除某key:
DEL runoob_list
;查看所有key:KEYS *
查看底层实现结构:
OBJECT ENCODING lst
查看键对应的值的类型:
TYPE lst
对一个已带有生存时间的 key 执行
EXPIRE
命令,新指定的生存时间会取代旧的生存时间。
PERSIST
可设置key永不过失。
缓存有效时间可以设置短点,如30秒
(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击。
【缓存预热】
关闭外网访问,先开启mysql,通过预热脚本将热点数据写入缓存中,启动缓存,开启外网服务。(上线时手动/人工操作加载缓存);
【redis雪崩】
redis服务由于负载过大宕机,导致mysql负载过大也宕机,最终整个系统瘫痪。
解决方法:
redis集群:将原来一个人干的工作,分发给多个人干;
服务降级:系统可以根据一些关键数据进行
自动降级
,也可以配置开关实现人工降级
。服务降级目的是保证核心服务可用,即使是有损的。有些服务是无法降级的(如加入购物车、结算)。redis缓存降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。
数据不要同时设置相同的生存时间,不然过期时,redis压力会大;
【redis穿透】
高并发下,由于key失效,导致多个线程去mysql查同一数据并存到redis(并发下,存了多份数据);一段时间后,多份数据同时失效,redis压力骤增。
解决方法:
计划任务(假如数据生存时间为30分钟,计划任务就20分钟执行一次更新缓存数据)
分级缓存(缓存两份数据,第二份数据生存时间长一点作为备份,第一份数据用于被请求命中,如果第二份数据被命中说明第一份数据已经过期,要去mysql请求数据重新缓存两份数据)
接口层基础校验(不合理的数据)+布隆过滤器拦截(一定不存在的数据)+加锁访问mysql
【数据使不使用缓存】
- 热点数据:缓存才有价值
- 冷数据:大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。
- 频繁修改的数据:看情况考虑使用缓存;使用的话:数据是热点又常修改:缓存+同步;
【内存优化】
一个是Redis Server本身对内存的优化,一个是应用方面的优化。
1. Redis Server层面降低内存占用
- 存储编码
Redis存储的数据都使用redisObject
结构体来封装,在redisObject中有个encoding
字段,表示Redis内部编码类型,同一个对象采用不同的编码实现内存占用存在明显差异
max size:
-5 到 -1
64KB 到 4KB ; -2 -1 good
- 共享对象池(Java中也存在类似优化)
Redis内部维护了[0-9999]
的整数对象池,用于节约内存;
list、hash、set和zset类型的内部元素都可以使用整数对象池;
对象共享意味着多个引用共享同一个RedisObject。
- 字符串优化 (simple dynamic string,
SDS
)
Redis没有采用原生C语言的字符串类型,而是自己实现了字符串结构:简单动态字符串。
其内部实现空间预分配机制,降低内存再分配次数。
要防止预分配带来的内存浪费。尽量减少字符串频繁修改操作append、setrange,改为直接使用set修改字符串,降低预分配带来的内存浪费和内存碎片。
2. 应用层面降低内存占用
- 数据模型抽象匹配
根据不同数据结构的特点,针对性的利用Hash,list,zset,set等集合类型数据结构,降低外层键的数量;
使用hash代替多个key-value,尽可能将数据模型抽象到一个散列表里。
- 缩减键值对象
key长度,在完整描述业务情况下,越短越好;
value对象缩减比较复杂,常见的做法是把业务对象序列化成二进制数组放入Redis,这时就要选择更高效的序列化工具。值对象除了存储二进制数据之外,通常还会使用通用格式存储数据比如json、xml等作为字符串存储在Redis中,可使用通用压缩算法压缩json、xml后再存入。
【redis的数据库】
Redis不支持自定义数据库的名字,每个数据库都以编号命名(0-15),开发者必须自己记录哪些数据库存储了哪些数据。另外,Redis也不支持为每个数据库设置不同的访问密码,所以一个客户端要么可以访问全部数据库,要么连一个数据库也没有权限访问。最重要的一点是多个数据库之间并不是完全隔离的,比如FLUSHALL
命令可以清空一个Redis实例中所有数据库中的数据。
综上所述,Redis这些数据库更像是一种命名空间
,而不适宜存储不同应用程序的数据。比如可以使用0号数据库存储某个应用生产环境中的数据,使用1号数据库存储测试环境中的数据,但不适宜使用0号数据库存储A应用的数据而使用1号数据库B应用的数据,不同的应用应该使用不同的Redis实例存储数据。由于Redis非常轻量级,一个空Redis实例占用的内在只有1M左右,所以不用担心多个Redis实例会额外占用很多内存。
【事务】
Redis所有单操作都是原子性的,意思就是要么成功执行要么失败。
单个操作是原子性的;Redis 事务的执行并不是原子性的。
使用命令 MULTI
开启事务;由 EXEC
命令触发 一并执行事务中的所有命令。
redis的事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作;
没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
事务中任意命令执行失败,其余的命令仍会被执行。中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。 要么全部执行,要么全部不执行。但不是要么全部执行成功,要么全部执行失败。
WATCH
指令类似于乐观锁:
在事务提交时,如果WATCH监控的多个key中的任何值若已被其他客户端更改,则使用EXEC执行事务时,事务队列将不被执行,同时返回Nullmulti-bulk
应答以通知调用者事务执行失败,WATCH对变量的监控也被取消。
故当事务执行失败后,需重新
WATCH
变量进行监控,再MULTI
新的事务进行操作。
EXEC
隐含 UNWATCH
作用:一旦执行EXEC,之前加的监控锁都会被取消 。
UNWATCH
:取消对所有key的监控;
DISCARD
:取消事务,放弃执行事务块内的所有命令。
丢弃缓存队列中的所有命令;不是回滚 (sql:
rollback to
xx) ;
【网络事件处理】file event handler
Redis Server 使用单线程单进程的方式处理命令请求。
非阻塞多路复用模型;
Redis基于Reactor
模式开发了网络事件处理器,也称为文件事件处理器。
组成结构4部分:多套接字监听、IO多路复用程序、文件事件分派器、事件处理器。
因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。
虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字,实现与多个客户端进行网络通信。
【连接】
Redis 通过监听一个 TCP 端口或者 Unix socket 的方式来接收来自客户端的连接;
首先,客户端 socket 会被设置为非阻塞模式,然后为这个 socket 设置
TCP_NODELAY
属性,禁用 Nagle 算法,然后创建一个可读的文件事件用于监听这个客户端 socket 的数据发送。
【redis客户端】
- Redission
Redisson是一个高级的分布式协调Redis客户端,实现了分布式环境可用的和可扩展的Java数据结构如
Bloom filter, BitSet, Set, SetMultimap, ScoredSortedSet, SortedSet, Map, ConcurrentMap, List, ListMultimap, Queue, BlockingQueue, Deque, BlockingDeque,Semaphore, Lock, ReadWriteLock, AtomicLong, CountDownLatch, Publish / Subscribe,HyperLogLog
- Jedis
Jedis是Java实现的客户端,仅支持基本的数据类型如:String、Hash、List、Set、Sorted Set
;
总结:
Jedis中方法调用是比较底层的暴露的Redis的API,也即Jedis中的Java方法基本和Redis的API保持着一致;Redisson中方法则是进行抽象,每个方法调用可能进行了一个或多个Redis方法调用。
Jedis使用阻塞I/O,且其方法调用都是同步的,程序流需要等到socket处理完I/O才能执行,不支持异步。Jedis客户端实例不是线程安全的,需要通过连接池来用Jedis:ThreadPool/JedisPool;Redisson使用非阻塞I/O和基于Netty框架的事件驱动的通信层,其方法调用是异步的。Redisson的API是线程安全的,所以可以操作单个Redisson连接来完成各种操作。
【key的类型】
数据库中可存的key(字符串)的值(五种)类型:
- string: 字符串、整数、浮点数 encoding : int(底层long) / embstr / raw
SET
runoob “abc” /
GET
runoob;
- hash(map): 包含键值对的无序散列表; encoding : ziplist / ht
HMSET
runoob run1 “a” run2 “b” run3 “cde”/
(单个key查) HGET
runoob run1 /
(多个) HGETALL
… /
HDEL
runoob “a” b cde /
HEXITS HKEYS HVALS HLEN
- list: 可对单个或者多个元素进行修剪 ; encoding : ziplist / linkedlist
LPUSH/RPUSH
runoob_list redis redis1 / LPOP/RPOP
(查范围,有序)LRANGE
runoob_list 0 10 /
(元素总数)LLEN
runoob_list
- set: 常见是 string 类型的无序集合; encoding : intset / ht
SADD
runoob_set redis mongodb /
(查所有,无序)SMEMBERS
runoob_set /
(remove element)SREM
/
(元素总数)SCARD
runoob
- zset :去重但可以排序的集合 encoding : ziplist / skiplist
应用举例:
string 可对整个字符串或者字符串子串执行操作;对整数和浮点数执行自增或自减操作,作计数器;
hash 可存储结构化数据;获取所有k-v;检查某个k是否存在;
list 存储些列表型的数据,类似粉丝列表、文章的评论列表之类的数据 ;
【异步阻塞队列】使用
list
类型保存数据信息,rpush
生产消息,lpop
消费消息,当lpop没有消息时,可以sleep段时间,再检查有没有来消息,如不想sleep,可以使用blpop
, 在没有信息的时候,会一直阻塞,直到信息到来
set 可以实现交集、并集等操作,从而实现共同好友等功能;
zset 可以实现有序性操作,从而实现排行榜等功能。
【延时队列】使用时间戳做score, 消息内容作为key,调用
zadd
来生产消息,消费者使用
zrangbyscore
获取n秒之前的数据做轮询处理。
【底层数据结构】
Redis本质上是个数据结构服务器,以高效的方式实现了多种数据结构。当我们提到Redis的“数据结构”,是在两个不同的层面来讨论它:
第一个层面,是从使用者的角度。
比如:string/list/hash/set/sorted set这一层面也是Redis暴露给外部的调用接口。
第二个层面,是从内部实现的角度,属于更底层的实现。
比如:dict/sds(动态字符串)/ziplist/quicklist/skiplist
Redis如何通过组合第二个层面的各种基础数据结构来实现第一个层面的更高层的数据结构。
- [
SDS
简单动态字符串]embster / raw
为确保Redis可以适用于各种不同的使用场景, SDS的API都是二进制安全的(binary-safe ),所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据;这也是我们将SDS的buf属性称为字节数组的原因:Redis用这个数组不是来保存字符,而是来保存一系列二进制数据。
通过使用二进制安全的SDS,而不是C字符串,使得Redis不仅可以保存文本数据,还可以保存任意格式的二进制数据。
- [双向无环链表]
linkedlist
typedef struct list{
listNode *head;
listNode *tail;
unsigned long len;
*match();
*dup();
*free();
}list;
作为一种常用数据结构,链表内置在很多高级的编程语言里面,因为Redis使用的c语言并没有内置这种数据结构,所以Redis构建了自己的链表实现。
链表在Redis中的应用非常广泛,比如列表键的底层实现
之一
就是链表。当一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时, Redis就会使用链表作为列表键的底层实现。
- [字典/符号表/map/关联数组]
ht hashtable
字典经常作为一种数据结构内置在很多高级编程语言里,但Redis所使用的c语言并没有内置这种数据结构,因此Redis构建了自己的字典实现。
//Redis的字典使用哈希表作为底层实现;
//一个哈希表里面可以有多个哈希表节点,每个哈希表节点保存了字典中的一个键值对。
//哈希表结构:
typedef struct dictht{
dictEntry **table;
unsigned long size;
unaigned long used;
}dictht;
//字典
Typedef struct dict{
dictType *type; //类似于泛型 //中有计算哈希值的函数:*hashFunction(*key);
void *privdata; //与上
dictht ht[2]; //数组中的每个项都是一个dictht哈希表;
//一般情况下,字典只使用ht[0], ht[1]只会在对ht[0]哈希表进行rehash时使用。
int trehashidx;
}
字典在Redis中的应用相当广泛,比如Redis的数据库就是使用字典来作为底层实现的;对数据库的增、删、查、改操作也是构建在对字典的操作之上的。除了用来表示数据库之外,字典还是哈希键的底层实现之一,当一个哈希键包含的键值对 比较多(
>512
),又或者键值对中的元素都是比较长的字符串(>64B
)时,Redis就会使用字典作为哈希键的底层实现。rehash:维持负载因子合理;ht[0]哈希表, ht[1]哈希表;渐进式rehash
- [跳跃表]
skiplist
和链表、字典等广泛应用的结构不同,Redis内部只在两个地方用到了跳跃表:一个是实现有序集合键,另一个是在集群节点中用作内部数据结构;
//跳跃表节点
typedef struct zskiplistNode{
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
}level[];
//跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,层的数量越多,访问其他节点的速度就越快。
struct zskiplistNode *backward;
double score; //在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。跳跃表中的节点按照分值从小到大进行排序,当分值相同时,节点按照成员对象的大小进行排序。
robj *obj;//--字符串对象--SDS值
} zskiplistNode;
//创建一个新跳跃表节点时,根据幂次定律(越大的数出现的概率越小)随机生成一个介于【1和32】之间的值作为leve1数组的大小,这个大小就是层的“高度”。
typedef struct zskiplist{
struct zskiplistNode *head, *tail;
unsigned long len;
int level; //最大的节点层数
}zskiplist;
Redis使用跳跃表作为有序集合键(sorted set)的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员(member)是比较长的字符串时,Redis就会使用跳跃表来作为有序集合键的底层实现。
- [整数集合]
intset
整数集合是集合键(set)的底层实现之一;当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。(有序,无重复)
因为C语言是静态类型语言,为了避免类型错误,我们通常不会将两种不同类型的值放在同一个数据结构里面。例如,我们一般只使用int16-t类型的数组来保存int16t类型的值,只使用int32t类型的数组来保存int32 t类型的值,诸如此类。但是,因为整数集合可以通过自动升级底层数组来适应新元素,所以我们可以随意地将
int16_t int32_t或者int64_t
类型的整数添加到同一集合中,而不必担心出现类型错误,这种做法非常灵活,也很节省内存。 (不支持降级)
- [压缩列表]
ziplist
for 节省内存 :特殊编码 连续内存块 组成的 顺序型数据结构
压缩列表是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。当一个哈希键只包含少量键值对,且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做哈希键的底层实现。
redis> RPUSH lst 1 3 5 10086 "hello" "world"
(integer) 6
redis> OBJECT ENCODING lst
"ziplist"
第一第二层面对应总结:
【redis cluster模式】
//分布式寻址算法
-
hash 算法(大量缓存重建)
-
一致性 hash 算法
(自动缓存迁移)+虚拟节点
(自动负载均衡)redis cluster 的 hash slot 算法
每key通过CRC16校验后对16384取模来决定放置/取自哪个槽,集群每个节点负责一部分槽
-
无中心架构,支持动态扩容,对业务透明
//redis 集群模式的工作原理?在集群模式下,redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?
- 所有的节点相互连接;
- 节点间通过集群总线通信,集群总线端口大小为
客户端服务端口+10000
,
这个10000是固定值; - 节点间通过TCP通道和P2P二进制协议
gossip协议
进行通信; - 客户端和集群节点之间通信和通常一样,通过TCP和
文本协议
进行; - 集群节点不用代理查询;
总结:
Redis Cluster是一种服务端Sharding(分区)技术,3.0版本开始正式提供。
Redis Cluster没有使用一致性hash,而是采用
hash slot
(槽)的概念,一共分成16384个槽。
自动故障转移
(Sentinel集群,至少3个) +自动缓存迁移
+自动负载均衡
(分布式寻址算法)客户端只需将请求发送到任意节点,服务端路由查询;
客户端直连redis服务,免去了proxy代理的性能损耗。(
pwemproxy,codis
集群方案有proxy)
运维复杂,数据迁移需要人工干预,只能使用0号数据库,不支持批量操作(pipeline管道操作)…
-
数据分片:by key的哈希值
one key to node; 每个节点均分对应一定数量的哈希槽(哈希值区间)
每份数据分片 to存储在 多个互为主从的节点上
-
数据写入:先写主节点,再同步到从节点 (支持配置为阻塞同步)
同一分片多个节点间的数据不保持一致性
redis cluster 节点间采用
gossip
协议进行通信。[16347] -
读取数据:先将数据key经hash判断
若没有分配在该节点上-> 落在对应的hash slots,redis会返回转向指令,指向正确的节点
-
扩容时需要需要把旧节点的数据迁移一部分到新节点
-
在 redis cluster 架构下,每个 redis 要放开两个端口号
比如一个是 6379,另外一个就是 加1w的端口号,比如 16379。
16379 端口号是用来进行节点间通信的,也就是cluster总线的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议, gossip 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。
【集群的主从复制模型】
为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型
,每个节点都会有N-1个复制品。(高可用)
每秒请求数(QPS)
- 过程:
当启动一个 slave node 时,它会发送一个 PSYNC
命令给 master node;
如果这是slave初次连接到master,那么会触发一次
full resynchronization
全量复制。
此时 master 会启动一个后台线程,开始生成一份 dump.rdb
快照文件,并将期间接收到的所有写命令缓存在内存中。
RDB 文件生成完毕后, master 会将这个.rdb
发送给 slave;
slave会先写入本地磁盘,再从本地磁盘载入到内存中;
接着master会将内存中缓存的写命令
发送到 slave,slave 也会执行这些命令;
slave跟master网络故障断开了连接,会自动重连,连接后master仅会复制给slave部分数据。
主实例宕机,都会自动故障迁移,slave 会自动变成 master 继续提供读写服务。
【集群分区】
分区是对数据来说的;Redis Cluster对Redis实例来说的。
(Redis集群:多个Redis实例组成的整体)
Redis Cluster本身提供了自动将数据分散到不同节点的能力;
一个数据集合被多个Redis实例同时服务;
一个Redis实例可服务多个来自不同数据集合的数据;
分区实现的关键点:
如何将数据自动地打散到不同的节点,使得不同节点存储数据相对均匀;
对用户来说,只关注这个数据集合,而整个集合的某个数据子集究竟存储在哪个节点对于用户来说是透明的,或者说不关心的。
【MYSQL与Redis】
MySQL和Redis不是竞争关系;当并发访问量比较大时,特别是读操作多,架构中可以引入Redis提升整体性能,减少Mysql(或其他关系型数据库)的压力;
不是MySQL or Redis;而是MySQL + Redis ;
【Redis应用】
因为Redis的性能十分优越,可以支持高速读/写操作,并且它还支持持久化、集群部署、分布式、主从同步等,在高并发的场景下数据的安全和一致性,所以它经常用于这些场景:
-
经常要被查询R,但是CUD操作频率低的数据;
比如数据字典,确定了之后很少被修改,是可以放到缓存中的;还有热点数据,查询极为频繁的数据,放到Redis中可以减少MySQL的压力;
缓存中的一个key(比如一个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这时候大并发的请求可能会瞬间把后端DB压垮。
解决方案:
采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,不存在的数据一定会被这个
bitmap
拦截(may在的被当成不在),避免了对底层存储系统的查询压力如果KEY不存在,
加锁
,然后查DB
,入缓存
,然后解锁
;其他进程如果发现有锁就等待
解锁后返回数据或者进入DB查询; -
经常被查询,但是实时性要求不高的数据;
比如购物网站的热销排行榜,定时统计一次后把统计结果放到 Redis中提供查询(请不要每次都使用Mysql 的
select top 10 from xxxx
)。 -
缓存还可以做数据共享(Session共享/缓存);
在分布式的架构中,把用户的Session数据缓存到Redis中。
-
高并发场景下的计数器;
比如秒杀,把商品库存数量放到Redis中(秒杀的场景会比较复杂,Redis只是其中之一,例如如果请求超过某个数量的时候,多余的请求就会被限流);
-
因为Redis对高并发的支持和单线程机制,它也经常用作分布式锁;
【引入Redis】
项目在引入Redis的时候,需要考虑的问题比较多:
-
首先要判断数据是否适合缓存到Redis中
数据会被经常查询么?命中率如何?写操作多么数据?大小?数据一致性如何保证?
-
经常采用这样的方式将数据刷到Redis中:
查询请求过来,
先查
询Redis,如果查询不到,再查
询数据库拿到数据,再放
缓存中;这样第二次相同的查询请求过来,就可直接在Redis中拿到数据;要注意【缓存穿透】的问题。
-
缓存的刷新会比较复杂
通常修改完数据库后,还需要对Redis中的数据进行操作;
代码简单,但需要保证这两步为同一事务,或【最终事务一致性】。