NOSQL

非关系型数据库,not only sql,nosql不依赖于业务逻辑实现,仅仅以简单的键值对方式存储。
NOSQL数据库分为很多类型,文件存储,列式存储等。
统一来看,NOSQL数据库是为了性能优先而改变原关系型数据库的存储形式的一种数据库

特点

  1. 不遵循sql标准
  2. 不支持ACID。(原子,一致,隔离,持久)
  3. 远超过sql的性能

应用场景:

  1. 高并发
  2. 海量数据
  3. 对数据的高扩展,秒杀

不适用场景:

  1. 需要事务支持
  2. 基于sql的结构化查询,处理复杂的关系。

Redis

安装:略
端口:6379
数据库个数:默认16个,从0开始,默认使用0号库;使用select (select 8)切换。
密码:统一密码,所有库相同
实现方式:单线程+多路IO复用。(todo)

多路IO复用:

数据类型

类型 特点 数据结构 主要操作命令
String 最基本的数据类型,最大支持512M;
二进制安全,图片视频等可以转成字符串进行传输或保存;

动态字符串,类似ArrayList。

初始值大于value长度,扩容时小于1M,翻倍扩容;大于1M,扩容1M。

1. 【set】命令添加数据;
1. 【setnx】无对应key时添加数据;
1. 【mset/msetnx】设置多条值;
1. 【getset】修改值;
1. 【get】获取数据;
1. 【mget】获取多条数据;
1. 【append】追加值;
1. 【strlen】获取值长度;
1. 【incr/decr】 数字类型值+/-1【原子性操作】;
1. 【incrby/decrby】数字类型值+/-n;
1. 【getrange/setrange】获取/修改范围内的值;
1. 【setex】插入的同时设置过期时间。
List 单键多值,字符串列表,按照插入顺序排列,可以选择左侧和右侧插入。 双向循环链表+压缩列表。

压缩列表是一块连续的内存区域,使用压缩链表能够减少空间浪费,也能够保证查找速度和插入速度。
- 列表中保存的单个数据小于64个字节
- 列表中数据个数少于512个

存储数据较多时,或不满足以上两个条件,使用双向循环链表。

压缩列表和数组的区别:
1. ziplist可以存储不同类型数据;数组只能存储相同类型。
1. ziplist和数组都是连续的内存空间
1. ziplist存储数据大小不同,数组大小相同
|
1. 【lpush/rpush】:从左/右插入一个列表;【从左边插入,会将左端的数据往右移动,新插入的在最左端】
1. 【lpop/rpop】:从左、右取出一个值【取出并非查询,值光键亡】;
1. 【rpoplpush】:从右端取出一个值插入到左端;
1. 【lrange】:根据范围取值;
1. 【lindex】:根据索引下标获取元素;【从左到右】
1. 【llen】:获取列表长度;
1. 【linsert before/after】:在前/后插入元素;
1. 【lrem】:删除n个为value元素;
1. 【lset】:修改下表为n的元素;
| | Set | 无序列表,不能重复,一个键可以对应多个值。 | ziplist+hash表

- 列表中保存的单个数据小于64个字节
- 列表中数据个数少于512个
存储数据较多时,或不满足以上两个条件,使用哈希表。 |
1. 【sadd】:添加一个或多个成员,已经存在成员被忽略;
1. 【smembers】:取出所有成员;
1. 【sismember】: 判断是否包含某value;
1. 【scard】:返回集合成员个数;
1. 【srem】:删除集合中的成员;
1. 【spop】:随机取出一个成员【值无键亡】;
1. 【srandmember】:随机取出n个值【不删除】;
1. 【smove】:移动值到另一集合;
1. 【sinter】:返回两个集合的交集;
1. 【sunion】:返回两个集合的并集;
1. 【sdiff】:返回两个集合的差集;
| | Hash | 键值对集合,一个键对应一个键值对集合,键值对的键不能重复 | 有序数组+哈希表
存储数据较多时,使用哈希表。 |
1. 【hset】:赋值或新增数据;
1. 【hget】:取出数据;
1. 【hmset】:批量设置hash的值;
1. 【hexists】:查看哈希表中key是否存在给定的域(属性);
1. 【hkeys】:列出hash集合的所有属性名;
1. 【hvals】:列出hash集合的所有值;
1. 【hincrby/hdecrby】:hash表中key的域的属性值+n/-n;
1. 【hsetnx】:设置key的域value值,【当原域不存在时】;

| | Zet | 在set的基础上增加了排序,依靠score从低到高排序 | 压缩列表+跳表

跳表全称为跳跃列表,它允许快速查询,插入和删除一个有序连续元素的数据链表。跳跃列表的平均查找和插入时间复杂度都是O(logn)。 |
1. 【zadd】:添加一个或多个成员及评分;
1. 【zrange】:返回根据评分排序(低到高)的范围内有序集合的成员,【withscores显示评分】;
1. 【zrangebyscore】:返回score介于某范围的成员;
1. 【zincrby/zdecrby】:成员评分+n/-n;
1. 【zrem】:删除指定成员;
1. 【zcount】:统计区间内成员个数;
1. 【zrank】:返回排名,从0开始;
|

字符串(String)

String是redis中最基本的数据类型。一个String value 最多可以存储512M
String是二进制安全的。也就是说图片,视频等序列化的对象转成字符串之后进行存储,同样可以通过字符串转换会原格式。

