非关系型数据库
基本特点
- 存储非结构化的数据,比如文本、图片、音频、视频
- 表于表之间没有关联,可扩展性强
- 保证数据的最终一致性,遵循base理论。Basiceally Avalibale(基本可用);Soft-state(软状态);Eventually Consistent(最终一致性)
- 支持海量的存储和高并发的读写
- 支持分布式,能够对数据进行分片存储,扩缩容简单
常见的非关系型数据库有下面几种类型
- KV存储:Redis和Memcached
- 文档存储:MongoDB
- 列存储:HBase
- 图存储:Neo4j
- 对象存储
- XML存储
Redis基本特性
- 速度快(存放在内存中)
- 内存的速度更快,10W QPS
- 减少计算的时间,减轻数据库压力
- 支持多种数据类型
- 支持多种编程语言
- 持久化、内存淘汰
- 功能丰富:事务、发布订阅、pipeline、lua
- 集群、分布式
默认16个db。可以在配置文件redis.conf中修改。
默认使用第一个db0,在集群里面只能使用第一个db。
database 16
select index:选择db
flushdb清空当前db缓存
flushAll 清空所有的缓存
数据类型
redis存储我们叫做key-value存储,或者叫做字典结构。key的最大长度限制是512M,值的限制不同,有的是用长度限制的,有的是用个数限制的。
-
String的三种编码
1. int,存储8个字节的长整型(long,2^63-1) 2. embstr,存储小于44个字节的字符串(SDS)。embstr是只读的,只要 3. raw,存储大于44个字节的字符串(SDS) set lichong 123 (增、改) get lichong(查) keys* 获取所有的key dbsize 获取字符串长度 exists lichong 判断字符串是否存在 del lichong huping (删) rename lichong lixiaochong 修改字符串key值 type lichong 查看字符串类型
数据模型
可以用来存int、float、string。
Redis是KV的数据库,最外层是通过hashtable实现的,每个键值都对应一个dicEntry,通过指针指向key的存储结构和value的存储结构,而且next存储了指向下一个键值对的指针。
Redis自己实现了一个字符串类型,叫做SDS(Simple Dynamic String)简单动态字符串。本质上其实还是字符数组。SDS有多种结构:sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64用于存储不同长度的字符串,分别代表25,28,216,232。
为什么要用SDS实现字符串?
1.使用字符数组必须先给目标变量分配足够的空间,否则可能会溢出。
2.如果要获取字符长度,必须便利字符数组,时间复杂度是O(n).
3.C字符串长度的变更会对字符数组做内存重分配。
4.通过从字符串开始到结尾碰到的第一个‘\0’来标记字符串的结束,因此不能保存图片、音频、视频、压缩文件等二进制保存的内容,二进制不安全。
应用场景:
- 缓存热点数据。
- 分布式数据共享,可以在多个应用之间共享数据,如:分布式Session,分布式锁。
- 全局ID,INT类型,INCRBY,利用原子性(incrby userid 1000)分库分表的场景,一次性拿一段。
- INT类型,INCR方法,如:文章的阅读量,微博点赞数,允许一定的延迟,先写入Redis在定时同步到数据库
- 限流。以访问者的IP和其他信息作为key,访问一次增加一次计数,超过次数则返回false。
String类型的setNX方法,只有不存在时才能添加成功,返回true。
-
hash的两种存储方式
Hash的特点: 节省内存空间 减少key冲突 取值减少性能消耗
1. ziplist存储 2. hashtable存储 最大存储 2^31-1(40亿左右) 命令:hset key field [key field ...] hmset key field [key field ...] hget key value hmget key value [key field ...] hkeys key hvals key hgetall key hdel key field [field] hlen key
-
list列表(有序)
lpush key element [element ...] 从左边添加元素 rpush key element [element ...] 从右边添加元素 lpop key 从左边弹出一个元素 rpop key 从右边弹出一个元素 lindex key index 取出第一个元素 lrange key start stop 从几个元素 blpop key timeout 移出并获取列表的第一个元素,如果没有元素会阻塞列表知道等待超时或发现可弹出元素为止。 brpop key timeout 同上移出列表最后一个元素 队列:rpush blpop 先进先出,左头右尾,右边进入队列,左边出队列 栈:先进后出:rpush brpop
-
set集合(无序)
使用场景:点赞、签到、打卡、商品标签、 可以对两个集合取差集、交集、并集(商品筛选) sadd key member [member ...] 添加元素 smembers key 获取所有元素 scard key 获取元素个数 srandmember key 随机获取一个元素 spop key 随机弹出一个元素 srem key meber [member ...]移除一个或多个元素 sismember key member查看元素是否存在 sdiff set1 set2 获取差集 sinter set1 set2 获取交集 sunion set1 set2 获取并集
应用场景:
- 抽奖:随机获取元素:spop myset
- 点赞、签到、打卡。一条微博的ID是t1001,用户id是u3001,用like:t1001来维护t1001这条微博的所有点赞用户。点赞了这条微博:sadd like :t1001 u3001。取消点赞:srem like :t1001 u3001。是否点赞:sismeber like:t1001 u3001。点赞的所有用户:smebers like:t1001。点赞数:scard like:t1001
- 商品标签。用tags:i5001来维护商品所有的标签。sadd tags:i5001画面清晰细腻
- 商品筛选,交集、并集、差集
- 用户关注、推荐模型
-
zset有序集合
使用场景:排行榜 zadd key score member [score member]添加元素 zrange key start stop 获取全部元素 zrevrange key start stop 倒序获取全部元素 zrangebyscore key start stop 根据分值区间获取元素 zrem key member [member ...]移除元素 zcard key 统计元素个数 zincrby key increment member 分值递增 zcount key start stop 根据分值统计个数 zrank key member 获取元素rank zscore key member 获取元素值
应用场景:
-
排行榜
例如百度热榜、微博热榜。
id为6001的新闻点击数加1:zincrby hotNews:20251111 1 n6001
-
-
geospatial
-
hyperloglogs
-
streams
数据结构 | 是否允许重复元素 | 是否有序 | 有序实现方式 |
---|---|---|---|
列表list | 是 | 是 | 索引下标 |
集合set | 否 | 否 | 无 |
有序集合zset | 否 | 是 | 分值score |
发布订阅
订阅频道:可以一次订阅多个
subscribe channel-1 channel-2 channel-3
向指定频道发布消息
publish channel-1 2673
取消订阅:
unsubscribe channel-1
按规则订阅频道
#支持?和*占位符。?代表一个字符,*代表0个或者多个字符。
psubscribe *sport
一般来说,考虑到性能和持久化的因素,不建议使用Redis的发布订阅功能来实现MQ。
事务
事务命令
multi 开启事务
exec 执行事务
discard 取消事务
watch 监视key,如果被监视的key在exec之前被修改,事务会取消。
#事务执行遇到的问题分为两种,一种是在执行exec之前发生错误,一种是在执行exec之后发生错误。
#1.在执行exec之前发生错误,入队的命令存在语法错误,包括参数数量,参数名等。事务会被拒绝执行。
#如下:
multi
set qingshang 2673
set huihui yes
hset bobo 666
exec
#2.在执行exec之后发生错误。比如对String使用了Hash的命令,参数个数正确,但数据类型错误,这是一种运行时错误。
multi
set k1 1
hset k1 a b
exec
#set k1 1的命令是成功的,也就是在中发生了运行时异常的情况下,只有错误的命令没有被执行,但是其他命令没有受到影响。这就导致了这种事务机制不能实现原子性,保证数据的一致。
#于redis事务有可能导致事务不会回滚,所以用lua脚本执行。
Lua脚本
由于redis事务有可能导致事务不会回滚,所以用lua脚本执行。
1. 批量执行命令,减少网络开销
2. Redis会将整个脚本作为一个整体执行,不会被其他请求打断,保持原子性
3. 对于复杂的组合命令,可以放在文件中,可以实现命令复用。
redis.call(command,key[param1,param2...])
* command是命令,包括set,get,del等
* key是被操作的键
* param1,param2...代表给key的参数
#例如:Lua脚本内容
redis.call('set','qingshan','lua666')
return redis.call('get','qingshan')
#调用脚本文件
redis-cli --eval gupao.lua 0
#IP限流,对某个IP频率进行限制,6秒钟访问10次
--ip_limit.lua
local num=redis.call('incr',KEYS[1])
if tonumber(num)==1 then
redis.call('expire',KEYS[1],ARGV[1])
return 1
elseif tonumber(num)>tonumber(ARGV[2]) then
return 0
else
return 1
end
#redisicli --eval ip_limit.lua app:ip:limit:192.168.111 , 6 10
#app:ip:limit:192.168.111是key值,后面是参数值,中间要加上一个空格和一个逗号,再加上一个空格。即
redis-cli --eval[lua脚本] [key]空格,空格[args...]
#多个参数之间用空格分隔
#缓存Lua脚本
#通过两个命令,首先是在服务端缓存lua脚本生成一个摘要码,用script load命令
script load "return 'hello world'"
#通过摘要码执行缓存的脚本
evalsha "47087a599ac" 0
#脚本超时
#Redis的指令执行本身是单线程的,这个线程还要执行客户端的Lua脚本,如果Lua脚本执行超时或者陷入了死循环,它会导致其他的命令都进入等待状态。
#脚本执行有一个超时时间,默认为5秒
lua-time-limit 5000
#如果遇到一些特殊的需求,可以用Lua来实现,但是要注意那些耗时的操作。
Redis为什么这么快
Redis的QPS 10万还是比较准确的,在高性能服务器上还能更强
纯内存KV
时间复杂度O(1)
请求单线程
好处:
- 没有创建线程、消费线程带来的消耗。
- 避免了上下文切换导致的CPU消耗
- 避免了线程之间带来的竞争问题,例如加锁释放锁死锁等等
同步非阻塞I/O–多路复用(多个tcp连接一个或多个线程)
虚拟存储器(虚拟内存Virtual Memory)
计算机里面的内存我们叫做主存,硬盘叫做辅存。
主存可以看作一个很长的数组,一个字节一个单元,每个字节有一个唯一的地址,这个地址叫做物理地址。
由于直接操作主存有各种弊端,所有在cpu和主存之间增加了一个中间层,即内存管理单元。
每一个进程开始创建的时候,都会分配一段虚拟地址,然后通过虚拟地址和物理地址的映射来获取真实数据,这样进程就不会直接接触到物理地址。在32位的系统上,虚拟地址空间大小是232=4G。在64位系统上,最大虚拟地址不是264=1024*1024(TB),一般Linux用低48位来表示虚拟地址空间,也就是2^48=256TB。
虚拟内存的作用:
- 通过把同一块物理内存映射到不同的虚拟地址空间实现内存共享
- 对物理内存进行隔离,不同的进程操作互不影响
- 虚拟内存可以提供更大的地址空间,并且地址空间是连续的,使得程序编写、链接更加简单。
Linux的虚拟内存又进一步划分成了两块:用户空间和内核空间.
进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。进程在内核空间可以调用系统的一切资源,用户空间只能执行简单的运行,不能直接调用系统资源,必须通过系统接口,才能向内核发出指令。
I/O多路复用
I/O指的是网络I/O。
多路指的是多个TCP连接,复用指的是复用一个或多个线程。
所以,I/O多路复用的特点是通过一种机制让一个进程能同时等待多个文件描述符,而这些文件描述符其中的任意一个进入读就绪状态,select()函数就可以返回。多路复用需要操作系统的支持,Redis的多路复用,提供了select,epoll,evport,kqueue几种选择,在编译的时候来选择一种。
- evport是Solaris系统内核提供支持的
- epoll是LUNUX系统内核提供支持的
- kquue是Mac系统提供支持的
- select是POSIX提供的,一般的操作系统都有支撑(保底方案)
作为一个KV系统,Redis服务肯定不是无限制地使用内存,应该设置一个上限,第二个,数据应该有过期属性,这样就能清除不再使用的key。
过期策略
-
立即过期(主动淘汰)
每个设置过期时间的的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好,但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
-
惰性过期(被动淘汰)
只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好,极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
-
定期过期
每隔一段时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。
总结:Redis中同时使用了惰性过期和定时过期两种过期策略,并不是实时地清除过期的key。
如果所有的key都没有设置过期属性,Redis内存满了怎么办?
淘汰策略
Redis的内存淘汰策略,是指当内存使用达到最大内存极限时,需要使用淘汰算法来决定清理掉哪些数据,以保证新数据的存入。
最大内存设置
redis.conf参数配置:#maxmemory
如果不设置maxmemory或者设置为0,32位系统最多使用3GB内存,64位系统不限制内存。
动态修改:config set maxmemory 2GB
到达最大内存以后怎么办?
淘汰策略
LRU,Least Recently Used:最近最少使用
LFU,Least Frequently Used:最不经常使用,按照使用频率删除
random 随机删除
#动态修改淘汰策略
config set maxmemory-policy volatile-lru
建议使用volatile-lru,在保证正常服务的情况下,优先删除最近最少使用的key。
LRU淘汰原理
LRU是一个很常见的算法,比如InnoDB的Buffer Pool也用到了LRU。
传统的LRU:通过链表+HashnMap实现,设置链表长度,如果新增或者被访问,就移动到头节点。超过链表长度,末尾的节点被删除。
如果基于传统的LRU算法实现Redis,需要额外的数据结构存储,消耗内存。
Redis LRU对传统的LRU算法进行了改良,通过随机采样来调整算法的精度。
如果淘汰策略时LRU,则根据配置的采样值maxmemory_samples(默认值是5个),随机从数据库中选择m个key,淘汰其中热度最低的key对应的缓存数据。所以采样参数m配置的数值越大,就越能精确的查找到带淘汰的缓存数据,但是也消耗更多的CPU计算,执行效率低。
如何找出热度最低的数据?
Redis中所有对象结构都有一个lru字段,且使用了unsigned的低24位,这个字段用来记录对象的热度。对象被创建时会记录lru值,在被访问的时候也会更新lru的值。但并不是获取系统当前的时间戳,而是设置位全局变量server.lruclock的值。
Redis中有个定时处理的函数serverCron,默认每100毫秒调用函数updateCachedTime更新一次全局变量的server.lruclock的值,他记录的是当前unix时间戳。(这样做不用每次调用系统函数time,可以提高执行效率)
评估指定对象的lru热度,方法就是对象的lru值和全局的server.lruclock的差值越大,该对象热度越低。
LFU原理
当这24bits用作LFU时,其被分为两部分:
高16位用来记录访问时间(单位为分钟,ldt,last decrement time)
低8位用来记录访问频率,简称counter。counter使用基于概率的对数计数器实现的,8位可以表示百万次的访问频率。对象被读写的时候,lfu的值会被更新。
增长的速率有一个参数决定,lfu-log-factor越大,counter增长的越慢。
redis.conf配置文件:#lfu-log-factor 10
如果一段时间热点高,就一直保持这个热度,肯定也是不行的,体现不了整体频率。所以,没有被访问的时候,计数器还要递减。
减少的值由衰减因子lfu-decay-time(分钟)来控制,如果值是1的话,N分钟没有访问,计数器就要减少N。lfu-decay-time越大,衰减越慢。
redis.conf配置文件:#lfu-decay-time 1
持久化机制
RDB
Redis DataBase 记录快照
默认的持久化方案。但满足一定条件的时候,会把当前内存中的数据写入磁盘,生成一个快照文件dump.rdb。Redis重启会通过加载dump.rdb文件恢复数据。
触发机制:
-
自动触发
-
配置规则触发
redis.conf,SNAPSHOTTING,其中定义了触发把数据保存到磁盘的出发频率。
save 900 1 #900秒内至少有一个key被修改(包括添加) save 300 10 #300秒内至少有10个key被修改 save 60 10000 #60秒内至少有10000个key被修改 #上面的配置是不冲突的,只要满足任意一个都会触发。 #用lastsave命令可以查看最近一次成功生成快照的时间。 #rdb文件位置和目录(默认在安装根目录下): #文件路径 dir ./ #文件名称 dbfilename dump.rdb #是否以LZF压缩rdb文件 rdbcompression yes #开启数据校验 rdbchecksum yes
-
shutdown触发,保证服务正常关闭
-
flushall,rdb文件是空的,没什么意义
-
-
手动触发
如果我们需要重启服务或者迁移数据,这个时候就需要手动触发RDB快照保存。Redis提供了两条命令:
-
save
save在生成快照的时候会阻塞当前redis服务器,Redis不能处理其他命令,如果内存中数据比较多,会造成Redis长时间的阻塞。生产环节不建议使用这个命令。
-
bgsave
执行bgsave时,Redis会在后台异步进行快照操作,快照时还可以响应客户端请求。
-
优势:紧凑,适合备份和灾难恢复;生成文件过程不影响主进程;大数据集恢复速度较快
不足:不能实时持久化,可能丢失数据
AOF
Append Only File 记录日志
两个都开启,执行第二种方式
AOF采用日志的形式来记录每个写操作,并追加到文件中。开启后,执行更改Redis数据的命令时,就会把命令写入到AOF文件中。
Redis重启时会根据日志文件的内容把写指令从前到后执行一次以完成数据的恢复工作。
#配置文件redis.conf
#开关
appendonly no
#文件名
appendfilename "appendonly.aof"
由于操作系统的缓存机制,AOF数据并没有正真地写入硬盘,而是进入了系统的硬盘缓存。
什么时候把缓冲区的内容写到AOF文件?
参数 | 说明 |
---|---|
appendfsync everysec | AOF持久化策略(硬盘缓存到磁盘),默认everysec |
1.no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快,但是不太安全 2.always表示每次写入都执行fsync,以保证数据同步到磁盘,效率很低 3.everysec表示每秒执行一次fsync,可能会导致丢失这1s的数据。通常选择everysec,兼顾安全性和效率。 |
文件越来越大怎么办?
如果计数器增加100万次,100万个命令都记录进去了,但是结果只有1个。
为了解决这个问题,Redis新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。
可以使用bgrewriteaof命令来重写。
AOF文件重写并不是对源文件进行重新整理,而是直接读取服务器现有的键值对,然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的AOF文件。
#重写触发机制
auto-aof-rewrite-percentage 100
#默认值100。aof自动重写配置,当目前aof文件大小超过上一次重写的aof文件大小的百分之多少进行重写,即aof文件增长到一定大小的时候,redis能够调用bgrewriteaof对日志文件进行重写。
auto-aof-rewrite-min-size 64mb
#默认64M。设置允许重写的最小aof文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写。
重写过程中,AOF文件被更改了怎么办?
当子进程在执行AOF重写时,主进程需要执行以下三个工作:
- 处理命令请求
- 将写命令追加到现有的AOF文件中
- 将写命令追加到AOF重写缓存中
优点:持久化方法提供了多种的同步频率,即使使用默认的同步频率每秒同步一次,Redis最多也就丢失1秒的数据。
缺点:对于具有相同数据的Redis,AOF文件通常会比RDB文件体积更大。虽然AOF提供了多种同步的频率,默认情况下,每秒同步一次的频率也具有较高的性能。在高并发的情况下,RDB比AOF具有更好的性能保证。
Redis分布式集群
Redis主从复制
例如:一主多从,186是主节点
#第一种方式在redis.conf配置文件增加一行
replicaof 192.168.44.186 6379
#第二种方式 启动服务时通过参数直接指定master节点
./redis-server --slaveof 192.16844.186 6379
#一个正在运行中的节点,可以变成其他节点的从节点,直接执行命令
slaveof 192.168.44.186 6379
#查看集群状态
info replication
#从节点是只读的,不能执行写操作,执行写命令会报错:
(error) READONLY You cant write aginst a read only replica.
#把配置文件里面的replica of去掉重启,或者直接断开复制:从节点就会变成主节点
slaveof no one
原理:Redis的主从复制分为两类
- 全量复制,就是一个节点第一次连接到master节点,需要全部的数据。
- 全量复制,之前已经连接到master节点,但是中间网络断开,或者slave节点宕机了,缺了一部分的数据。
1.连接阶段
- slave节点启动时(或者执行slaveof命令时),会在本地保存master节点的信息,包括master node的host和ip
- slave节点内部有个定时任务replicationCron,每隔1秒钟检查是否有新的master node要连接和复制
如果发现由master节点,就跟master节点建立连接。如果连接成功,从节点就为连接一个专门处理复制工作的文件事件处理器负责后续的复制工作。为了让主节点感知到slave节点的存活,slave节点定时会给主节点发送ping请求。
2.数据同步阶段
如果时新加入的slave节点,那就需要全量复制。master通过bgsave命令在本地生成一份RDB快照,将RDB快照文件发给slave节点。
如果slave本来有数据,首先需要清除自己的旧数据,然后用RDB文件加载数据。
master节点生成RDB期间,接收到的写命令会缓存到内存中,在slave节点保存了RDB之后,再将新的写命令赋值给slave节点。
第一次全量同步完了,主从已经保持一致了,后面就是持续把接收到的命令发送给slave节点。
3.命令传播阶段
master节点持续把写命令异步复制给slave节点。
一般情况下我们不会用Redis做读写分离,因为Redis的吞吐量已经够高了,做集群分片之后并发的问题更少,所以不需要考虑主从延迟的问题。
如果slave节点有一段时间断开了与master节点的连接,可以通过master_repl_offset记录的偏移量知道上次复制到哪里。
info replication
6.0的新特性,主从复制的无盘复制(从2.8.18版本开始支持无盘复制)
为了降低主节点磁盘开销,Redis支持无盘复制,master生成的RDB文件不保存到磁盘而是直接通过网络发送给从节点。无盘复制适用于主节点磁盘性能较差但网络宽带较充裕的场景。
缺点:解决了数据备份和一部分性能的问题,但是没有解决高可用的问题。
Sentinel哨兵模式
原理
怎么实现高可用呢?第一个对于服务端来说,能够实现主从自动切换,第二个,对客户端来说,如果发生了主从切换,它需要获取最新的master节点。
哨兵思路就是通过运行监控服务器来保证服务的可用性。
从Redis2.8版本起,提供了一个稳定版本的Sentinel(哨兵),用来解决高可用的问题。
启动奇数个的Sentinel的服务
#通过sentinel的脚本启动
./redis-sentinel ../sentinel.conf
#用redis-server的脚本加sentinel参数启动
./redis-server ../sentinel.conf --sentinel
它本质上只是一个运行在特殊模式之下的Redis。Sentinel通过info命令得到被监听Redis机器的master,slave等信息。
为了保证监控服务器的可用性,我们会对Sentinel做集群的部署。Sentinel既监控所有的Redis服务,Sentinel之间也互相监控。
注意:Sentinel本身没有主从之分,地位是平等的,只有Redis服务节点有主从之分。
#因为Sentinel是一个特殊状态的Redis节点,他也有发布订阅的功能。
#哨兵上线时,给所有的Redis节点的名字为_sentinel_:hello的channel发送消息。
#每个哨兵都订阅了所有的Redis节点名字为_sentinel_:hello的channel,所以能互相感知对方的存在,而进行监控。
服务下线
Sentinel默认以每秒钟1次的频率向Redis服务节点发送PING命令。如果在指定时间内没有收到有效回复,Sentinel会将该服务器标记为下线(主观下线)。
#sentinel.conf
sentinel down-after-milliseconds <master-name><milliseconds>
#默认是30秒
#只有一个Sentinel发现master下线,并不代表master真的下线了,也有可能是自己的网络出问题了。所以,这个时候第一个发现master下线的Sentinel节点会继续访问其他的Sentinel节点,确认这个节点是否下线,如果多数Sentinel节点都任务master下线,master才真正确认被下线(客观下线)
#确认master下线后,需要重新选举master。
故障转移
Redis的选举和故障转移都是由Sentinel完成的。故障转移流程的第一步就是在Sentinel集群选择一个Leader,由Leader完成故障转移流程。Sentinel通过Raft算法,实现Sentinel选举。
Raft是一个共识算法。它的核心思想:先到先得,少数服从多数。