Redis学习缓存持久化哨兵模式
Posted 程序dunk
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis学习缓存持久化哨兵模式相关的知识,希望对你有一定的参考价值。
个人博客欢迎访问
总结不易,如果对你有帮助,请点赞关注支持一下
我写代码是为了更好的表达自我,这是艺术创作,而不单单是为了把事情搞定。 —Antirez
目录
Redis背景
NoSQL
什么是NoSQL
NoSQL = Not Only SQL(不仅仅是SQL)
泛指非关系型数据库,常用的都是关系型数据库。就像我们常用的mysql,sqlServer一样,这些数据库一般用来存储重要信息,应对普通的业务是没有问题的,但是,随着互联网的高速发展,传统的关系性数据库在应对超大规模超大流量以及高并发的时候力不从心。
存储结构
关系型数据库对应的是结构化数据,数据表都是预先定义了结构(列的定义),结构描述了数据的形式和内容,这一点对数据建模至关重要,虽然预定义结构带来了可靠性和稳定性,但是修改这些数据比较困难。
NoSQL数据库基于动态结构,使用于非结构化数据,因为NoSQL数据库是动态结构,可以很容易适应数据结构类型和结构的变化
NoSQL的特点
- 方便扩展(数据之间没有关系)
- 大数据量高性能(Redis一秒写8万次,读取11万次)
- 数据类型多样型(不需要实现设计数据库,随取随用)
传RDBMS和NoSQL
RDBMS
结构化组织、SQL、数据和关系都存在单独的表中、操作数据,数据定义语言、严格的一致性、基础的事务
NoSQL
不仅仅是数据、没有固定的查询语言、键值对存储、列存储、文档存储、图形化存储、最终一致性、CAP定理和BASE理论、高性能、高可用、高可扩展3
NoSQL的四大分类
KV键值对
- 新浪:Redis
- 美团:Redis + Tair
- 阿里、百度:Redis + memecache
文档型数据库
- MongoDB
- MongoDB是一个基于分布式文件存储的数据库,C++编写,主要用来处理大量的文档
- MonoDB是一个介于关系型数据库和非关系性数据库中中间的产品(MongoDB是非关系型数据库中功能最丰富的,最像关系型数据库)
- ConthDB
列存储
- HBase
- 分布式文件系统
图形化数据库
- 他不是存图形的,放的是关系,比如:朋友圈社交网络、广告推荐!
- Neo4J、InfoGrid
分类 | Examples举例 | 典型应用场景 | 数据模型 | 优点 | 缺点 |
---|---|---|---|---|---|
键值(key-value) | Tokyo Cabinet/Tyrant, Redis, Voldemort, Oracle BDB | 内容缓存,主要用于处理大量数据的高访问负载,也用于一些日志系统等等 | Key 指向 Value 的键值对,通常用hashtable来实现 | 查找速度快 | 数据无结构化,通常只被当作字符串或者二进制数据 |
列存储数据库 | Cassandra, HBase, Riak | 分布式的文件系统 | 以列簇式存储,将同一列数据存在一起 | 查找速度快,可扩展性强,更容易进行分布式扩展 | 功能相对局限 |
文档型数据库 | CouchDB, MongoDb | Web应用(与Key-Value类似,Value是结构化的,不同的是数据库能够了解Value的内容) | Key-Value对应的键值对,Value为结构化数据 | 数据结构要求不严格,表结构可变,不需要像关系型数据库一样需要预先定义表结构 | 查询性能不高,而且缺乏统一的查询语法。 |
图形(Graph)数据库 | Neo4J, InfoGrid, Infinite Graph | 社交网络,推荐系统等。专注于构建关系图谱 | 图结构 | 利用图结构相关算法。比如最短路径寻址,N度关系查找等 | 很多时候需要对整个图做计算才能得出需要的信息,而且这种结构不太好做分布式的集群方案。 |
Redis入门
Redis概述
Remote Dictionary Server(Redis)
Redis是一个开源的使用C语言编写、遵循BSD协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库,并提供多种语言的API。
Redis 通常被称为结构化数据库,因为值(value)可以是字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets)等类型。
与传统数据库不同的是Redis的数据是存储在内存中的,所以读写的速度非常快,因此Redis被广泛应用于缓存方向,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。另外,Redis 也经常用来做分布式锁。除此之外,Redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。
Redis可以做什么
- Redis支持数据的持久化(rdb、aof),可以将内存中的数据保存在磁盘中,重启的时候再次加载使用
- Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
- Redis支持数据的备份,即master-slave模式的数据备份。
- 效率高,可以用于高速缓存
- 发布订阅系统
- 地图信息分析
- 计时器、计数器(浏览量)
Redis的优缺点
优点
- 读写性能优异,Redis能读的速度是110000次/s,写的速度是81000次/s
- 支持数据持久化,支持AOF和RDB两种持久化方式
- 支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行
- 数据结构丰富,除了支持string类型的value外还支持hash、set、zset、list等数据结构
- 支持主从复制,主机会自动同步到从机,可以进行读写分离
缺点
- 数据库容量受到物理内存的限制,不能做海量数据的高性能读写,因此Redis适合的场景主要是局限在数据量较小的高性能操作和运算上
- Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复
- 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
- Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。
为什么要用缓存
主要从“高性能”和“高并发”这两点来看待这个问题。
高性能
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
高并发
直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
为什么使用Redis而不使用map/guava做缓存
缓存分为本地缓存和分布缓存。以Java为例,自带的map或者guava实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着JVM的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性
使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。
Redis为什么快
关系型数据库跟Redis本质上的区别
- 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速,数据存在内存中,类似以HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
- 数据结构简单,对数据操作也简单,Redis中的数据结构是专门设计的
- 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换消耗CPU,不用考虑锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗,不会浪费多核CPU,因为可以通过单机开多个Redis实例
Redis在处理客户端的请求时,包括获取(socket读)、解析、执行、内容返回(socket写)等都是由一个顺序串行的主线程处理的,这就是所谓的“单线程”。但如果严格来讲,Redis4.0之后并不是单线程,除了主线程外,它也有后台线程在处理一些较为缓慢的操作,例如清理脏数据、无用链接释放、大key的删除等等
使用Redis时,几乎不存在CPU成为瓶颈的情况, Redis主要受限于内存和网络。6.0版本带来了多线程特性,因为读写网络的read/write系统调用占用了了Redis执行期间大部分CPU时间,瓶颈主要在于网络的 IO 消耗。多线程任务可以分摊Redis同步IO读写负荷。
Redis的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。
- 使用非阻塞I/O多路复用模型,多路指的是多个socket连接,复用指的是复用一个线程,Redis使用epoll作为I/O多路复用技术的实现,在加上Redis自身的事件处理模式将epoll的read、write、close等都转换成事件,不在网络I/O浪费过多的时间
- 阻塞式IO(处理一个socket就要占用一个线程)让出CPU,进到等待队列,等socket就绪后再次获取时间片继续执行
- 非阻塞式IO,不让出CPU,频繁检查socket就绪状态,忙等待,难把握轮询间隔,空耗CPU
- IO多路复用(一次系统调用,监听多个socket),操作系统提供支持,把需要等待的socket加入到监听集合。
epoll是Linux内核为处理大批量文件描述符而做了改进的poll,是Linux下多路复用I/O接口select/poll的增强版本,他能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率
当多个请求发送到服务端的时候,实际上会有一个文件事件处理器同时监听多个套接字,并且根据套接字目前执行的任务来关联不同的事件处理器。
事件处理器只需要将他们做绑定即可,IO多路复用程序时会将所有产生的套接字都存入一个有序且同步的队列中,最后Redis会逐一对这个队列中的元素进行处理
epoll没有最大并发连接的限制,只管你“活跃”的连接 ,而跟连接总数无关。Epoll使用了“共享内存 ”,省去内存拷贝。
- 使用底层模型不同,他们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求
Redis的使用场景
计数器
可以对String进行自增自减运算,从而实现计数器的动能,Redis这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量
缓存
将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率Redis安装
会话缓存
可以使用 Redis 来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。
全页缓存(FPC)
除基本的会话token之外,Redis还提供很简便的FPC平台。以Magento为例,Magento提供一个插件来使用Redis作为全页缓存后端。此外,对WordPress的用户来说,Pantheon有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。
查找表
例如 DNS 记录就很适合使用 Redis 进行存储。查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。
消息队列(发布/订阅功能)
List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息。不过最好使用 Kafka、RabbitMQ 等消息中间件。
分布式锁实现
在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。
Redis安装
windows安装
双击运行服务 redis-server.exe
启动Redis服务器
运行成功
再次运行redis客户端 启动 redis-cli.exe
127.0.0.1:6379> ping ----> 测试是否连接成功
PONG
127.0.0.1:6379> set name changan -----> 设置 key value
OK
127.0.0.1:6379> get name ------> 用 key 去寻找 value
"changan"
Linux安装
下载、解压、编译Redis
$ wget http://download.redis.io/releases/redis-6.0.6.tar.gz
$ tar xzf redis-6.0.6.tar.gz
$ cd redis-6.0.6
make
make install
make报错
目前Redis官网下载的版本为 6.0版本 make安装
会报错是因为gcc版本过低,yum安装的gcc是4.8.5的。因此需要升级gcc,升级过程如下:
[root@hadoop01 redis-6.0.5]# gcc -v # 查看gcc版本
[root@hadoop01 redis-6.0.5]# yum -y install centos-release-scl # 升级到9.1版本
[root@hadoop01 redis-6.0.5]# yum -y install devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils
[root@hadoop01 redis-6.0.5]# scl enable devtoolset-9 bash
以上为临时启用,如果要长期使用gcc 9.1的话:
[root@hadoop01 redis-6.0.5]# echo "source /opt/rh/devtoolset-9/enable" >>/etc/profile
安装c++环境
yum install gcc-c++
进入程序安装目录,拷贝配置文件
cd /usr/local/bin
cp /opt/redis-6.0.6/redis.conf config/
修改配置文件,让Redis后台启动
daemonize yes
启动Redis服务(通过指定文件启动)
redis-server config/redis.conf
#连接测试
redis-cli -h 127.0.0.1 -p 6379
查看Redis服务进程
ps -ef |grep redis
关闭Redis服务
shutdown
exit
Redis基础
基本知识
数据库
Redis有16个数据库,默认使用第0个
使用select num切换数据库
select 3
清空当前数据库的内容
flushdb
清空所有数据库内容
flushall
查看所有所有的key
keys *
配置信息
获取配置信息
CONFIG GET CONFIG_SETTING_NAME
获取所有配置信息
CONFIG GET *
编辑配置信息
CONFIG SET CONFIG_SETTING_NAME NEW_CONFIG_VALUE
#实例
CONFIG SET loglevel "notice"
INCLUDES
这里包括一个或多个其他配置文件。这很有用,如果你有一个标准的模板,去所有的Redis服务器,但也需要自定义一些服务器设置。包括文件可以包括其他文件,所以明智地使用这个。注意选项“include”不会被命令“CONFIG REWRITE”重写。
include /path/to/local.conf
include /path/to/other.conf
NWTWORK
bing 0.0.0.0
对所有人开放,可以指定单个或者多个ip
protected-mode yes
开启受保护模式
- 关闭protected-mode模式,此时外部网络可以直接访问
- 开启protected-mode保护模式,需配置bind ip或者设置访问密码
port 6379
默认端口号
GENERAL
daemonize yes
以守护进程的方式运行,默认是 no 我们需要自己设置为yes
pidfile /www/server/redis/redis.pid
如果是守护进程方式运行,我们需要指定一个pid文件
loglevel notice
设置日志文件的级别
- debug(大量信息,对开发/测试有用)
- verbose(很多很少有用的信息,但不像调试级别那样混乱)
- notice(有点冗长,可能是在生产中需要的内容)
- warning(只记录非常重要/关键的消息)
logfile "/www/server/redis/redis.log"
日志的文件位置
databases
默认的数据库数量 16 个
always-show-logo yes
是否显示log 默认为开启
SNAOSHOTTING
持久化数据 因为 Redis是内存数据库 如果断电等因素 会失去数据 所以我们需要在一定时间里 持久化数据
在 900s 内 有 1个key进行了操作 那么将会持久化一下
save 900 1
在 300s 内 有 10个key进行了操作 那么将会持久化一下
save 300 10
在 60s 内 有 1w个key进行了操作 那么将会持久化一下
save 60 10000
stop-writes-on-bgsave-error yes
持久化 出错了是否继续工作 默认继续
rdbcompression yes
是否压缩 rdb文件,默认压缩 压缩会消耗cpu资源
rdbchecksum yes
保存 rdb 文件时进行错误的校验
rdb
文件保存的目录
SECURITY
requirepass xxxxx
设置redis 登录密码
CLIENTS
maxclients 10000
默认有 1w 个用户可以同时连接redis 服务器
maxmemory <bytes>
redis 配置最大的内存容量
maxmemory-policy noeviction
内存到达上限的处理策略 6种
**1、volatile-lru:**只对设置了过期时间的key进行LRU(默认值)
2、allkeys-lru : 删除lru算法的key
**3、volatile-random:**随机删除即将过期key
**4、allkeys-random:**随机删除
5、volatile-ttl : 删除即将过期的
6、noeviction : 永不过期,返回错误
APPEND ONLY MODE
appendonly no
默认不开启 默认使用rdb持久化方式
- appendfsync always:每修改一个key都会执行 sync,消耗性能
- appendfsync everysec:每一秒执行一次 sync,可能会丢失这1s的数据
- appendfsync no:不执行 sync,这个时候操作系统会自己同步数据速度是最快的
appendfilename "appendonly.aof"
持久化文件的名字
参数说明
序号 | 配置项 | 说明 |
---|---|---|
1 | daemonize no | Redis 默认不是以守护进程的方式运行,可以通过该配置项修改,使用 yes 启用守护进程(Windows 不支持守护线程的配置为 no ) |
2 | pidfile /var/run/redis.pid | 当 Redis 以守护进程方式运行时,Redis 默认会把 pid 写入 /var/run/redis.pid 文件,可以通过 pidfile 指定 |
3 | port 6379 | 指定 Redis 监听端口,默认端口为 6379,作者在自己的一篇博文中解释了为什么选用 6379 作为默认端口,因为 6379 在手机按键上 MERZ 对应的号码,而 MERZ 取自意大利歌女 Alessia Merz 的名字 |
4 | bind 127.0.0.1 | 绑定的主机地址 |
5 | timeout 300 | 当客户端闲置多长秒后关闭连接,如果指定为 0 ,表示关闭该功能 |
6 | loglevel notice | 指定日志记录级别,Redis 总共支持四个级别:debug、verbose、notice、warning,默认为 notice |
7 | logfile stdout | 日志记录方式,默认为标准输出,如果配置 Redis 为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给 /dev/null |
8 | databases 16 | 设置数据库的数量,默认数据库为0,可以使用SELECT 命令在连接上指定数据库id |
9 | save <seconds> <changes> Redis 默认配置文件中提供了三个条件:save 900 1save 300 10save 60 10000分别表示 900 秒(15 分钟)内有 1 个更改,300 秒(5 分钟)内有 10 个更改以及 60 秒内有 10000 个更改。 | 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合 |
10 | rdbcompression yes | 指定存储至本地数据库时是否压缩数据,默认为 yes,Redis 采用 LZF 压缩,如果为了节省 CPU 时间,可以关闭该选项,但会导致数据库文件变的巨大 |
11 | dbfilename dump.rdb | 指定本地数据库文件名,默认值为 dump.rdb |
12 | dir ./ | 指定本地数据库存放目录 |
13 | slaveof <masterip> <masterport> | 设置当本机为 slave 服务时,设置 master 服务的 IP 地址及端口,在 Redis 启动时,它会自动从 master 进行数据同步 |
14 | masterauth <master-password> | 当 master 服务设置了密码保护时,slav 服务连接 master 的密码 |
15 | requirepass foobared | 设置 Redis 连接密码,如果配置了连接密码,客户端在连接 Redis 时需要通过 AUTH (password) 命令提供密码,默认关闭 |
16 | maxclients 128 | 设置同一时间最大客户端连接数,默认无限制,Redis 可以同时打开的客户端连接数为 Redis 进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis 会关闭新的连接并向客户端返回 max number of clients reached 错误信息 |
17 | maxmemory <bytes> | 指定 Redis 最大内存限制,Redis 在启动时会把数据加载到内存中,达到最大内存后,Redis 会先尝试清除已到期或即将到期的 Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis 新的 vm 机制,会把 Key 存放内存,Value 会存放在 swap 区 |
18 | appendonly no | 指定是否在每次更新操作后进行日志记录,Redis 在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis 本身同步数据文件是按上面 save 条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为 no |
19 | appendfilename appendonly.aof | 指定更新日志文件名,默认为 appendonly.aof |
20 | appendfsync everysec | 指定更新日志条件,共有 3 个可选值:no:表示等操作系统进行数据缓存同步到磁盘(快)always:表示每次更新操作后手动调用 fsync() 将数据写到磁盘(慢,安全)everysec:表示每秒同步一次(折中,默认值) |
21 | vm-enabled no | 指定是否启用虚拟内存机制,默认值为 no,简单的介绍一下,VM 机制将数据分页存放,由 Redis 将访问量较少的页即冷数据 swap 到磁盘上,访问多的页面由磁盘自动换出到内存中(在后面的文章我会仔细分析 Redis 的 VM 机制) |
22 | vm-swap-file /tmp/redis.swap | 虚拟内存文件路径,默认值为 /tmp/redis.swap,不可多个 Redis 实例共享 |
23 | vm-max-memory 0 | 将所有大于 vm-max-memory 的数据存入虚拟内存,无论 vm-max-memory 设置多小,所有索引数据都是内存存储的(Redis 的索引数据 就是 keys),也就是说,当 vm-max-memory 设置为 0 的时候,其实是所有 value 都存在于磁盘。默认值为 0 |
24 | vm-page-size 32 | Redis swap 文件分成了很多的 page,一个对象可以保存在多个 page 上面,但一个 page 上不能被多个对象共享,vm-page-size 是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page 大小最好设置为 32 或者 64bytes;如果存储很大大对象,则可以使用更大的 page,如果不确定,就使用默认值 |
25 | vm-pages 134217728 | 设置 swap 文件中的 page 数量,由于页表(一种表示页面空闲或使用的 bitmap)是在放在内存中的,,在磁盘上每 8 个 pages 将消耗 1byte 的内存。 |
26 | vm-max-threads 4 | 设置访问swap文件的线程数,最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为4 |
27 | glueoutputbuf yes | 设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启 |
28 | hash-max-zipmap-entries 64 hash-max-zipmap-value 512 | 指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法 |
29 | activerehashing yes | 指定是否激活重置哈希,默认为开启(后面在介绍 Redis 的哈希算法时具体介绍) |
30 | include /path/to/local.conf | 指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件 |
官方文档
Redis是一个开源(BSD许可)的内存数据结构存储,用作数据库、缓存和消息中间件MQ。Redis提供诸如字符串、哈希、列表、集合、有序集合(sort sets)、位图(bitmaps)、超日志(hyperloglogs)、地理空间(geospatial)索引半径查询等数据结构。Redis具有内置的复制(replication)、Lua脚本(Lua scripting)、LRU驱动事件(LRU eviction)、事务( transactions)和不同级别的磁盘持久性( persistence),并通过Redis哨兵(Sentinel)和自动分区(Cluster)提供高可用性
Redis数据类型
SDS
Redis需要的不仅仅是一个字符串字面值,而是一个可以被修改的字符串值
Redis没有直接使用C语言中的字符串,而是自己构建了一种名为简单动态字符串(Simple dynamic string,SDS)的抽象类型,并将SDS作为默认的Redis的默认字段
定义
struct sdshdr {
//记录buf数组已经使用字节的数量
//等于SDS保存字符串的长度
int len;
//记录buf数组未使用的字节数量
int free;
//字节数组,用于保存字符串
char buf[];
}
- free属性值为0,表示SDS没有分配任何未使用空间
- len属性的值为5,表示这个SDS保存了一个五字节长的字符串
- buf属性是一个char类型的数组,保存着前五个字节
C字符串和SDS的区别
常数负责读获取字符串长度
因为C字符串不记录自身的长度信息,所以为了获取一个C字符串的长度,程序必须遍历整个C字符串,整个操作的复杂度为O(N),而SDS不同于C字符串,保存着自身的长度,所以Redis将获取字符串长度的复杂度从O(N)降到了O(1)
杜绝缓存区溢出
字符串拼接函数
char *stract(char *dest, char *src);
因为C字符串不记录自身的长度,所以stract假定用户在执行函数时,已经为dest分配了足够多的内存,可以容纳src中的所有内容,而一旦这个假定不成立时就会产生缓存区溢出
假设存在字符串如下
程序员由于粗心在拼接字符串前未给s1分配足够的空间,而执行stract(S1, “Cluster”);
相比于C字符串,SDS的空间分配策略完全杜绝了放生缓冲区溢出的可能性:当SDSAPI需要对SDS进行修改的时候,API会先检查SDS的空间是否满足修改所需的要求,如果不满足,API会自定将SDS的空间扩展至执行修改所需要的大小。然后在执行修改,所以SDS既不需要手动修改SDS的空间大小,也不会出现缓冲区溢出的问题。
减少修改字符串时带来的内存重分配的次数
因为C字符串笔记录自身的长度,所以对于一个包含N个字符串的C字符串来说,这个C字符串底层实现总是一个N + 1个字符长的数组(额外一个字符空间保存空字符)。因为C字符串的长度和底层数组的长度之间存在这种关联性,所以每次增长或者缩短一个字符串都对应一次内存重分配操作。
为了避免C字符串的这种缺陷,SDS通过未使用的空间解除了字符串长度和底层数组长度之间的关联:在SDS中,buf数组的长度不一定就是字符数量+1,数组里面可以包含未使用的字节,而这些字节的数量就有SDS的free属性记录
通过未使用的空间,SDS实现了空间预分配和惰性空间释放两种优化策略
空间预分配
- 对于空间小于1M来说,分配空间为原有总长度+同样长度+1byte(B,字节).
- 对于大于1M来说,分配空间为原有总长度+1MB+1byte
通过空间预分配策略,SDS将连续增长N次的字符串所需要的内存重分配次数从必定N次降低为最多N次
惰性空间释放
对于需要缩短字符串的情景,即需要释放空间,SDS将需要移除的字符串移除,但是多余出来的的空间不释放,而是保留下来,记录在free里,这样扩展就有多余空间来进行,当然有真正释放空间的方法.
二进制安全
C字符串中的字符必须符合某种编码(ASCII),并且除了字符串末尾外,字符串里面不能包含空字符串,否则最先被程序读入的空字符串将被误认为字符串结尾,因此不能保存二进制文件,通多SDS可以避免这样的问题。
兼容部分C字符串函数
由于SDS一样遵循了C字符串以空字符结尾的惯例:这些API总会将SDS保存的数据的末尾设置为空字符,这是为了让那些保存文本数据的SDS可以重用一部分<String.h>库定义的函数
总结
C字符串和SDS的区别
C字符串 | SDS |
---|---|
获取字符串长度复杂度O(N) | 获取字符串长度复杂度O(1) |
API不安全,可能会造成缓冲区溢出 | API安全,不会造成缓冲区溢出 |
修改字符串长度N必然执行N次 | 修改字符串长度N最多执行N次 |
只能保存文本数据 | 可以保存文本或二进制数据 |
可以使用所有<string.h>库中的函数 | 可以使用一部分<string.h>库中的函数 |
链表
列表键的底层实现之一
定义
typedef struct listNode {
//前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
//节点的值
void *value;
}
typedef struct list {
//表头节点
listNode *head;
//表尾节点
listNode *tail;
//链表所包含的节点数量
unsigned long len;
//节点值赋值函数
void *(*dup)(void *ptr);
//节点释放函数
void *(*free)(void *ptr);
//节点值对比函数
void *(*match)(void *ptr, void *key);
}
特性
- 双端:链表具有
前置节点
和后置节点
的引用,获取这两个节点时间复杂度都为O(1)。 - 无环:
表头节点的 prev 指针
和表尾节点的 next 指针
都指向 NULL
,对链表的访问都是以 NULL 结束。 - 带表头指针和表尾指针
- 带链表长度计数器:通过
len 属性 获取链表长度
的时间复杂度为 O(1)
。 - 多态(保存各种不同类型的值):链表节点使用 void* 指针来保存节点值,可以保存
各种不同类型的值
。
字典
Redis的数据库底层就是使用字典作为底层实现的
字典,又被称为符号表、关联数组或映射,是一种用于保存键值对的抽象数据结构。字典中的每个键都是独一无二的,程序可以在字典中根据键查找与之关联的值,或者通过键来更新值,又或者根据键来删除整个键值对
实现
//哈希表节点
typedef struct dictEntry{
//键
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
}v;
//指向下一个哈希表节点,形成链表
struct dictEntry *next;
}dictEntry;
//哈希表
typedef struct dictht{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于 size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
}dictht;
//字典
typedef struct dict{
//类型特定函数
dictType *type;
//私有数据
void *privdata;
//哈希表
dictht ht[2];
//rehash索引
//当rehash不在进行时,为-1;
int rehashidx;
}dict;
type属性是一个执行dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同类型的特定函数,
privadata属性保存了需要传给那些特定类型特定函数的可选参数
哈希算法
1、使用字典设置的哈希函数,计算键 key 的哈希值
hash = dict->type->hashFunction(key);
2、使用哈希表的sizemask属性和第一步得到的哈希值,计算索引值
index = hash & dict->ht[x].sizemask;
根据不同的状态,h[x]可以是0或者1
Redis使用的是MurmurHash2算法来计算键的
以上是关于Redis学习缓存持久化哨兵模式的主要内容,如果未能解决你的问题,请参考以下文章
Linux分布式缓存系统——Redis持久化+Sentinel哨兵模式+Redis集群