数据结构:简单的动态字符串类似ArrayList。采用预分配冗余空间的方式减少内存的频繁分配。
value初始空间分配高于实际字符串长度;扩容时,长度小于1M,加倍扩容。超过1M,每次扩容1M。最大512M。

操作命令:(基础命令中有具体操作)

  1. 【set】命令添加数据;
  2. 【setnx】无对应key时添加数据;
  3. 【mset/msetnx】设置多条值;
  4. 【getset】修改值;
  5. 【get】获取数据;
  6. 【mget】获取多条数据;
  7. 【append】追加值;
  8. 【strlen】获取值长度;
  9. 【incr/decr】 数字类型值+/-1【原子性操作】;
  10. 【incrby/decrby】数字类型值+/-n;
  11. 【getrange/setrange】获取/修改范围内的值;
  12. 【setex】插入的同时设置过期时间。

列表(List)

单键多值。多个值使用字符串列表存储。按照插入顺序排序。可以从头部或尾部添加。

数据结构:双向链表。两端的操作性能很高,通过下标索引效率低,查询效率不高。
快速链表由压缩列表+双向链表组成。

为什么要使用压缩列表:
压缩列表是一块连续的存储空间,数据量较多时变为快速链表,多个压缩列表作为双向链表的节点。
普通链表每个节点之间都需要指针空间,普通节点换成压缩列表后压缩列表中的多个节点不需要存储指针,减少空间浪费。同时又能够满足快速插入删除。

压缩列表和数组的区别:

  1. ziplist可以存储不同类型数据;数组只能存储相同类型。
  2. ziplist和数组都是连续的内存空间
  3. ziplist存储数据大小不同,数组大小相同

操作命令:

  1. 【lpush/rpush】:从左/右插入一个列表;【从左边插入,会将左端的数据往右移动,新插入的在最左端】
  2. 【lpop/rpop】:从左、右取出一个值【取出并非查询,值光键亡】;
  3. 【rpoplpush】:从右端取出一个值插入到左端;
  4. 【lrange】:根据范围取值;
  5. 【lindex】:根据索引下标获取元素;【从左到右】
  6. 【llen】:获取列表长度;
  7. 【linsert before/after】:在前/后插入元素;
  8. 【lrem】:删除n个为value元素;
  9. 【lset】:修改下表为n的元素;

集合Set

set提供的功能类似list,区别是可以自动排除重复元素。

set集合是一个无序集合,通过hash算法找到对应的位置。添加,删除,查找复杂度都是O(1)。

数据结构:字典,哈希表。和java中的hashSet类似。

操作命令:

  1. 【sadd】:添加一个或多个成员,已经存在成员被忽略;
  2. 【smembers】:取出所有成员;
  3. 【sismember】: 判断是否包含某value;
  4. 【scard】:返回集合成员个数;
  5. 【srem】:删除集合中的成员;
  6. 【spop】:随机取出一个成员【值无键亡】;
  7. 【srandmember】:随机取出n个值【不删除】;
  8. 【smove】:移动值到另一集合;
  9. 【sinter】:返回两个集合的交集;
  10. 【sunion】:返回两个集合的并集;
  11. 【sdiff】:返回两个集合的差集;

哈希(hash)

hash存储的是一个键值对集合
类似于hashMap的Object是一个对象,对象包含多个属性【user:{id=1,name=zhangsan, age=20}】。
或者HashMap>,一个key,对应一个map对象,map中有多个key。

数据结构:对应两种数据结构,当键值对长度较短且个数较少时,使用压缩列表,否则使用哈希表

操作命令:

  1. 【hset】:赋值或新增数据;
  2. 【hget】:取出数据;
  3. 【hmset】:批量设置hash的值;
  4. 【hexists】:查看哈希表中key是否存在给定的域(属性);
  5. 【hkeys】:列出hash集合的所有属性名;
  6. 【hvals】:列出hash集合的所有值;
  7. 【hincrby/hdecrby】:hash表中key的域的属性值+n/-n;
  8. 【hsetnx】:设置key的域value值,【当原域不存在时】;

有序集合(zset)

有序集合,在set的基础上加了评分进行排序。
成员和set一样不能重复,评分可以重复。
可以通过评分获取一个范围的成员。
由于评分的存在,访问集合中间元素也是速度很快的。
数据结构:
跳表,用于给value排序,根据score获取范围内元素。

跳表

image.png

跳表全称为跳跃列表,它允许快速查询,插入和删除一个有序连续元素的数据链表。跳跃列表的平均查找和插入时间复杂度都是O(logn)。
快速查询是通过维护一个多层次的链表,且每一层链表中的元素是前一层链表元素的子集。一开始时,算法在最稀疏的层次进行搜索,直至需要查找的元素在该层两个相邻的元素中间。这时,算法将跳转到下一个层次,重复刚才的搜索,直到找到需要查找的元素为止。

操作命令:

  1. 【zadd】:添加一个或多个成员及评分;
  2. 【zrange】:返回根据评分排序(低到高)的范围内有序集合的成员,【withscores显示评分】;
  3. 【zrangebyscore】:返回score介于某范围的成员;
  4. 【zincrby/zdecrby】:成员评分+n/-n;
  5. 【zrem】:删除指定成员;
  6. 【zcount】:统计区间内成员个数;
  7. 【zrank】:返回排名,从0开始;

redis6新数据类型

Bitmaps

