NoSQL介绍和redis介绍
not only SQL:非关系型数据库;
作用:应用于海量数据用户数据的前提下的数据处理问题。
特征:可扩容;可伸缩;大数据量下高性能;灵活的数据模型;高可用
redis:远程字典服务,键值对数据库
redis特征:
①数据之间没有必然的联系;
②单线程工作机制;
③高性能;
④多数据类型支持;
⑤持久化支持,数据灾难恢复。
Redis的基本数据类型
String类型
String对象底层是int、raw、embstr
1.如果一个String对象的内容可以转换为long类型,那么该字符串就会被转换为long类型,而且对象类型会用int类型表示
2.普通的字符串有两种,embstr和raw,在redis3.0之后
2.1.如果字符串对象的长度小于39个字节,会使用embstr,
2.2.否则是用raw对象
# set key value
# 设置key为name,value为hu的string类型
# get key
# get key
# strlen key
# key对应的字符的长度
# incr key
# key对应的value值加一
# incrby key increment
# key对应的value值加increment
# incrbyfloat key increment
# key对应的value值加increment(类型为float)
注:类型不能转换或者超范围,会报错,范围是2^63 - 1
hash类型
哈希对象的底层实现可以是ziplist或者hashtable
ziplist中的哈希对象是按照key1,value1,key2,value2这样的顺序来存储的,元素不多的时候,效率很高
hashtable是由dict这个结构来实现的
# hset key field value
# field是键,value是值
# hget key field
# 获取field对应的value值
# hgetall key
# 获取所有的键值对,奇数时field,偶数时value
# hdel key field1 [field2...]
# 删除指定field
# hmset key field1 value1 [field2 value2]
# 一次性设置多个键值对
# hmget key field1 [field2]
# 获取多个field对应的value值
# hlen key
# 获取hash的长度,一个键值对对应一个长度
# hexist key field
# 确认key是否有指定field的键值对存在
# hkeys key
# 获取所有的field
# hvals key
# 获取所有的value
# hincrby key field increment
# field对应的value值加increment
# hincrbyfloat key field increment
# field对应的value值加类型为flaot的increment
注:①hash中的value只能存储字符串;②每个hash类型最大存储2^31-1个键值对;③hgetall谨慎使用
list类型
list对象底层可以是 ziplist或者linkedlist
ziplist是一种压缩链表,节省空间,所存储的内容都是在连续的内存区域中的。
1.当list对象元素不大,每个元素也不大的时候,采用ziplist存储
当数据量过大时,ziplist不是那么好用,因为为了保证内存的连续性,此时的时间复杂度事O(N)
2.数据量过大是,使用linkedlist,是一个双向链表,插入方便
# lpush key value
# 从key的左边添加元素
# rpush key value
# 从key的右边添加元素
# lrange key start end
# 获取从start到end之间的元素
# lindex key index
# 获取指定下标下的值
# llen key
# 获取key的长度
# lpop key
# 从左边移除元素
# rpop key
# 从右边移除元素
# blpop key timeout
# 从左边在规定时间内部移除最左边元素
# brpop key timeout
# 从右边在规定时间内部移除最右端元素
# lrem key count value
# 移除值为value的元素count个
注:①数据时string类型的,总长度为2^32-1;②具有索引的概念;③获取全部数据操作将结束索引设置为-1;④可以分页
set类型
set对象底层可以是intset或者hashtable
intset是一个正数集合,里面存放的时某种同一类型的整数
支持一下三种长度的整数
define INTSET_ENC_INT16 (sizeof(int16_t))
define INTSET_ENC_INT32 (sizeof(int32_t))
define INTSET_ENC_INT64 (sizeof(int64_t))
intset是一个有序集合,查找元素的时间复杂度时O(logN)
但是插入不一定是O(logN),因为可能涉及到升级的操作,
当set中是int16_t的整数,插入int32_t的整数,为了维护set中数据类型的一致,
所有的数据都会转换为int32_t,这个时候时间复杂度就是O(N),
而且set不支持降级操作
# sadd key member1 [member2]
# 添加元素
# smembers key
# 获取所有的值
# srem key member1 [member2]
# 删除元素
# scard key
# 获取集合中的数据总数
# sismember key member
# 是否有指定数据存在
# srandmember key [count]
# 随机获取count个元素
# spop key
# 随机获取集合中某个元素并将其移除
# sinter key1 key2
# 获取两个集合的交集,存储在key1中
# sunion key1 key2
# 获取两个集合的并集,存储在key1中
# sdiff key1 key2
# 获取两个集合的差集,存储在key1中
# sinterstore destination key1 key2
# 获取两个集合的交集,存储在指定集合中
# sunionstore destination key1 key2
# 获取两个集合的并集,存储在指定集合中
# sdiffstore destination key1 key2
# 获取两个集合的差集,存储在指定集合中
# smove source destination member
# 将指定数据移动到指定集合
sorted_set
在set的基础上面做了排序,不同的是每个元素都会关联一个double类型的score;redis正是通过分数来为集合中的成员进行从小到大的排序。
有序集合可以是ziplist,也可以是skiplist和dict的结合
ziplist作为zset和作为hash是一样的,member和score顺序存放,
按照score从小到大顺序排列。
skiplist作为一种跳跃表,实现了zset的快速查找,大多数情况下他的速度和平衡树差不多
typedef struct zset {
// 字典
dict *dict;
// 跳跃表
zskiplist *zsl;
} zset;
// 使用两者相加的原因
// 1.单一的使用hashtable,可以快速地查找,添加,删除元素,但是没有办法保证有序性
// 2.单一的使用skiplist,查找太慢了,有序性可以保障
// 所以hashtable用于查找,skiplsit用于保证有序性
# zadd key score1 member1 [score2 member2]
# 添加数据
# zrange key start stop
# 获取数据
# zrevrange key start end
# 逆序获取数据
# zrem key member1 [member2]
# 移除元素
# zrangebuscore key min max [withscore] [limit]
# 获取区间score之间的数据
# zrangebyscore key max min [withxcore] [limit]
# 逆序获取
# zremrangebyrank key start end
# 删除索引区间内部的元素
# zremrangebyscore key start end
# 删除score区间内部的数据
# zcard key
# 查询全部数量
# zcount min max
# 查询指定范围的元素数量
# zinterstore destination numkeys key1 [key2]
# 交集
# zunionstore destination numkeys key1 [key2]
# 并集
# zrank key number
# 获取数据的索引
# zrevrank key number
# 获取数据的逆序索引
# zscore key member
# 获取member的score
# zincrby key increment member
# 对member的score操作
key的通用操作
# del key
# 删除key
# exists key
# key是否存在
# type key
# key的类型
# expire key seconds
# 设置key的有效时间(秒)
# pexpire key milliseconds
# 设置key的有效时间(毫秒)
# ttl key
# 查看key的有效时间(秒)
# pttl key
# 查看key的有效时间(毫秒)
# 有效时间结束为-2,若时永久数据为-1
# keys *
# 查看所有的key "*"匹配任意数量的任意字符;"?"匹配任意的一个字符;"[]"匹配一个指定的字符
# rename key newname
# 重命名
# renamenx key newname
# 如果存在则重命名
# sort list/set/sorted_set
# 排序,只能是list/set/sorted_set
# select index
# 切换数据库,默认为0,一共有16个(0-15)
# flushdb
# 清除当前数据库
# flushall
# 清除所有数据
Redis底层数据结构
持久化
为了数据的安全;持久化的方式:RDB、AOF
RDB
save指令,每次save都会保存一次数据,以二进制的数据报错
# save的相关配置
# save 60 1
# 当60s发生一次数据改变,就自动save
# dbfilename dump.rdb
# 设置本地数据库的文件名,默认为dump.rdb
# dir
# 设置存储.rdb文件的路径
# rdbcompression yes
# 是否采用压缩格式,是的话采用LZF压缩
# rdbchecksum yes
# 是否开启文件校验,在读和写的时候均会进行
save指令的工作原理
执行save指令的时候会阻塞当前服务器,直到当前RDB过程执行完毕为止,有可能会造成长时间的阻塞。
bgsave指令的工作原理
①发送指令,返回消息”bgsaving started”;
②调用fork函数生成子进程;
③子进程来创建rdb文件;
④返回信息给redis服务端。
RDB的优缺点
优点 | 缺点 |
---|---|
二进制存储,存储效率高 | 无法做到实时性持久化 |
比AOF的恢复速度快 | bgsave需要创建子进程 |
数据备份,灾难恢复快 | 多个版本文件格式未进行统一 |
AOF
AOF(append only file):以独立日志的方式记录每条写命令,重启时执行存储的写命令达到恢复数据的目的。
主要作用:保持数据持久化的实时性
AOF写数据的三种策略
①always:每次写操作都会同步到AOF文件中,误差小,性能低
②everysec:每秒同步一次,误差较小,性能较高
③no:由系统控制,整体过程不可控
# 开启功能
# appendonly yes/no
# 是否开启AOF持久化,默认不开启
# appendsync always/everysec/no
# AOF的写策略
AOF重写
当数据不断写入,AOF文件会变得越来越大,为了解决此问题,redis引入AOF重写机制来压缩文件体积,对于一个数据的操作执行最后的指令,保存最终结果。
AOF重写的作用
①降低磁盘占用量,提高磁盘的利用率;
②提高持久化率,降低持久化写时间,提高IO性能;
③提高用时重写规则
重写规则
①已经超时的数据不会重写;
②忽略无效的数据;
③同一数据多条指令合并成一条指令
重写方式
①手动重写
# bgrewtireof -> 调用子进程
②自动重写
# auto_aof_rewrite_min_size size
# 设置最小重写的指令数,超过就重写
# auto_aof_rewrite_percentage percentage
# 当前AOF的大小和上一次重写时AOF文件的大小
# aof_current_size
# 当前AOF文件的大小
# aof_base_size
# 基准文件的大小
# 触发条件
# aof_current_szie > auto_aof_rewrite_min_size
# (aof_current_size - aof_base_size) / aof_base_size >= auto_aof_rewrite_percentage
RDB和AOF的对比
持久化方式 | RDB | AOF |
---|---|---|
占用存储空间 | 小,二进制压缩文件 | 大,存储的是指令 |
存储速度 | 慢 | 快 |
恢复速度 | 快 | 慢 |
数据安全性 | 会丢失 | 依据写策略来决定 |
资源消耗 | 高 | 低 |
启动优先级 | 低 | 高 |
事务
基本操作
# multi
# 开启事务,设定事务的开启位置,从此处指令执行后,后续的所有指令都加入到队列中并且不执行
# exec
# 执行事务,设定事务的结束位置,同时执行事务,与multi成对出现,成对使用
# discard
# 终止事务,发生在multi和discard之间
注:
①若是事务中有语法错误,则事务中所有的指令都不执行;
②若是指令格式正确,但是不能执行,则能执行的命令执行,不能执行的命令不执行,而且已经执行的任务不能回滚。
删除策略
过期数据:具有时效性的数据在expire之后还未被删除的数据。
所有时效性的数据在redis中一哈希表的形式存储,key是数据的地址,value是key的过期时间。
数据的删除策略
①定时删除:设置定时器,当过期时间达到时,由定时器任务立即执行对key的删除
优点:节约内存;
缺点:CPU压力大
②惰性删除:过期数据等到下次访问的时候再删除;expireIfNeeded()函数
优点:CPU压力小
缺点:耗费内存
③定期删除:Redis启动服务器初始化时,读取配置server.hz的值,默认每秒执行server.hz次ServerCron() -> databases() -> ActiveExpireCycle()函数,activeExpireCycle()函数对每个expire[*]逐一进行检测,每次检测250ms/server.hz.
对于某个expire[ * ] 检测时,随机的挑W个key进行检测:a.如果key已经超时,则删除key;b.如果在此轮删除的key数量大于W * 0.25,则循环;c.如果小于W * 0.25,检查下一个expire[*] 。
逐出算法
作用:用来解决空间不足的问题
Redis使用内存存储数据,执行每一个指令之前,会使用freeMemoryIfNeeded来检测内存空间是否充足,如果内存不足的话,redis会临时删除一些数据为当前指令清理空间,清理的过程称为逐出算法。
注:逐出算法可能不能够100%清理出足够的空间,不成功则反复执行,所有尝试之后还不能则返回OOM
影响逐出算法的相关配置:
# maxmemory
# 最大可用内存,默认全部使用
# maxmemory-samples
# 每次选取的删除的数据个数
# maxmemory-policy
# 逐出策略
# 1.检查易失数据(可能会过期的数据集,server.db[i].expires)
# ① volatile-lru:最近最少使用的数据淘汰
# ② volatile-lfu:最近使用次数最少的数据淘汰
# ③ volatile-ttl:挑选即将过期的数据
# ④ volatile-random:随机挑选
# 2.检测全库数据(server.db[i].dict)
# ① allkeys-lru:最近最少使用
# ② allkeys-lfu:最近使用次数最少的数据
# ③ allkeys-random:随机挑选
# 3.放弃逐出策略
# ① no-enviction
高级数据类型
bitmaps
是对bit进行操作的数据类型
# setbit key offset value
# 设置key上的偏移量上的bit值为value,value只能为0 or 1
# getbit key offset
# 获取指定key偏移量上的bit值
# bitcount key start end
# 统计key中指定长范围下为1的数量
# bitop op destkey key1 [key2...]
# 对指定的key进行操作,将结果存入到destkey中
# and/or/not/xor
HyperLogLog
统计不重复的数量,基数是数据中不重复的数量
# pfadd key element1 [element2]
# 添加数据
# pfcount key1 [key2]
# 统计数据
# pfmerge deskey sourcekey1 [sourcekey2]
# 合并数据
GEO
存储地理位置信息
# geoadd key longitude latitude member
# 添加数据
# geopos key member
# 获取数据的经纬度
# geodist member1 member2
# 计算两个地理位置的距离,默认为m
主从复制
多台服务器连接方案:
提供数据方:master -> 主节点,主服务器,主库
接受数据方:slave -> 从节点,从服务器,从库
主从复制:把master的数据即时的,有效的复制到slave中。一个master可有多个slave,一个slave只能由一个master
作用:
①读写分离,master写数据,slave读数据,提高服务器的读写负载均衡;
②负载均衡:基于主从复制结构,配合需求改变slave的数量,通过多个从节点分担数据获取负载,大大提高redis的服务器并发量和数据吞吐量;
建立连接阶段
①设置master的地址和端口,保存master信息
②简历socket连接
③发送ping
④身份认证
⑤发送slave端口信息
此时的状态:
slave的状态:保存master的地址和端口;
master的状态:保存slave的端口;
两者之间有创建连接的socket
三种实现方式:
①在slave服务器上面加入 slaveof host port;
②在启动服务器时加入 如 redis-server ./conf/redis-6379.conf –slaveof host port;
③在配置文件中加入 slave of host port
断开连接:slave of no one
数据同步阶段
①请求同步数据(slave -> master)
②master执行bgsave指令,创建RDB文件并创建缓冲区;
③slave接受RDB文件,清空数据,执行RDB的恢复过程;
④请求部分数据(发送的时缓冲区中的指令,AOF),所以slave接收到数据之后使用bgrewriteaof来后台执行;
⑤恢复部分数据
注重点?。?!:全量复制和部分复制
全量复制:在执行bgsave的时候,master将数据通过RDB的当时发送给slave
部分复制:在全量复制的过程中,将这个阶段的指令存入到缓冲区中,等全量复制结束之后,将缓冲区中的指令通过AOF的形式发送到slave中,通过bgrewriteaof来执行恢复
此时的状态:
slave:有master的所有数据,包括RDB过程中接收到的数据;
master:保存了slave当前同步数据的位置
命令传播阶段
出现断网的情况:
①闪断闪联:忽略;②短时间中断:部分复制;③长时间中断:全量复制
部分复制的三个要素:
①服务器运行的id(Runid):每次运行,每个服务器都有一个Runid,40位16进制的随机数
②主服务器的复制缓冲区:队列,每次当master向slave发送时,会将记录存储在复制缓冲区中;
缓冲区包括两行,第一行时偏移值,第二行是具体的字节;
例如 set name hu先转换成AOF存储的形式4\r\nname\r\n$2\r\nhu\r\n,然后存储在缓存区中
偏移量 | 4321 | 4322 | 4323 | 4324 | … | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
字节指令 | … | $ | 3 | \r | \n | s | e | t | \r | \n | $ | 4 | \r | \n | n | a | m | r | … |
③主从服务器的复制偏移量:如上所示
心跳机制
salve:
指令:REPLCONF ACK offset
周期:1s
作用:①汇报slave的偏移量,获取最新的数据变更指令;②判断master是否存活
master:
指令:ping
周期:10s as default
作用:判断slave是否在线
总结主从复制的完整过程
master | slave |
---|---|
建立连接阶段 | |
数据同步阶段(全量复制、部分复制) | |
①发送指令replconf ack offset(slave) | |
②接收到命令,判断offset(slave)是否在复制缓冲区中 | |
③如果不在的话,全量复制; 如果在,且offset(slave) = offset(master),忽略; 如果 offset(slave) != offset(master),通过部分复制,把从offset(slave) 到offset(master)的数据传入到slave中 | |
④收到offset(master),接受数据,执行完重新从①开始 |
Redis哨兵
Sentinel:是一个分布式系统,用于对主从结构中的每台服务器进行监测,当出现故障可以通过投票选出新的master并将slave连接到新的master中。
作用:
①检测:检测master和slave是否能够正常运行;
②通知:当被监控服务器出现问题时,向其他的哨兵发送通知;
③自动故障转移:当master断开的时候,选取一个slave作为新的master,并和其他的slave相连接并告知客户端新的master地址。
配置哨兵
redis-sentinel sentinel-port.conf
工作原理
阶段一:监控,用于同步各个节点之间的状态
①获取各个sentinel的状态是否在线;
②获取master的状态:runid;role:master;以及该master各个slave的详细信息;
③获取master中所有slave的状态:runid;role:slave;master_host;master_host;offset
阶段二:通知阶段,哨兵集群之间的信息互通
阶段三:故障转移阶段
若sentinel向master发送信息,未接受到信息,则标记master为SRI_S_DOWN,并在sentinel内网中传播,使得其他sentinel都去向master求证是否真的下线,若超过半数的sentinel求证得到SDI_S_DOWN(主观下线),则标记master为SDI_O_DOWN(客观下线)并认为其下线。
若客观下线,则投票选出新的master,在sentinel内部,每个salve发送自己的被选择次数和runid,先进来的先被选择,若是有超过半数的slave,则选择成功,若没有,则进行下一轮的票选,此轮被选择的slave被选+1。
被选择的原则:①在线的slave;②响应快的slave;③与原来的master断开时间长的不?。虎苡畔仍?/p>
当选中之后,向新的master发送slaveof no one,断开连接;②向其他的slave发送新master的runid,并连接新的master。
redis集群
cluster
集群:使用网络将若干计算机连接起来,提供一个统一的管理方式,使其对外呈现单机的效果。
作用:
①分散单台计算机的访问压力,实现负载均衡;
②分散单台计算机的存储压力,实现可扩展性;
③降低单机下线带来的损失。
哨兵故障转移期间redis不可用,所以使用cluster集群,内部实现哨兵的作用但是不需要哨兵。
redis-cli -c 来开启集群
数据存储设计:
①通过算法设计,计算key应该保存的位置;
②将所有的存储空间划分成16384份(slot,槽),每台主机保存一部分;通过哈希算法得到数据所在的slot。
③将key按照计算除的结果放到对应的存储空间。
一致性哈希原理:将所有的数据当作一个token环,token中的数据范围市0-2^32,
为每一个数据节点分配一个token范围值,这个节点就负责保存这个范围内的数据。
对么一个key进行hash运算,被哈希后的结果在那个token范围内,则按顺时针去寻找最近的节点,
这个key将会被保存到这个节点
内部通讯设计:
①各个数据库相互通信,保存各个库中槽的编号;
②一次命中,直接返回;
③一次未命中,被告知位置,二次命中。
若master宕机,slave变成master,旧master重新上线,变成新master的slave。
其他
缓存与数据库的一致性
不一致分为三种
①数据库中有数据,缓存中没有数据;
②数据库中有数据,缓存中也有数据,数据不一致;
③数据库中没有数据,缓存中有数据;
缓存策略:cache Aside Pattern
①首先尝试从缓存中读取数据,若是成功,则直接返回,若是不成功,则读数据库,并把数据写道缓存中;
②需要更新数据的时候,先更新数据库,然后把缓存中的数据失效掉。
缓存预热
在系统启动前,提前将相关的缓存数据直接加载到缓存系统中,避免在用户请求的时候,先查询数据库再将数据缓存的问题。用户直接实现查询预热的数据。
缓存穿透
查询大量一个不存在的key,服务器绕过Redis直接向数据库去查询,极大的增加了数据库的压力。
解决方案:布隆过滤器
布隆过滤器
布隆过滤器:基于布隆算法,来解决缓存穿透问题。
布隆算法:对一个key通过k个hash算法计算出k个hash值,将这些hash值在bit数组中对应的位置置为1,然后查询的时候直接查看这k个位置都为1的话,则判断该key存在。在Redis中,使用bitmaps来作bit数组,支持2^32大小。
布隆算法的错误率:当布隆算法判断一个key不存在,则一定不存在,若是判断一个key存在,则不一定存在,有可能出现误判的情况。出现的原因是hash碰撞导致的。
缓存击穿
一个key在过期的时候还是超热数据,服务器直接绕过Redis去数据库访问,增大压力
解决方案:第三方缓存;分布式锁
分布式锁
# setnx key value
# 设置分布式锁
# expire key time
# 设置时间防止死锁
缓存雪崩
①同一时间多个可以过期,但是还是热数据,服务器绕过Redis去访问数据库,增大压力;②Redis宕机
解决方案
针对①:给过期的key增加随机时间,让所有的key均匀的过期而不是集中的过期。
针对②:设置Redis集群