Redis:
C语言编写,基于内存存储
NOSQL适用情况:
- 数据模型比较简单
- 需要灵活性更强的IT系统
- 对数据库性能要求更高
- 不需要高度的数据一致性
- 对于给定key,比较容易映射复杂值的环境
Redis特点:
- 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候继续加载使用
- 不仅支持key-value,还提供list,set,zset,hash等数据结构
- 支持数据的备份,集群等高可用功能
- 优点:
- 丰富的数据结构
- 高速读写,redis使用自己实现的分离器,代码量很短,没有使用Lock
- 缺点:
- 持久化。Redis使用两种方式实现持久化。定时快照:每隔一段时间将整个数据库写到磁盘上,每次均是全部数据,代价非常高。第二种方式基于语句追加(AOF):只追踪变化的数据,但是追加的log可能过大,同时所有的操作均重新执行一遍,回复速度慢。
- 耗内存,占用内存高
数据类型:
String:
Redis的基本类型,是二进制安全的,在传输数据时,保证二进制数据的信息安全,也就是不被篡改,破译等,如果被攻击,能够及时检测出来。一个键最大能存储512MB
Hash:
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。Redis中每个hash可以存储2^32-1键值对。可以看成具有KEY和VALUE的MAP容器,该类型特别适合于存储值对象的信息,该类型的数据仅占用很少的磁盘空间(相比JSON)
List:
Redis列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素到表的头部或者尾部。应用场景:1.用于存储一组数据 2.热点数据(数据量较大的数据做删除/修改功能)3.任务队列、RPOPLPUSH集合A、集合B
Set:
是String类型的无序集合。集合成员是唯一的。集合中不能出现重复的数据。Set底层使用intset和hashtable两种数据结构存储
有序集合ZSet:
String类型元素集合,且不允许重复的成员。每个元素都会关联一个double类型的分数,redis是通过分数来为集合中的成员从小到大排序。集合是通过跳跃表实现的
Redis发布订阅:
Redis发布订阅是一种消息通信模式:发送者发送消息,订阅者接收消息。Redis客户端可以订阅任意数量的频道。应用场景:构建实时消息系统,例如即时聊天,群聊等
Redis多数据库:
Redis下,数据库是由一个整数索引标识,而不是一个数据库名称,默认情况下连接到数据库0
Redis事务:
Redis事务可以一次执行多个命令(按顺序串行化执行,执行中不会被其他命令插入,不许加塞),并且带有以下保证:
·批量操作在发送EXEC命令前被放入队列缓存(从输入Multi命令开始)
·收到EXEC命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
Redis数据淘汰策略:
内存不足时,Redis会根据配置的缓存策略淘汰部分keys,以保证写入成功。当无淘汰策略或没有找到适合淘汰的key时,redis直接返回out of memory错误。
Redis持久化:
存于内存:断电数据丢失
存于硬盘:读写速度慢,数据不丢失
RDB持久化:是redis默认的持久化机制。保存的是一种状态,即将内存中的数据以快照的方式写入到二进制文件中,默认文件名为dump.rdb。
·优点:保存数据极快、还原数据极快,适用于灾难备份
·缺点:小内存机器不适用,RDB机制符合要求就会照快照
AOF持久化:由于快照方式是在一定间隔时间做一次,所以如果redis意外down,就会丢失最后一次快照后的修改,对于不允许丢失任何修改的应用,采用AOF持久化方式。AOF比快照方式有更好的持久性,是由于在使用aof持久化方式时,redis会将每一个收到的写命令通过write函数追加到文件中(默认appendonly.aof)。当redis重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。
产生的问题:aof的方式会使持久化文件变得越来越大
Redis缓存与数据库一致性:
- 实时同步:对强一致性要求比较高的,应采用实时同步方案,即查询缓存查询不到再从DB查询,保存到缓存;更新缓存,先更新数据库,再将缓存的设置过期
- 异步队列:对于并发程度较高的,可采用异步队列的方式同步,可采用kafka等消息中间件处理消息生产和消费。
缓存穿透:
- 指查询一个一定不存在的数据,由于缓存查找不到时需要从数据库中查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库中去查询,造成缓存穿透
- 解决方法:持久层查询不到就缓存空结果,查询时先判断缓存中是否存在,如果有直接返回空,没有则查询后返回
缓存雪崩:
如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩。
这个没有完美的解决办法,但可以分析用户的行为,尽量让失效时间点均匀分布。在缓存失效后,通过加锁或者队列来控制读数据库写缓存的进程数量。
大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上
为什么Redis是单线程的
Redis 内部使用文件事件处理器file event handler,这个文件事件处理器是单线程的,所以Redis才叫做单线程的模型。采用IO多路复用,同时监听多个Socket。
为什么Redis读写效率这么快?
1.Redis将数据存储在内存上,避免了频繁的IO操作
2.Redis其本身采用字典的数据结构,时间复杂度为O(1),且其采用渐进式的扩容手段
3.Redis是单线程的,避免了上下文切换带来的消耗,采用网络IO多路复用技术来保证在多连接,提高了系统的高吞吐量。
数据结构
1.字典
字典底层用哈希表实现。哈希表的结构
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
类似HashMap,使用拉链法解决哈希冲突。
字典的源码:
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
可以看到,字典的数据结构包含了一个长度为2的哈希表数组,这是为了方便进行rehash操作,在扩容时,会将ht[0]上的数据rehash到ht[1],完成rehash后交换两个哈希表的角色。rehash是渐进式完成的,一次性rehash会令服务器负担过大,这就涉及到rehashidx值,从0开始,每次都会把table[rehashidx]进行移动,并令rehashidx++,完成后,rehashidx=-1。
2.跳跃表
跳跃表是ZSET数据类型的底层,基于多指针有序链表实现,从上层开始查到,到对应区间再进入下一层,插入和查询的时间复杂度都是O(logN),和红黑树相比,其插入速度更快、不需要进行旋转等维护平衡性,且更容易实现,支持无锁操作。
Redis集群-复制
命令:从服务器向主服务器发送 SLAVEOF 命令
执行步骤:
- 从服务器向主服务器发送同步命令(SYNC/PSYNC)
- 主服务器执行BGSSAVE命令,在后台生成一个快照(RDB)文件,并使用一个缓冲区记录从现在开始执行的所有命令。
- 主服务器将RDB文件发送给从服务器,从服务器接收并载入这个RDB文件,将自己数据库状态更新至主服务器执行BGSAVE命令时的状态。
- 主服务器将记录在缓冲区里面的所有写命令发送给从服务器,从服务器执行相同命令。
旧版本的复制实现:
从服务器发送SYNC的同步指令,在从服务器断开重连后,主服务器会重新创建快照文件,这个文件包含主服务器中的所有数据,但由于从服务器已经同步了绝大部分数据,这样就会导致效率极低,SYNC命令是非常耗费资源的。新版本的复制实现:
从Redis 2.8开始,使用PSYNC命令代替SYNC命令执行复制时的同步操作。PSYNC命令包含了完整重同步和部分重同步,完整重同步用于初次复制的情况,部分重同步用于处理断线后重复制的情况,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器。
分布式锁
单机场景下,可以使用语言的内置锁来实现进程同步。但在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁。
Redis实现分布式锁
- SETNX命令
使用SETNX指令指令插入一个键值对,如果key已经存在,那么会返回False,否则插入成功并返回True。 - RedLock算法
- 使用了多个Redis来实现分布式锁。尝试从N个相互独立的Redis实例获取锁
- 计算获取锁消耗的时间,只要当这个时间小于锁的过期时间,并且从大多数(N/2+1)实例上获取了锁,那么就认为锁获取成功了
- 如果锁获取失败,就到每个实例上释放锁
应用场景举例:比如一个商城在做秒杀活动,商品的库存有限,我们需要保证每次库存-1都是有效的,如果是只部署在一个节点上的程序,我们当然可以使用synchonize和lock来锁住减库存的代码块,但如果是互不干扰的两个商品A和B同时下单,同一时间内也会只有一个商品能减掉库存,这样效率就很低了。如果使用了Redis实现的分布式锁,我们可以查看相应的key是否存在,如果存在则证明已经被加锁,需要等待锁释放。
哨兵(Sentinel)模式
哨兵通过发送命令,等待服务器响应,从而监控运行的多个Redis实例。当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。
哨兵的工作过程:
1.主观下线(某个哨兵发现了主redis节点故障)
2.客观下线(和其他节点商量,发现超过qnum个哨兵节点都认为主节点故障了,证明她确实故障了)
3.选举出一个领导者哨兵节点,负责处理主节点的故障转移
4.故障转移sentinel领导者的选举
1.每个做主观下线的sentinel节点向其他哨兵节点发送命令,要求将它设置为领导者
2.收到命令的sentinel节点如果没有同意其他sentinel节点发送命令,那么将同意该请求,否则拒绝
3.如果该sentinel节点发现自己的票数已经超过sentinel集合半数且超过quorum,那么它将成为领导者
4.如果此过程有多个sentinel节点成为了领导者,那么将等待一段时间重新选举redis 主节点的选举
1.选择健康状态的从节点
2.选择slave-priority高的从节点
3.选择偏移量大的
4.选择runid小的