使用bitmaps实现位操作。它本身不是一种数据类型,就是通过字符串形式存储,但是可以对字符串的位进行操作。
类似一个只存储了0和1的数组,整个字符串构成一个字符串。数组的下标叫做偏移量。

bitmap和set占用空间对比:
1亿总用户,5000万用户活跃的记录。数据量大,需要记录的占比大时效率高,反之,浪费空间较大。
image.png

操作命令:

  1. setbit:设置bitmaps中某个偏移量的值。偏移量从0开始;
  2. bitcount:统计值为1的偏移数量。【可以假如开始和结束位置】
  3. bitop and/or/not/xor:进行与/或/非/异或的操作,将结果放到指定的新集合。

hyperLogLog

主要用于基数统计(将数据集去重)的一种算法。例如,网站统计独立访客,独立ip等情况,需要用到去重操作,使用其他数据类型也能够实现,且结果精确,但是消耗空间资源较大。以上场景的精确统计是没有必要的,降低精度来平衡存储空间使用hyperLogLog是一种优秀解决方案。

  • 优点:输入元素数据量或体积非常大时,计算基数所需的空间总是固定的。只需要使用12k的空间,就能够计算2^64个不同的元素的基数。
  • 缺点:只能够进行数量统计,无法返回具体元素。

基数集:即数据集去重后的集合。
操作命令:

  1. pfadd:添加制定元素到HyperLogLog中;
  2. pfcount:统计数量;
  3. pfmerge:合并集合;

    geospatial

    对地理信息二维坐标(经纬度)的操作。

操作命令:

  1. geoadd:添加经纬度信息;北极点,南极点不能添加。有效经度(-180~180),有效维度(-85~85);
  2. geopos:获取制定地区坐标;
  3. geodist:获取两个地点的直线距离,【需要显示单位,默认米:m,米;km,千米;mi,英里;ft,英尺】;
  4. georadius:获取以某点(经纬度)为中心,一定半径内的元素。

基础命令

  1. // 连接数据库
  2. redis-cli
  3. // 查看数据库key的数量
  4. dbsize
  5. // 清空当前库
  6. flushdb
  7. // 清除所有库
  8. flushall
  9. // 填加数据
  10. set <key> <value>
  11. // 填加数据,(不存在key时添加成功)
  12. setnx <key> <value>
  13. // 填加多条数据
  14. mset <key> <value>....
  15. // 填加数据,(不存在key时添加成功)
  16. msetnx <key> <value>....
  17. // 查询数据
  18. getnx <key> <key>....
  19. // 查看当前库所有的key
  20. key *
  21. // 判断某个key是否存在
  22. exits key
  23. // 查看某个key的类型
  24. type key
  25. // 删除某个key
  26. del key
  27. // 根据value选择非阻塞删除,(真实删除操作后续异步执行)
  28. unlink key
  29. // 设置过期时间
  30. expire key <time>
  31. // 查看还有多长时间过期, (-1永不过期,-2已过期)
  32. ttl key
  33. // 获取数据
  34. get key
  35. // 追加值
  36. append key value
  37. // 获取值长度
  38. strlen key
  39. // 值+1;值-1(值为数字类型)
  40. incr key
  41. decr key
  42. // 值+n;值-n
  43. incrby key n
  44. decrby key n
  45. // 获取范围内的值
  46. getrange key a b
  47. // 修改下标后的值
  48. setrange key n value
  49. --------list---------
  50. // 左/右端插入列表
  51. lpush/rpush key v1 v2..
  52. // 左/右端取出一个值
  53. lpop/rpop key
  54. // 右端取出插到左端
  55. rpoplpush key1 key2
  56. // 根据范围取值(-1,表示取出所有值)
  57. lrange key a b
  58. // 获取下标是n的元素(从左到右)
  59. lindex key n
  60. // 获取列表长度
  61. llen key
  62. // 在value前/后(从左往右)插入一个新值
  63. linsert key before/after value newvalue
  64. // 删除n个为value的元素
  65. lrem key n value
  66. // 修改下标为n的值为value
  67. lset key n value
  68. --------set----------
  69. // 添加值
  70. sadd key v1 v2..
  71. // 取出值
  72. smembers key
  73. // 是否存在value (有1,无0)
  74. sismember key value
  75. // 返回成员个数
  76. scard key
  77. // 删除value
  78. srem key v1 v2..
  79. // 随机取出一个值
  80. spop key
  81. // 随机取出n个值,不删除
  82. srandmembers key n
  83. // 移动成员到另一个集合
  84. smove key key2 value
  85. // 返回两个集合的并集
  86. sunion k1 k2
  87. // 返回两个集合的交集
  88. sinter k1 k2
  89. // 返回两个集合的差集
  90. sdiff k1 k2
  91. --------hash--------
  92. // hash集合赋值
  93. hset key f1 v1
  94. // hash集合取值
  95. hget key f1
  96. // 批量赋值
  97. hmset key f1 v1 f2 v2
  98. //查看hash表给定域是否存在
  99. hexists key f1
  100. // 列出hash表的所有域
  101. hkeys key
  102. // 列出hash表所有域的值
  103. hvals key
  104. // 值+n/-n
  105. hincrby/decrby key f1 n
  106. // 为key的域赋值,【域不存时】
  107. hsetnx key f1 v1
  108. -------zset--------
  109. // 添加一个或多个值
  110. zadd key s1 v1 s2 v2
  111. // 查询范围内(第a到b)的成员【根据评分排序后的结果】(包括评分)
  112. zrange key a b (withscores) (limit)
  113. // 根据评分范围查询【b > a】
  114. zrangebyscore key a b (withscores) (limit)
  115. // 倒序排列【b > a】
  116. zrevrangebyscore key b a (withscores) (limit)
  117. // 成员评分+n/-n
  118. zincrby key n v1
  119. // 删除指定元素
  120. zrem key v
  121. // 统计范围内成员数
  122. zcount key s1 s2
  123. // 返回成员的排名
  124. zrank key v1
  125. -------bitmaps--------
  126. // 设置偏移量的值
  127. setbit key offset value
  128. // 获取偏移量的值
  129. getbit key offset
  130. // 统计值被设置为1的数量(-1,最后一位;-2,倒数第二位)
  131. bitcount key offset1 offset2
  132. // 取并集
  133. bitop and newkey k1 k2
  134. ----------hyperLogLog---------
  135. // 添加value到hyperLogLog
  136. pfadd key v1 v2...
  137. // 统计数量
  138. pfcount key
  139. // 合并集合
  140. pfmerge newkey k1 k2
  141. // 添加多个经纬度信息
  142. geoadd key 经度 纬度 名称...
  143. // 获取坐标值
  144. geopos key name
  145. // 获取两地直线距离
  146. geodist key name1 name2 km
  147. // 获取某坐标半径r以内的所有元素
  148. georadius key 经度 维度 r km
  149. --------cluster-------
  150. cluster keyslot key】计算key属于哪个slot
  151. cluster countkeysinslot slotnum 】查询某个slot中有多少key,只能查看自己slot区间的数量
  152. cluster getkeysinslot slotnum limit】查询某个slot中的key,可以限制查询数量
  153. //
  154. //
  155. //
  156. //

配置文件

  1. 配置大小单位

大小写不敏感;只支持byte,不支持bit

  1. 引入其他文件
  2. 网络配置

【bind 127.0.0.1 -::1】只支持本地访问,不能远程连接
【protected-mode no/yes】是否开启保护模式
【port 6379】端口
【tcp-backlog 511】tcp连接队列日志,记录了已经完成和未完成的三次握手队列,高并发环境下需增大该值。另外linux会减小该值到128,所以需要同时增加/proc/sys/net/core/somaxconn的值。
【timeout 0】无操作超时时间,0:永不超时
【tcp-keepalive 300】tcp心跳检测周期,检测tcp连接是否存活

  1. 常规配置

【daemonize yes/no】后台启动
【pidfile /var/run/redis_6379.pid】pid文件目录,每次操作的进程号
【loglevel notice】日志级别(默认notice)。四种日志级别:1.debug 2.verbose 3.notice 4.warning
【logfile /dev/log】日志保存路径
【database 16】数据库的数量,默认16

  1. 安全设置

【requirepass 】密码设置,默认无密码。可以取消注释或通过config set requirepass "123456"设置。

  1. 限制设置

【maxclients 10000】最大客户端连接数,默认10000.
【maxmemory 】最大内存设置。必须设置。内存达到上限会通过移除规则(maxmemory-policy)删除数据。
【maxmemory-policy】数据移除规则。全员随机,全员lru,ttl随机,ttllru,不移除,ttl最小

  1. - volatile-lru:使用**LRU**算法移除key,**只针对设置了过期时间的键**。(最少使用)
  2. - allkeys-lru:**所有集合key**中,使用LRU算法移除。
  3. - volatile-random:在集合中**随机**移除,只针对**设置了过期时间的键**。
  4. - allkeys-random:**所有key**中,**随机**移除。
  5. - volatile-ttl:移除ttl最小的key,即**马上过期的key**。
  6. - noeviction:**不进行移除**,针对写操作。**只返回错误信息。**

【maxmemory-samples】样本数量。LRU算法和TTL算法并非精确算法,通过设置样本大小,redis检查样本数量的key并选择其中LRU的那个。一般为3-7,数字越小,越不准确,性能消耗越小。

  1. 快照配置

【dbfilename dump.rdb】数据持久化文件名
【dir ./】文件位置(相对路径)
【stop-writes-on-bgsave-error yes/no】当redis无法写入磁盘后,是否直接关掉redis,建议yes。
【rdbcompression yes/no】持久化文件是否进行压缩。(LZF算法,消耗额外cpu)
【rdbchecksum yes】持久化文件是否进行完整性检查。(CRC64算法,增大10%cpu消耗)
【save 900 1】save秒钟,触发持久化间隔设置(默认一分钟修改10000次,或5分钟10次,或15分钟1次)
———aof—————
【appendonly yes/no】是否开启aof
【appendfilename “appendonly.aof”】aof生成文件名,存储路径和rdb一样
【appendfsync always】日志记录频率

  1. - **always**:始终同步,每次redis的写入都会**立刻记入**日志文件,性能较差,完整性高
  2. - **everysec**:**每秒同步**,每秒记入日志一次,宕机只会丢失前一秒的数据
  3. - **no**:**不主动同步**,把同步实际交给操作系统

【auto-aof-rewrite-precentage】重写基准百分比,文件达到基准值(100%)时开始重写。
【auto-aof-rewrite-min-size】重写基准值,最小64M

  1. 集群配置

cluster-enabled yes】开启集群模式
【cluster-config-file xxxx.conf】设定节点配置文件名
【cluster-node-timeout 15000】设定节点失联时间,超时自动进行主从切换

LRU淘汰算法

LRU淘汰算法根据最近数据使用的频率对访问的可能性进行预测
最近被频繁访问的数据将来被访问的可能性也越大。缓存中的数据一般会有这样的访问分布:一部分数据拥有绝大部分的访问量。当访问模式很少改变时,可以记录每个数据的最后一次访问时间,拥有最少空闲时间的数据可以被认为将来最有可能被访问到。
举例如下的访问模式,A每5s访问一次,B每2s访问一次,C与D每10s访问一次,|代表计算空闲时间的截止点:
image.png
可以看到,LRU对于A、B、C工作的很好,完美预测了将来被访问到的概率B>A>C,但对于D却预测了最少的空闲时间。
redis实现的是近似LRU算法,并非完全的LRU算法,通过设置【maxmemory-samples】样本数量,redis检查样本数量的key并选择其中LRU的那个。一般为3-7,数字越小,越不准确,性能消耗越小。

发布和订阅

redis发布订阅是一种消息通信模式:发送者发送消息,订阅者接收消息。
接收者可以订阅任意数量的频道。

  1. 两个客户端连接到redis。redis-cli
  2. 其中一个客户端订阅频道1。**SUBSCRIBE **channel1
  3. 另一个客户端向向频道1发送消息。**publish **channel1 hello
  4. 订阅者客户端会收到发送者的消息。

客户端工具

jedis,使用java语音操作redis

  1. 导入依赖。【redis.clients.jedis.3.2.0】
  2. 创建jedis对象。Jedis jedis = new Jedis(host:"",post:"");
  3. 测试。String value = jedis.ping();

方法:
ping:ping通redis,保护模式下无法访问,绑定127.0.0.1无法访问,关闭服务器防火墙;

todo:
p18代码演示,手动实现。

SpringBoot整合redis

  1. 创建maven工程
  2. 导入依赖【spring-boot-starter-data-redis;commons-pool2】
  3. 修改配置文件
    • spring.redis.host:服务器地址
    • spring.redis.port:端口
    • spring.redis.database:数据库
    • spring.redis.timeout:连接超时时间
    • spring.redis.lettuce.pool.max-active:连接池最大连接数
    • spring.redis.lettuce.pool.max-wait:最大阻塞等待时间
    • spring.redis.lettuce.pool.max-idle:最大空闲连接
    • spring.redis.lettuce.pool.max-idle:最新空闲连接
  4. 创建redis配置类(固定写法)
  5. 测试

事务

redis的事务是一个单独的隔离操作。所有的命令都会序列化,按顺序执行。
事务在执行过程中不会被其他发送来的命令请求所打断。
redis事务主要的作用就是防止串联多个命令,防止别的命令插队。

执行流程

image.png
三个命令

  1. multi:开启事务,输入的命令会依次进入命令队列,等待执行。
  2. discard:组队阶段可以通过discard来放弃执行。
  3. exec:开启执行阶段,队列中的命令依次执行。

错误与回滚:

  • 组队阶段出现错误,整个队列会被取消,无法执行。
  • 执行阶段抛出错误,只有报错的命令不被执行,其他正常执行,不会回滚

redis事务冲突的解决办法
乐观锁,使用check-and-set机制实现。

在执行multi之前,先执行watch命令,通过watch命令监视多个key,如果事务执行前,存在key被其他命令修改,那么事务将被打断。

redis事务的三个特性

  1. 单独的隔离操作。事务中所有命令都会序列化按顺序执行,事务执行过程中不会被其他客户端发送的命令打断。
  2. 没有隔离级别的概念。命令没有提交之前都不会被实际执行,因为事务提交前任何指令都不会被实际执行。
  3. 不保证原子性。事务中一条命令执行失败,其他命令仍然被执行,没有回滚。

秒杀案例

todo

持久化

redis中数据存在内存中,将内存中的数据写入到磁盘中叫做持久化。
两种不同的持久化方式:

  1. RDB(redis Database)
  2. AOF(append of File)

RDB

默认开启
指定的时间间隔内将内存中的数据集快照写入磁盘,恢复时直接从快照文件读取到内存中

redis会fork一个单独的子进程来进行持久化,会将数据先写入到一个临时文件中,待持久化过程结束,再使用这个临时文件替换上次持久化的文件

image.png

优点:

  1. 适合大规模的数据恢复
  2. 对数据完整性和一致性要求不高更适合使用
  3. 节省磁盘空间
  4. 恢复速度快

缺点:

  1. fork后,复制了一份相同子进程,内存中数据也被克隆了一份,2倍内存膨胀。
  2. fork时,数据量庞大,资源消耗比较严重
  3. 备份周期在一定间隔时间做一次备份,redis意外宕机,会丢失最后一次快照后的所有修改。

适用场景 如果需要大规模数据恢复,且对于数据恢复的完整性不敏感,那么RDB方式要比AOF更加高效。

临时文件的意义(为什么不直接写入到目标持久化文件):
为了防止在复制的过程中,因服务器宕机导致数据丢失问题。
确保完全复制到临时文件中,保证数据一致性后,替换原持久化文件。

SAVE和BGSAVE区别:

  • 都是将当前 Redis 实例的所有数据快照(snapshot)以 RDB 文件的形式保存到硬盘
  • SAVE 保存是阻塞主进程,客户端无法连接redis,等SAVE完成后,主进程才开始工作,客户端可以连接。配置【save m n】后在m秒或n次请求后自动执行保存,也可以手动保存。
  • BGSAVE 是fork一个save的子进程,在执行save过程中,不影响主进程,客户端可以正常链接redis,等子进程fork执行save完成后,通知主进程,子进程关闭
  • 客户端可以通过 LASTSAVE 命令查看相关信息,判断 BGSAVE 命令是否执行成功。

Fork

fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量,环境变量,程序计数器等)数值与原进程一致,但是一个完全的进程,作为原进程的子进程。

写时复制技术: 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段内容要发生变化时,才会将父进程的内容赋值给子进程一份。

rdb的备份恢复

  1. 关闭redis
  2. 将备份rdb文件拷贝到配置文件中的rdb文件目录
  3. 启动redis,rdb文件自动加载

AOF

默认不开启
append only file,以日志的形式记录每个写操作(增量保存),将redis执行过的所有写指令记录下来读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取文件重新构建数据。换句话说,redis重启的话就根据日志文件的内容将写指令从前到后执行一次,来完成数据的恢复工作。
AOF和RDB同时开启,redis默认取AOF数据。因为AOF不会造成数据丢失。

优点:

  1. 备份机制更稳健,丢失概率更低。
  2. 可读的日志文本,通过操作AOF文件,可以处理误操作。

缺点:

  1. 比rdb占用更多磁盘空间
  2. 恢复备份速度更慢
  3. 每次写都同步,性能浪费
  4. 存在个别bug,aof文件不能恢复

持久化流程

  1. 客户端请求命令append追加到aof缓冲区。
  2. aof缓冲区根据aof持久化策略,将操作sync同步到磁盘aof文件中。
  3. aof文件大小超过重写策略或手动重写时,会压缩aof文件容量。
  4. redis重启时,重新加载aof文件中的写操作到达恢复数据的目的。

    【appendfsync always】日志持久化策略

    • always:始终同步,每次redis的写入都会立刻记入日志文件,性能较差,完整性高
    • everysec每秒同步,每秒记入日志一次,宕机只会丢失前一秒的数据
    • no不主动同步,把同步实际交给操作系统

    【auto-aof-rewrite-precentage】重写基准百分比,文件达到基准值(100%)时开始重写。 【auto-aof-rewrite-min-size】重写基准值,最小64M

image.png

重写压缩

重写机制:当aof的文件大小超过所设定的阈值时,redis启动aof文件内容压缩,将指令进行压缩,多条指令合并成一条不影响结果的指令,只保留可以恢复数据的最小指令集。手动重写可以使用命令bgrewriteaof操作。

重写机制实现原理

aof文件过大时,会fork一条新进程来将文件重写(先写临时文件后rename重命名),redis4.0后的重写,是把rdb的快照以二进制的形式附在新aof的头部,作为已有历史数据,替换掉之前的流水账操作。

触发机制

默认配置是当aof文件大小是上次rewrite后大小(redis会记录上次重写时aof的大小)的一倍(auto-aof-rewrite-precentage 100)且文件大于64M(auto-aof-rewrite-min-size 64M)时触发。

重写流程
  1. bgwriteaof触发重写,判断当前是否有bgsave或bgrewriteaof在运行,如果有则等待该命令结束再执行。
  2. 主进程fork子进程执行重写操作,保证主进程不阻塞。
  3. 子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区,保证原aof文件完整以及新写入的数据不会丢失。
  4. 子进程写完aof文件后,向主进程发送信号,父进程更新统计信息。
  5. 主进程把aof_rewrite_buf中数据写入到新的aof文件。
  6. 使用新的aof文件覆盖旧的aof文件,完成重写。

image.png

AOF备份恢复

  • 正常备份恢复和RDB一致,替换aof文件后,启动redis自动加载。
  • 异常恢复,遇到aof文件损坏

通过**redis-check-aof --fix appendonly.aof**命令进行恢复,
重启redis,自动加载恢复的aof文件

持久化方式选择建议

RDB能够在指定间隔内备份数据快照
AOF能够记录每次写操作,并且后台进行aof文件重写,保证aof文件体积,确保文件完整性

  • 官方推荐两个都启用
  • 对数据完整性不敏感推荐使用rdb
  • 不建议单独使用aof,因为可能出现bug
  • 只做内存缓存,可以都不用

同时开启两种持久化方式,redis重启时优先载入aof文件来恢复原始数据,因为在通常情况下aof文件保存的数据集比rdb文件保存的数据集要完整。

为什么不建议只开启aof? rdb更适合用于备份数据库(aof一直在变化不适合备份),快速重启且不会有aof文件错误bug风险。

性能建议:

  1. rdb只做后备用途,建议只在slave上持久化rdb文件,15分钟一次(save 900 1)。
  2. 使用aof,最多丢失两秒数据,同时带来了持续的IO,重写aof文件会造成阻塞。应当减少aof rewrite频率,重写基础大小建议设置5G以上,默认超过原大小100%重写可以修改。

主从复制

主机数据更新后根据配置和策略,同步到备机。master以写为主,slave以读为主。

image.png

优点:

  1. 读写分离,性能更高。
  2. 容灾恢复速度快,指从机恢复

一主多从情况下,一台从机宕机,可以从其他从机读取数据。并不能解决主机宕机问题。

主从复制配置

一主两从模式

  1. 创建三个配置文件,端口区分。
  2. 修改配置文件
    • 【关闭aof或修改aof配置名称】
    • 【include 公共配置文件】 引入原配置文件
    • 【pdifile 目录】设置pid文件目录
    • 【port 6379】配置端口号
    • 【dbfilename dump6379.rdb】配置备份文件名称
  3. 启动三个服务info replication查看服务是master或slave
  4. 从机执行命令slaveof masterhost 6379加入主机
  5. 从机重启后续重新执行命令加入。【加入主机后,会同步数据】
  6. 主机宕机后,默认从机不会升级为主机,主机重启后,从机会自动与主机建立连接。

复制原理

分为全量复制和增量复制。Redis全量复制一般发生在Slave初始化阶段,后续增量复制。

  1. 从连接主后,从发送消息请求同步数据。
  2. 主接收请求,先将数据进行持久化,存到rdb文件中
  3. 主把rdb文件发送给从,从节点从rdb文件中读取数据【全量复制
  4. 每次主进行写操作后,会与从服务器同步【增量复制】

image.png

薪火相传模式

一主(A)二从(B,C)模式下
其中一台从机加入另外一台从机slaveof Bhost 6380,两台从机都还是从机,但是他们的主机并不都是master,其中一个从机(B)的主机是另一台主机(C)。
在数据进行同步时(主机将新的写入数据同步给从机),master只会同步给其中一台主机(B),再由这一台主机同步给另外一台(C)。
缺点:其中一个节点宕机,会造成后续的同步无法完成。修改从属机器时会清除数据重新同步

反客为主模式

当主节点宕机后,可以在从节点上执行slaveof no one来升级成为主节点。
缺点:需要手动执行命令。

哨兵模式

image.png
哨兵模式在反客为主的基础上,自动执行。后台监控主机是否故障,如果故障了根据投票数自动将从升级为主。

哨兵的作用

  • 通过发送命令,让Redis服务器返回其运行状态,包括主服务器和从服务器。
  • 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。

多哨兵模式:一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。

故障切换(failover)的过程:

  • 假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线
  • 当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票
  • 投票的结果由其中一个哨兵发起,进行failover操作。
  • 切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。

哨兵模式配置:

  1. 创建sentinel.conf哨兵配置文件
  2. 【sentinel monitor mymaster masterhost port num】

mymaster是为监控服务器起的名称,num是多少个哨兵同意后可以迁移

  1. 【redis-sentinel 配置文件路径】启动哨兵模式,端口:26379

选取规则

  1. 选择优先级靠前的。【redis.conf中默认配置slave-priority 100,数值越小优先级越高】
  2. 选择偏移量大的。【指原主机数据最全的的节点】
  3. 选择runid最小的服务。【每个redis实例启动后都会随机生成一个40位的runid】
  4. 选取出新的主节点后,哨兵向原主节点的从节点发送slaveof命令,从节点的主节点变为新主节点。
  5. 下线的服务上线时,会向其发送slaveof命令,成为新主节点的从节点。

集群

容量不够时,redis如何进行扩容?

使用集群模式进行扩容,增加集群服务器,集群的内存可以用来进行扩容。

并发写操作时,redis如何处理?

使用集群模式处理,多个master节点同时执行写操作,分担压力。

集群的作用:

  • 扩容
  • 提高性能
  • 高可用

redis集群实现了redis的水平扩容,启动n个节点,将数据分布存储在n个节点中,每个节点存储1/n。
redis集群通过分区来提供一定程度的可用性:即使集群中一部分节点失效或无法进行通信,集群也可以继续处理命令请求。

任何模式下,主机宕机后,主节点的ip地址和端口会发生变化,解决方式是?

redis3.0之前使用代理主机来解决。3.0之后,redis提供了无中心化集群配置。 代理主机:一直请求代理主机的ip,代理主机再去请求不同阶段的主节点。 无中心化集群:任何一台服务器都可以作为主机,收到请求会进行判断是否属于自己的请求,如果不是会将请求转发给目标机器。

构建集群

  1. 集群配置
    1. 【cluster-enabled yes】开启集群模式
    2. 【cluster-config-file xxxx.conf】设定节点配置文件名
    3. 【cluster-node-timeout 15000】设定节点失联时间,超时自动进行主从切换

todo: 配置文件完善

  1. 启动服务
  2. 将单独的服务合并成集群

在redis的src目录下执行命令

  1. redis-cli --cluster create --cluster-replicas 1 hostname:port....
  2. 1:以最简单的方式配置集群
  3. 执行命令后redis会自动分配集群主从节点。

image.png

  1. 测试。使用集群模式连接.
    1. // 集群方式连接
    2. redis-cli -c -p port
    3. // 查看集群信息
    4. cluster nodes
    1. <br />redis如何自动分配?

    一个集群至少三个节点。 【—cluster-replicas 1】表示希望每个主仅有一个从节点 分配原则尽量保证主数据库运行在不同的ip。每个从和主不在一个ip地址上。

slots

  • 一个redis集群包含16384个slots(插槽)。集群生成后会将16384个slots分配到主节点上。每个主节点负责一个区间,目的是为了将写入的数据平均分配到集群的每个主节点。
  • 在写入数据时,对数据的key进行计算【CRC16(key) % 16384】,计算出要写入的数据应当放到哪个clots,然后找到对应的主节点,进行存储。
  • 无中心化集群在连接时,任意连接节点,插入数据时计算出应当存储的节点后,将数据发送给目标节点存储。

插入多条数据时,无法直接插入(因为多个key值不属于同一个slots),可以通过组的形式插入。如**mset k1{user} v1 k2{user} v2**k1和k2属于同一个user组,在插入时就会计算user组属于哪一个slot,然后插入到同一个slots。

操作命令:
【cluster keyslot key】计算key属于哪个slot
【cluster countkeysinslot slotnum 】查询某个slot中有多少key,只能查看自己slot区间的数量
【cluster getkeysinslot slotnum limit】查询某个slot中的key,可以限制查询数量

集群故障恢复

  • 主机挂掉后,从机升级为主机,原主机上线后作为原从机的从机。
  • 从机挂掉后,主机不变,上线后还是从机。
  • 主从都挂掉,根据配置情况【cluster-require-full-coverage yes/no】yes,整个集群挂掉;no,该插槽数据全部宕机,也无法存储

问题解决

缓存穿透、缓存击穿、缓存雪崩

问题 原因 解决
缓存穿透
- 缓存中没有,数据库中也没有
- 大量请求在不停的请求不存在的数据

- 对空值做缓存,弊端是可能需要缓存大量的空值,造成浪费
- 布隆过滤器。有误判几率
缓存击穿
- 缓存过期,数据库有数据
- 大量请求不停请求过期key

- 热点数据设置永不过期
- 加锁,只有第一个请求可以达到数据库,其余请求等待第一个请求将数据重新放回缓存中,查询缓存。
缓存雪崩
- 大面积缓存过期,数据库中有数据
- 大量请求不停请求

- 配置高可用,多主多从
- 加锁,配置分布式锁
- 预先请求,过期前预先手动请求,重新加载
- 设置不同的过期时间,避免大面积同时过期

布隆过滤器

image.png

  • 一个大型的二进制位数组(bitmap),存储这大量的0和1。
  • 在缓存中增加数据时,通过多个hash函数计算得到多个下标,计算出的下标全部标记为1。
  • 判断key是否存在时,也是通过相同算法进行计算,如果下标都为1,说明存在
  • 有几率存在误判的情况。只会误判为存在,不会误判为不存在。

    误判原因:存在key1和key2,计算分别标记了n个下标,在判断key3时,可能存在计算key3的下标占了key1和key2下标的各一部分,本来不存在的key3,误判为存在。 误判几率和数组的大小、hash算法个数、hash算法的优劣有关。

分布式锁

分布式集群的形式使得单机的锁失效。需要一种跨jvm的互斥机制来控制共享资源的访问。
分布式锁的主流方案:

  1. 基于数据库实现分布式锁
  2. 基于缓存(redis)
  3. 基于zookeeper

    性能:redis最高 可靠性:zookeeper最高

redis实现分布式

setnx命令可以上锁,del可以释放锁。一直不释放锁,其他线程无法访问,建议设置过期时间。
为了保证解锁操作的原子性,可以使用用LUA脚本完成这一操作。先判断当前锁的字符串是否与传入的值相等,是的话就删除Key,解锁成功。

  1. // 上锁的同时设置过期时间 nx上锁,ex设置过期时间
  2. set k1 v1 nx ex 20
  3. // 释放锁
  4. del k1

image.png
为什么setnx可以实现分布式锁?

  • 首先redis是单线程的,同时只会有一个线程执行写操作。
  • 假设AB两个线程同时获取c资源。规定获取资源前首先向redis中插入一条数据key1。
  • A执行setnx命令插入key1,成功返回1后,获取资源执行操作。
  • B获取资源时也要先执行setnx插入key1,此时,key1已经被A线程插入了,B插入数据返回0,获取资源失败后,一直不停重复请求。
  • A执行完其余操作后,执行del命令,释放锁。
  • B重复的过程中,setnx成功返回1。此时B获取了锁。

死锁

原因:A在加锁完成后,宕机了,没能执行del操作,锁无法释放,其他线程也无法获取。
解决办法:设置过期时间

锁失效

原因:A在获取锁之后,执行后续操作的时长超过了过期时间,导致自动释放锁,B线程获取到锁。此时A在执行完后,执行del释放锁会造成当前使用者B加的锁释放,造成锁失效。
解决办法:释放锁前先确认锁是否属于自己。使用Redisson客户端工具。在锁过期之前重新修改过期时间。

新功能

ACL

权限控制
操作命令

  1. 【acl list】查看所有用户信息;
  2. 【acl cat】查看所有命令类别;
  3. 【acl setuser name】添加用户
  4. 【acl whoami】查看当前用户
  5. 【acl set name on >password ~cached:* +get】添加用户并设置密码权限等
    1. 【on】可用状态
    2. 【>password】设置密码
    3. 【~cached:】所有对“cached:”开头的key操作;【~ &】,表示对所有key直接使用该命令
    4. 【+get】只能执行get命令。【+@all】,表示可以使用任何命令

IO多线程

redis执行命令依然是单线程。
IO多线程是指客户端交互部分的网络IO处理模块。
多线程IO默认不开启。【io-threads-do-read yes/no】【io-threads num】

支持cluster

将redis-trib.rb功能继承到redis-cli。
官方redis-benchmark支持集群模式,通过多线程方式对多个分片(slots)压测。

其他

  • 新协议:RESP3
  • 客户端缓存,减少tcp交互
  • 集群代理模式proxy
  • api更新