> [TOC] # Nosql概述 *** ## 为什么要使用Nosql ### 1、单机MySQL的年代 90年代,一个网站的访问量一般不会太大,单个数据库完全够用。随着用户增多,网站出现以下问题 1. 数据量增加到一定程度,单机数据库就放不下了 2. 数据的索引(B+ Tree),一个机器内存也存放不下 3. 访问量变大后(读写混合),一台服务器承受不住。 ![image-20220517095951464](assets/image-20220517095951464.png) ### 2、Memcached(缓存) + MySQL + 垂直拆分 (读写分离) 网站80%的情况都是在读,每次都要去查询数据库的话就十分的麻烦!所以说我们希望减轻数据的压 力,我们可以使用缓存来保证效率! 发展过程: 优化数据结构和索引--> 文件缓存(IO)---> Memcached(当时最热门的技术!) ![image-20220517100347511](assets/image-20220517100347511.png) ### 3、分库分表 + 水平拆分 + MySQL集群 技术和业务在发展的同时,对人的要求也越来越高! 本质:数据库(读,写) 早些年MyISAM: 表锁,十分影响效率!高并发下就会出现严重的锁问题 转战Innodb:行锁 慢慢的就开始使用分库分表来解决写的压力! MySQL 在哪个年代推出 了表分区!这个并没有多少公司 使用! MySQL 的 集群,很好满足哪个年代的所有需求! ![image-20220517101228747](assets/image-20220517101228747.png) ### 4、如今最近的年代 2010--2020 十年之间,世界已经发生了翻天覆地的变化;(定位,也是一种数据,音乐,热榜!) MySQL 等关系型数据库就不够用了!数据量很多,变化很快~! MySQL 有的使用它来村粗一些比较大的文件,博客,图片!数据库表很大,效率就低了!如果有一种数 据库来专门处理这种数据, MySQL压力就变得十分小(研究如何处理这些问题!)大数据的IO压力下,表几乎没法更大! ![image-20220517101336924](assets/image-20220517101336924.png) ### 5、为什么要用NoSQL! 用户的个人信息,社交网络,地理位置。用户自己产生的数据,用户日志等等爆发式增长! 这时候我们就需要使用NoSQL数据库的,Nosql 可以很好的处理以上的情况 ## 什么是NoSQL NoSQL = Not Only SQL(不仅仅是SQL) Not Only Structured Query Language 关系型数据库:列+行,同一个表下数据的结构是一样的。 非关系型数据库:数据存储没有固定的格式,并且可以进行横向扩展。 NoSQL泛指非关系型数据库,随着web2.0互联网的诞生,传统的关系型数据库很难对付web2.0时代!尤其是超大规模的高并发的社区,暴露出来很多难以克服的问题,NoSQL在当今大数据环境下发展的十分迅速,Redis是发展最快的。 ### 1、Nosql特点 1. 方便扩展(数据之间没有关系,很好扩展!) 2. 大数据量高性能(Redis一秒可以写8万次,读11万次,NoSQL的缓存记录级,是一种细粒度的缓存,性能会比较高!) 3. 数据类型是多样型的!(不需要事先设计数据库,随取随用) 4. 传统的 RDBMS 和 NoSQL ```diff 传统的 RDBMS(关系型数据库) - 结构化组织 - SQL - 数据和关系都存在单独的表中 row col - 操作,数据定义语言 - 严格的一致性 - 基础的事务 ``` ```diff Nosql - 不仅仅是数据 - 没有固定的查询语言 - 键值对存储,列存储,文档存储,图形数据库(社交关系) - 最终一致性 - CAP定理和BASE - 高性能,高可用,高扩展 - .. ``` ### 2、了解:3V + 3高 大数据时代的3V :主要是描述问题的 1. 海量Velume 2. 多样Variety 3. 实时Velocity 大数据时代的3高 : 主要是对程序的要求 1. 高并发 2. 高可扩 3. 高性能 真正在公司中的实践:NoSQL + RDBMS 一起使用才是最强的。 ## Nosql的四大分类 > **KV键值对** - 新浪:Redis - 美团:Redis + Tair - 阿里、百度:Redis + Memcache > **文档型数据库(bson数据格式):** - MongoDB(掌握) - 基于分布式文件存储的数据库。C++编写,用于处理大量文档。 - MongoDB是RDBMS和NoSQL的中间产品。MongoDB是非关系型数据库中功能最丰富的,NoSQL中最像关系型数据库的数据库。 - ConthDB > **列存储数据库** - HBase(大数据必学) - 分布式文件系统 > **图关系数据库** 用于广告推荐,社交网络 - Neo4j、InfoGrid | 分类 | Examples举例 | 典型应用场景 | 数据模型 | 优点 | 缺点 | | ------------------- | -------------------------------------------------- | ------------------------------------------------------------ | ----------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | 键值对(key-value) | Tokyo Cabinet/Tyrant, Redis, Voldemort, Oracle BDB | 内容缓存,主要用于处理大量数据的高访问负载,也用于一些日志系统等等。 | Key 指向 Value 的键值对,通常用hash table来实现 | 查找速度快 | 数据无结构化,通常只被当作字符串或者二进制数据 | | 列存储数据库 | Cassandra, HBase, Riak | 分布式的文件系统 | 以列簇式存储,将同一列数据存在一起 | 查找速度快,可扩展性强,更容易进行分布式扩展 | 功能相对局限 | | 文档型数据库 | CouchDB, MongoDb | Web应用(与Key-Value类似,Value是结构化的,不同的是数据库能够了解Value的内容) | Key-Value对应的键值对,Value为结构化数据 | 数据结构要求不严格,表结构可变,不需要像关系型数据库一样需要预先定义表结构 | 查询性能不高,而且缺乏统一的查询语法。 | | 图形(Graph)数据库 | Neo4J, InfoGrid, Infinite Graph | 社交网络,推荐系统等。专注于构建关系图谱 | 图结构 | 利用图结构相关算法。比如最短路径寻址,N度关系查找等 | 很多时候需要对整个图做计算才能得出需要的信息,而且这种结构不太好做分布式的集群 | # Redis入门 ## 概述 > Redis是什么? Redis(Remote Dictionary Server ),即远程字典服务。 是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。 与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。 > Redis能干什么? 1. 内存存储、持久化,内存是断电即失的,所以需要持久化(RDB、AOF) 2. 高效率、用于高速缓冲 3. 发布订阅系统 4. 地图信息分析 5. 计时器、计数器(eg:浏览量) 6. 。。。 > 特性 1. 多样的数据类型 2. 持久化 3. 集群 4. 事务 ## Linux安装 1. 基本安装 ```shell # 1.下载安装包! redis-7.0.0.tar.gz # 2.解压Redis的安装包! tar -zxvf redis-7.0.0.tar.gz yum install gcc-c++ -y # 然后进入redis目录下执行 make && make install ``` 2. redis默认安装路径 `/usr/local/bin` 3. 将redis的配置文件复制到 程序安装目录 `/usr/local/bin/redis-config`下 4. redis默认不是后台启动的,需要修改配置文件! ![image-20220517111032308](assets/image-20220517111032308.png) 5. 通过制定的配置文件启动redis服务 ```shell redis-server redis-config/redis.conf ``` 6. 关闭Redis服务 `shutdown` ## 基础知识 redis默认有16个数据库 ```shell 默认使用的第0个; 16个数据库为:DB 0~DB 15 默认使用DB 0 ,可以使用select n切换到DB n,dbsize可以查看当前数据库的大小,与key数量相关。 ``` `keys *` :查看当前数据库中所有的key。 `flushdb`:清空当前数据库中的键值对。 `flushall`:清空所有数据库的键值对。 > Redis是单线程的,Redis是基于内存操作的。 所以Redis的性能瓶颈不是CPU,而是机器内存和网络带宽。 那么为什么Redis的速度如此快呢,性能这么高呢?QPS达到10W+ > Redis为什么单线程还这么快? - 误区1:高性能的服务器一定是多线程的? - 误区2:多线程(CPU上下文会切换!)一定比单线程效率高! 核心:Redis是将所有的数据放在内存中的,所以说使用单线程去操作效率就是最高的,多线程(CPU上下文会切换:耗时的操作!),对于内存系统来说,如果没有上下文切换效率就是最高的,多次读写都是在一个CPU上的,在内存存储数据情况下,单线程就是最佳的方案。 # 五大数据类型 *** Redis是一个开源(BSD许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持[字符串](https://www.redis.net.cn/tutorial/3508.html)、[哈希表](https://www.redis.net.cn/tutorial/3509.html)、[列表](https://www.redis.net.cn/tutorial/3510.html)、[集合](https://www.redis.net.cn/tutorial/3511.html)、[有序集合](https://www.redis.net.cn/tutorial/3512.html),[位图](https://www.redis.net.cn/tutorial/3508.html),[hyperloglogs](https://www.redis.net.cn/tutorial/3513.html)等数据类型。内置复制、[Lua脚本](https://www.redis.net.cn/tutorial/3516.html)、LRU收回、[事务](https://www.redis.net.cn/tutorial/3515.html)以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动[分区](https://www.redis.net.cn/tutorial/3524.html)。 ## Redis-key > 在redis中无论什么数据类型,在数据库中都是以key-value形式保存,通过进行对Redis-key的操作,来完成对数据库中数据的操作。 下面学习的命令: - `exists key`:判断键是否存在 - `del key`:删除键值对 - `move key db`:将键值对移动到指定数据库 - `expire key second`:设置键值对的过期时间 - `type key`:查看value的数据类型 - `expire key time`: 设置键值对的过期时间 - `ttl key`:查看key的过期剩余时间 关于`TTL`命令 Redis的key,通过TTL命令返回key的过期时间,一般来说有3种: 1. 当前key没有设置过期时间,所以会返回-1. 2. 当前key有设置过期时间,而且key已经过期,所以会返回-2. 3. 当前key有设置过期时间,且key还没有过期,故会返回key的正常剩余时间. 关于重命名`RENAME`和`RENAMENX` - `RENAME key newkey`修改 key 的名称 - `RENAMENX key newkey`仅当 newkey 不存在时,将 key 改名为 newkey 。 [更多命令学习](https://www.redis.net.cn/order) ## String(字符串) 普通的set、get直接略过。 | 命令 | 描述 | 示例 | | ------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | | `APPEND key value` | 向指定的key的value后追加字符串 | 127.0.0.1:6379> set msg hello OK127.0.0.1:6379> append msg " world" (integer) 11 127.0.0.1:6379> get msg “hello world” | | `DECR/INCR key` | 将指定key的value数值进行+1/-1(仅对于数字) | 127.0.0.1:6379> set age 20 OK 127.0.0.1:6379> incr age (integer) 21 127.0.0.1:6379> decr age (integer) 20 | | `INCRBY/DECRBY key n` | 按指定的步长对数值进行加减 | 127.0.0.1:6379> INCRBY age 5 (integer) 25 127.0.0.1:6379> DECRBY age 10 (integer) 15 | | `INCRBYFLOAT key n` | 为数值加上浮点型数值 | 127.0.0.1:6379> INCRBYFLOAT age 5.2 “20.2” | | `STRLEN key` | 获取key保存值的字符串长度 | 127.0.0.1:6379> get msg “hello world” 127.0.0.1:6379> STRLEN msg (integer) 11 | | `GETRANGE key start end` | 按起止位置获取字符串(闭区间,起止位置都取) | 127.0.0.1:6379> get msg “hello world” 127.0.0.1:6379> GETRANGE msg 3 9 “lo worl” | | `SETRANGE key offset value` | 用指定的value 替换key中 offset开始的值 | 127.0.0.1:6379> SETRANGE msg 2 hello (integer) 7 127.0.0.1:6379> get msg “tehello” | | `GETSET key value` | 将给定 key 的值设为 value ,并返回 key 的旧值(old value)。 | 127.0.0.1:6379> GETSET msg test “hello world” | | `SETNX key value` | 仅当key不存在时进行set | 127.0.0.1:6379> SETNX msg test (integer) 0 127.0.0.1:6379> SETNX name sakura (integer) 1 | | `SETEX key seconds value` | set 键值对并设置过期时间 | 127.0.0.1:6379> setex name 10 root OK 127.0.0.1:6379> get name (nil) | | `MSET key1 value1 [key2 value2..]` | 批量set键值对,原子操作 | 127.0.0.1:6379> MSET k1 v1 k2 v2 k3 v3 OK | | `MSETNX key1 value1 [key2 value2..]` | 批量设置键值对,仅当参数中所有的key都不存在时执行 | 127.0.0.1:6379> MSETNX k1 v1 k4 v4 (integer) 0 | | `MGET key1 [key2..]` | 批量获取多个key保存的值 | 127.0.0.1:6379> MGET k1 k2 k3 1) “v1” 2) “v2” 3) “v3” | | `PSETEX key milliseconds value` | 和 SETEX 命令相似,但它以毫秒为单位设置 key 的生存时间, | | | `getset key value` | 如果不存在值,则返回nil,如果存在值,获取原来的值,并设置新的值 | | String类似的使用场景:value除了是字符串还可以是数字,用途举例: - 计数器 - 统计多单位的数量:uid:123666:follow 0 - 粉丝数 - 对象存储缓存 ## List(列表) > Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边) > > 一个列表最多可以包含 2^32 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。 ![image-20220517145312078](assets/image-20220517145312078.png) 首先我们列表,可以经过规则定义将其变为队列、栈、双端队列等 正如图Redis中List是可以进行双端操作的,所以命令也就分为了LXXX和RLLL两类,有时候L也表示List例如LLEN | 命令 | 描述 | | --------------------------------------- | ------------------------------------------------------------ | | `LPUSH/RPUSH key value1[value2..]` | 从左边/右边向列表中PUSH值(一个或者多个)。 | | `LRANGE key start end` | 获取list 起止元素==(索引从左往右 递增)== | | `LPUSHX/RPUSHX key value` | 向已存在的列名中push值(一个或者多个) | | `LINSERT key BEFORE|AFTER pivot value` | 在指定列表元素的前/后 插入value | | `LLEN key` | 查看列表长度 | | `LINDEX key index` | 通过索引获取列表元素 | | `LSET key index value` | 通过索引为元素设值 | | `LPOP/RPOP key` | 从最左边/最右边移除值 并返回 | | `RPOPLPUSH source destination` | 将列表的尾部(右)最后一个值弹出,并返回,然后加到另一个列表的头部 | | `LTRIM key start end` | 通过下标截取指定范围内的列表 | | `LREM key count value` | List中是允许value重复的 `count > 0`:从头部开始搜索 然后删除指定的value 至多删除count个 `count < 0`:从尾部开始搜索… `count = 0`:删除列表中所有的指定value。 | | `BLPOP/BRPOP key1[key2] timout` | 移出并获取列表的第一个/最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 | | `BRPOPLPUSH source destination timeout` | 和`RPOPLPUSH`功能相同,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 | ```shell 127.0.0.1:6379> LPUSH mylist k1 # LPUSH mylist=>{1} (integer) 1 127.0.0.1:6379> LPUSH mylist k2 # LPUSH mylist=>{2,1} (integer) 2 127.0.0.1:6379> RPUSH mylist k3 # RPUSH mylist=>{2,1,3} (integer) 3 127.0.0.1:6379> get mylist # 普通的get是无法获取list值的 (error) WRONGTYPE Operation against a key holding the wrong kind of value 127.0.0.1:6379> LRANGE mylist 0 4 # LRANGE 获取起止位置范围内的元素 1) "k2" 2) "k1" 3) "k3" 127.0.0.1:6379> LRANGE mylist 0 2 1) "k2" 2) "k1" 3) "k3" 127.0.0.1:6379> LRANGE mylist 0 1 1) "k2" 2) "k1" 127.0.0.1:6379> LRANGE mylist 0 -1 # 获取全部元素 1) "k2" 2) "k1" 3) "k3" ---------------------------LPUSHX---RPUSHX----------------------------------- 127.0.0.1:6379> LPUSHX list v1 # list不存在 LPUSHX失败 (integer) 0 127.0.0.1:6379> LPUSHX list v1 v2 (integer) 0 127.0.0.1:6379> LPUSHX mylist k4 k5 # 向mylist中 左边 PUSH k4 k5 (integer) 5 127.0.0.1:6379> LRANGE mylist 0 -1 1) "k5" 2) "k4" 3) "k2" 4) "k1" 5) "k3" ---------------------------LINSERT--LLEN--LINDEX--LSET---------------------------- 127.0.0.1:6379> LINSERT mylist after k2 ins_key1 # 在k2元素后 插入ins_key1 (integer) 6 127.0.0.1:6379> LRANGE mylist 0 -1 1) "k5" 2) "k4" 3) "k2" 4) "ins_key1" 5) "k1" 6) "k3" 127.0.0.1:6379> LLEN mylist # 查看mylist的长度 (integer) 6 127.0.0.1:6379> LINDEX mylist 3 # 获取下标为3的元素 "ins_key1" 127.0.0.1:6379> LINDEX mylist 0 "k5" 127.0.0.1:6379> LSET mylist 3 k6 # 将下标3的元素 set值为k6 OK 127.0.0.1:6379> LRANGE mylist 0 -1 1) "k5" 2) "k4" 3) "k2" 4) "k6" 5) "k1" 6) "k3" ---------------------------LPOP--RPOP-------------------------- 127.0.0.1:6379> LPOP mylist # 左侧(头部)弹出 "k5" 127.0.0.1:6379> RPOP mylist # 右侧(尾部)弹出 "k3" ---------------------------RPOPLPUSH-------------------------- 127.0.0.1:6379> LRANGE mylist 0 -1 1) "k4" 2) "k2" 3) "k6" 4) "k1" 127.0.0.1:6379> RPOPLPUSH mylist newlist # 将mylist的最后一个值(k1)弹出,加入到newlist的头部 "k1" 127.0.0.1:6379> LRANGE newlist 0 -1 1) "k1" 127.0.0.1:6379> LRANGE mylist 0 -1 1) "k4" 2) "k2" 3) "k6" ---------------------------LTRIM-------------------------- 127.0.0.1:6379> LTRIM mylist 0 1 # 截取mylist中的 0~1部分 OK 127.0.0.1:6379> LRANGE mylist 0 -1 1) "k4" 2) "k2" # 初始 mylist: k2,k2,k2,k2,k2,k2,k4,k2,k2,k2,k2 ---------------------------LREM-------------------------- 127.0.0.1:6379> LREM mylist 3 k2 # 从头部开始搜索 至多删除3个 k2 (integer) 3 # 删除后:mylist: k2,k2,k2,k4,k2,k2,k2,k2 127.0.0.1:6379> LREM mylist -2 k2 #从尾部开始搜索 至多删除2个 k2 (integer) 2 # 删除后:mylist: k2,k2,k2,k4,k2,k2 ---------------------------BLPOP--BRPOP-------------------------- mylist: k2,k2,k2,k4,k2,k2 newlist: k1 127.0.0.1:6379> BLPOP newlist mylist 30 # 从newlist中弹出第一个值,mylist作为候选 1) "newlist" # 弹出 2) "k1" 127.0.0.1:6379> BLPOP newlist mylist 30 1) "mylist" # 由于newlist空了 从mylist中弹出 2) "k2" 127.0.0.1:6379> BLPOP newlist 30 (30.10s) # 超时了 127.0.0.1:6379> BLPOP newlist 30 # 我们连接另一个客户端向newlist中push了test, 阻塞被解决。 1) "newlist" 2) "test" (12.54s) ``` > 小结 - list实际上是一个链表,before Node after , left, right 都可以插入值 - 如果key不存在,则创建新的链表 - 如果key存在,新增内容 - 如果移除了所有值,空链表,也代表不存在 - 在两边插入或者改动值,效率最高!修改中间元素,效率相对较低 应用: 消息排队!消息队列(Lpush Rpop),栈(Lpush Lpop) ## Set(集合) > Redis的Set是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。 > > Redis 中 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 > > 集合中最大的成员数为 2^32 - 1 (4294967295, 每个集合可存储40多亿个成员)。 | 命令 | 描述 | | ----------------------------------------- | ------------------------------------------------------------ | | `SADD key member1[member2..]` | 向集合中无序增加一个/多个成员 | | `SCARD key` | 获取集合的成员数 | | `SMEMBERS key` | 返回集合中所有的成员 | | `SISMEMBER key member` | 查询member元素是否是集合的成员,结果是无序的 | | `SRANDMEMBER key [count]` | 随机返回集合中count个成员,count缺省值为1 | | `SPOP key [count]` | 随机移除并返回集合中count个成员,count缺省值为1 | | `SMOVE source destination member` | 将source集合的成员member移动到destination集合 | | `SREM key member1[member2..]` | 移除集合中一个/多个成员 | | `SDIFF key1[key2..]` | 返回所有集合的差集 key1- key2 - … | | `SDIFFSTORE destination key1[key2..]` | 在SDIFF的基础上,将结果保存到集合中==(覆盖)==。不能保存到其他类型key噢! | | `SINTER key1 [key2..]` | 返回所有集合的交集 | | `SINTERSTORE destination key1[key2..]` | 在SINTER的基础上,存储结果到集合中。覆盖 | | `SUNION key1 [key2..]` | 返回所有集合的并集 | | `SUNIONSTORE destination key1 [key2..]` | 在SUNION的基础上,存储结果到及和张。覆盖 | | `SSCAN KEY [MATCH pattern] [COUNT count]` | 在大量数据环境下,使用此命令遍历集合中元素,每次遍历部分 | ```shell ---------------SADD--SCARD--SMEMBERS--SISMEMBER-------------------- 127.0.0.1:6379> SADD myset m1 m2 m3 m4 # 向myset中增加成员 m1~m4 (integer) 4 127.0.0.1:6379> SCARD myset # 获取集合的成员数目 (integer) 4 127.0.0.1:6379> smembers myset # 获取集合中所有成员 1) "m4" 2) "m3" 3) "m2" 4) "m1" 127.0.0.1:6379> SISMEMBER myset m5 # 查询m5是否是myset的成员 (integer) 0 # 不是,返回0 127.0.0.1:6379> SISMEMBER myset m2 (integer) 1 # 是,返回1 127.0.0.1:6379> SISMEMBER myset m3 (integer) 1 ---------------------SRANDMEMBER--SPOP---------------------------------- 127.0.0.1:6379> SRANDMEMBER myset 3 # 随机返回3个成员 1) "m2" 2) "m3" 3) "m4" 127.0.0.1:6379> SRANDMEMBER myset # 随机返回1个成员 "m3" 127.0.0.1:6379> SPOP myset 2 # 随机移除并返回2个成员 1) "m1" 2) "m4" # 将set还原到{m1,m2,m3,m4} ---------------------SMOVE--SREM---------------------------------------- 127.0.0.1:6379> SMOVE myset newset m3 # 将myset中m3成员移动到newset集合 (integer) 1 127.0.0.1:6379> SMEMBERS myset 1) "m4" 2) "m2" 3) "m1" 127.0.0.1:6379> SMEMBERS newset 1) "m3" 127.0.0.1:6379> SREM newset m3 # 从newset中移除m3元素 (integer) 1 127.0.0.1:6379> SMEMBERS newset (empty list or set) # 下面开始是多集合操作,多集合操作中若只有一个参数默认和自身进行运算 # setx=>{m1,m2,m4,m6}, sety=>{m2,m5,m6}, setz=>{m1,m3,m6} -----------------------------SDIFF------------------------------------ 127.0.0.1:6379> SDIFF setx sety setz # 等价于setx-sety-setz 1) "m4" 127.0.0.1:6379> SDIFF setx sety # setx - sety 1) "m4" 2) "m1" 127.0.0.1:6379> SDIFF sety setx # sety - setx 1) "m5" -------------------------SINTER--------------------------------------- # 共同关注(交集) 127.0.0.1:6379> SINTER setx sety setz # 求 setx、sety、setx的交集 1) "m6" 127.0.0.1:6379> SINTER setx sety # 求setx sety的交集 1) "m2" 2) "m6" -------------------------SUNION--------------------------------------- 127.0.0.1:6379> SUNION setx sety setz # setx sety setz的并集 1) "m4" 2) "m6" 3) "m3" 4) "m2" 5) "m1" 6) "m5" 127.0.0.1:6379> SUNION setx sety # setx sety 并集 1) "m4" 2) "m6" 3) "m2" 4) "m1" 5) "m5" ``` 微博,将A用户所有关注的人放在一个集合中,将他的粉丝也放在一个集合中! 共同关注,共同爱好 ## Hash(哈希) > Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。 > > Set就是一种简化的Hash,只变动key,而value使用默认值填充。可以将一个Hash表作为一个对象进行存储,表中存放对象的信息。 > > 存储 key - map | 命令 | 描述 | | ------------------------------------------------ | ------------------------------------------------------------ | | `HSET key field value` | 将哈希表 key 中的字段 field 的值设为 value 。重复设置同一个field会覆盖,返回0 | | `HMSET key field1 value1 [field2 value2..]` | 同时将多个 field-value (域-值)对设置到哈希表 key 中。 | | `HSETNX key field value` | 只有在字段 field 不存在时,设置哈希表字段的值。 | | `HEXISTS key field` | 查看哈希表 key 中,指定的字段是否存在。 | | `HGET key field value` | 获取存储在哈希表中指定字段的值 | | `HMGET key field1 [field2..]` | 获取所有给定字段的值 | | `HGETALL key` | 获取在哈希表key 的所有字段和值 | | `HKEYS key` | 获取哈希表key中所有的字段 | | `HLEN key` | 获取哈希表中字段的数量 | | `HVALS key` | 获取哈希表中所有值 | | `HDEL key field1 [field2..]` | 删除哈希表key中一个/多个field字段 | | `HINCRBY key field n` | 为哈希表 key 中的指定字段的整数值加上增量n,并返回增量后结果 一样只适用于整数型字段 | | `HINCRBYFLOAT key field n` | 为哈希表 key 中的指定字段的浮点数值加上增量 n。 | | `HSCAN key cursor [MATCH pattern] [COUNT count]` | 迭代哈希表中的键值对。 | ```shell ------------------------HSET--HMSET--HSETNX---------------- 127.0.0.1:6379> HSET studentx name sakura # 将studentx哈希表作为一个对象,设置name为sakura (integer) 1 127.0.0.1:6379> HSET studentx name gyc # 重复设置field进行覆盖,并返回0 (integer) 0 127.0.0.1:6379> HSET studentx age 20 # 设置studentx的age为20 (integer) 1 127.0.0.1:6379> HMSET studentx sex 1 tel 15623667886 # 设置sex为1,tel为15623667886 OK 127.0.0.1:6379> HSETNX studentx name gyc # HSETNX 设置已存在的field (integer) 0 # 失败 127.0.0.1:6379> HSETNX studentx email 12345@qq.com (integer) 1 # 成功 ----------------------HEXISTS-------------------------------- 127.0.0.1:6379> HEXISTS studentx name # name字段在studentx中是否存在 (integer) 1 # 存在 127.0.0.1:6379> HEXISTS studentx addr (integer) 0 # 不存在 -------------------HGET--HMGET--HGETALL----------- 127.0.0.1:6379> HGET studentx name # 获取studentx中name字段的value "gyc" 127.0.0.1:6379> HMGET studentx name age tel # 获取studentx中name、age、tel字段的value 1) "gyc" 2) "20" 3) "15623667886" 127.0.0.1:6379> HGETALL studentx # 获取studentx中所有的field及其value 1) "name" 2) "gyc" 3) "age" 4) "20" 5) "sex" 6) "1" 7) "tel" 8) "15623667886" 9) "email" 10) "12345@qq.com" --------------------HKEYS--HLEN--HVALS-------------- 127.0.0.1:6379> HKEYS studentx # 查看studentx中所有的field 1) "name" 2) "age" 3) "sex" 4) "tel" 5) "email" 127.0.0.1:6379> HLEN studentx # 查看studentx中的字段数量 (integer) 5 127.0.0.1:6379> HVALS studentx # 查看studentx中所有的value 1) "gyc" 2) "20" 3) "1" 4) "15623667886" 5) "12345@qq.com" -------------------------HDEL-------------------------- 127.0.0.1:6379> HDEL studentx sex tel # 删除studentx 中的sex、tel字段 (integer) 2 127.0.0.1:6379> HKEYS studentx 1) "name" 2) "age" 3) "email" -------------HINCRBY--HINCRBYFLOAT------------------------ 127.0.0.1:6379> HINCRBY studentx age 1 # studentx的age字段数值+1 (integer) 21 127.0.0.1:6379> HINCRBY studentx name 1 # 非整数字型字段不可用 (error) ERR hash value is not an integer 127.0.0.1:6379> HINCRBYFLOAT studentx weight 0.6 # weight字段增加0.6 "90.8" ``` Hash变更的数据user name age,尤其是用户信息之类的,经常变动的信息!Hash更适合于对象的存储,Sring更加适合字符串存储! ## Zset(有序集合) > 不同的是每个元素都会关联一个double类型的分数(score)。redis正是通过分数来为集合中的成员进行从小到大的排序。 > > score相同:按字典顺序排序 > > 有序集合的成员是唯一的,但分数(score)却可以重复。 > > zset k1 source1 v1 | 命令 | 描述 | | ------------------------------------------------- | ------------------------------------------------------------ | | `ZADD key score member1 [score2 member2]` | 向有序集合添加一个或多个成员,或者更新已存在成员的分数 | | `ZCARD key` | 获取有序集合的成员数 | | `ZCOUNT key min max` | 计算在有序集合中指定区间score的成员数 | | `ZINCRBY key n member` | 有序集合中对指定成员的分数加上增量 n | | `ZSCORE key member` | 返回有序集中,成员的分数值 | | `ZRANK key member` | 返回有序集合中指定成员的索引 | | `ZRANGE key start end` | 通过索引区间返回有序集合成指定区间内的成员 | | `ZRANGEBYLEX key min max` | 通过字典区间返回有序集合的成员 | | `ZRANGEBYSCORE key min max` | 通过分数返回有序集合指定区间内的成员==-inf 和 +inf分别表示最小最大值,只支持开区间()== | | `ZLEXCOUNT key min max` | 在有序集合中计算指定字典区间内成员数量 | | `ZREM key member1 [member2..]` | 移除有序集合中一个/多个成员 | | `ZREMRANGEBYLEX key min max` | 移除有序集合中给定的字典区间的所有成员 | | `ZREMRANGEBYRANK key start stop` | 移除有序集合中给定的排名区间的所有成员 | | `ZREMRANGEBYSCORE key min max` | 移除有序集合中给定的分数区间的所有成员 | | `ZREVRANGE key start end` | 返回有序集中指定区间内的成员,通过索引,分数从高到底 | | `ZREVRANGEBYSCORRE key max min` | 返回有序集中指定分数区间内的成员,分数从高到低排序 | | `ZREVRANGEBYLEX key max min` | 返回有序集中指定字典区间内的成员,按字典顺序倒序 | | `ZREVRANK key member` | 返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序 | | `ZINTERSTORE destination numkeys key1 [key2 ..]` | 计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中,numkeys:表示参与运算的集合数,将score相加作为结果的score | | `ZUNIONSTORE destination numkeys key1 [key2..]` | 计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中 | | `ZSCAN key cursor [MATCH pattern\] [COUNT count]` | 迭代有序集合中的元素(包括元素成员和元素分值) | ```shell -------------------ZADD--ZCARD--ZCOUNT-------------- 127.0.0.1:6379> ZADD myzset 1 m1 2 m2 3 m3 # 向有序集合myzset中添加成员m1 score=1 以及成员m2 score=2.. (integer) 2 127.0.0.1:6379> ZCARD myzset # 获取有序集合的成员数 (integer) 2 127.0.0.1:6379> ZCOUNT myzset 0 1 # 获取score在 [0,1]区间的成员数量 (integer) 1 127.0.0.1:6379> ZCOUNT myzset 0 2 (integer) 2 ----------------ZINCRBY--ZSCORE-------------------------- 127.0.0.1:6379> ZINCRBY myzset 5 m2 # 将成员m2的score +5 "7" 127.0.0.1:6379> ZSCORE myzset m1 # 获取成员m1的score "1" 127.0.0.1:6379> ZSCORE myzset m2 "7" --------------ZRANK--ZRANGE----------------------------------- 127.0.0.1:6379> ZRANK myzset m1 # 获取成员m1的索引,索引按照score排序,score相同索引值按字典顺序顺序增加 (integer) 0 127.0.0.1:6379> ZRANK myzset m2 (integer) 2 127.0.0.1:6379> ZRANGE myzset 0 1 # 获取索引在 0~1的成员 1) "m1" 2) "m3" 127.0.0.1:6379> ZRANGE myzset 0 -1 # 获取全部成员 1) "m1" 2) "m3" 3) "m2" #testset=>{abc,add,amaze,apple,back,java,redis} score均为0 ------------------ZRANGEBYLEX--------------------------------- 127.0.0.1:6379> ZRANGEBYLEX testset - + # 返回所有成员 1) "abc" 2) "add" 3) "amaze" 4) "apple" 5) "back" 6) "java" 7) "redis" 127.0.0.1:6379> ZRANGEBYLEX testset - + LIMIT 0 3 # 分页 按索引显示查询结果的 0,1,2条记录 1) "abc" 2) "add" 3) "amaze" 127.0.0.1:6379> ZRANGEBYLEX testset - + LIMIT 3 3 # 显示 3,4,5条记录 1) "apple" 2) "back" 3) "java" 127.0.0.1:6379> ZRANGEBYLEX testset (- [apple # 显示 (-,apple] 区间内的成员 1) "abc" 2) "add" 3) "amaze" 4) "apple" 127.0.0.1:6379> ZRANGEBYLEX testset [apple [java # 显示 [apple,java]字典区间的成员 1) "apple" 2) "back" 3) "java" -----------------------ZRANGEBYSCORE--------------------- 127.0.0.1:6379> ZRANGEBYSCORE myzset 1 10 # 返回score在 [1,10]之间的的成员 1) "m1" 2) "m3" 3) "m2" 127.0.0.1:6379> ZRANGEBYSCORE myzset 1 5 1) "m1" 2) "m3" --------------------ZLEXCOUNT----------------------------- 127.0.0.1:6379> ZLEXCOUNT testset - + (integer) 7 127.0.0.1:6379> ZLEXCOUNT testset [apple [java (integer) 3 ------------------ZREM--ZREMRANGEBYLEX--ZREMRANGBYRANK--ZREMRANGEBYSCORE-------------------------------- 127.0.0.1:6379> ZREM testset abc # 移除成员abc (integer) 1 127.0.0.1:6379> ZREMRANGEBYLEX testset [apple [java # 移除字典区间[apple,java]中的所有成员 (integer) 3 127.0.0.1:6379> ZREMRANGEBYRANK testset 0 1 # 移除排名0~1的所有成员 (integer) 2 127.0.0.1:6379> ZREMRANGEBYSCORE myzset 0 3 # 移除score在 [0,3]的成员 (integer) 2 # testset=> {abc,add,apple,amaze,back,java,redis} score均为0 # myzset=> {(m1,1),(m2,2),(m3,3),(m4,4),(m7,7),(m9,9)} ----------------ZREVRANGE--ZREVRANGEBYSCORE--ZREVRANGEBYLEX----------- 127.0.0.1:6379> ZREVRANGE myzset 0 3 # 按score递减排序,然后按索引,返回结果的 0~3 1) "m9" 2) "m7" 3) "m4" 4) "m3" 127.0.0.1:6379> ZREVRANGE myzset 2 4 # 返回排序结果的 索引的2~4 1) "m4" 2) "m3" 3) "m2" 127.0.0.1:6379> ZREVRANGEBYSCORE myzset 6 2 # 按score递减顺序 返回集合中分数在[2,6]之间的成员 1) "m4" 2) "m3" 3) "m2" 127.0.0.1:6379> ZREVRANGEBYLEX testset [java (add # 按字典倒序 返回集合中(add,java]字典区间的成员 1) "java" 2) "back" 3) "apple" 4) "amaze" -------------------------ZREVRANK------------------------------ 127.0.0.1:6379> ZREVRANK myzset m7 # 按score递减顺序,返回成员m7索引 (integer) 1 127.0.0.1:6379> ZREVRANK myzset m2 (integer) 4 # mathscore=>{(xm,90),(xh,95),(xg,87)} 小明、小红、小刚的数学成绩 # enscore=>{(xm,70),(xh,93),(xg,90)} 小明、小红、小刚的英语成绩 -------------------ZINTERSTORE--ZUNIONSTORE----------------------------------- 127.0.0.1:6379> ZINTERSTORE sumscore 2 mathscore enscore # 将mathscore enscore进行合并 结果存放到sumscore (integer) 3 127.0.0.1:6379> ZRANGE sumscore 0 -1 withscores # 合并后的score是之前集合中所有score的和 1) "xm" 2) "160" 3) "xg" 4) "177" 5) "xh" 6) "188" 127.0.0.1:6379> ZUNIONSTORE lowestscore 2 mathscore enscore AGGREGATE MIN # 取两个集合的成员score最小值作为结果的 (integer) 3 127.0.0.1:6379> ZRANGE lowestscore 0 -1 withscores 1) "xm" 2) "70" 3) "xg" 4) "87" 5) "xh" 6) "93" ``` 应用案例: - set排序 存储班级成绩表 工资表排序! - 普通消息,1.重要消息 2.带权重进行判断 - 排行榜应用实现,取Top N测试 # 三种特殊数据类型 *** ## Geospatial(地理位置) > 使用经纬度定位地理坐标并用一个有序集合zset保存,所以zset命令也可以使用 | 命令 | 描述 | | ------------------------------------------------------------ | ------------------------------------------------------------ | | `geoadd key longitud(经度) latitude(纬度) member [..]` | 将具体经纬度的坐标存入一个有序集合 | | `geopos key member [member..]` | 获取集合中的一个/多个成员坐标 | | `geodist key member1 member2 [unit]` | 返回两个给定位置之间的距离。默认以米作为单位。 | | `georadius key longitude latitude radius m|km|mi|ft [WITHCOORD][WITHDIST] [WITHHASH] [COUNT count]` | 以给定的经纬度为中心, 返回集合包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。 | | `GEORADIUSBYMEMBER key member radius...` | 功能与GEORADIUS相同,只是中心位置不是具体的经纬度,而是使用结合中已有的成员作为中心点。 | | `geohash key member1 [member2..]` | 返回一个或多个位置元素的Geohash表示。使用Geohash位置52点整数编码。 | 有效经纬度 > - 有效的经度从-180度到180度。 > - 有效的纬度从-85.05112878度到85.05112878度。 指定单位的参数 unit 必须是以下单位的其中一个: - m 表示单位为米。 - km 表示单位为千米。 - mi 表示单位为英里。 - ft 表示单位为英尺。 关于GEORADIUS的参数 > 通过`georadius`就可以完成 附近的人功能 > > withcoord:带上坐标 > > withdist:带上距离,单位与半径单位相同 > > COUNT n : 只显示前n个(按距离递增排序) 朋友定位,附近的人,打车距离技术,这个功能可以推算地理位置信息,两地之间的距离,方圆几里的人! 可以查询一些测试数据 只有六个命令! > **geoadd** 添加数据 规则:两级无法添加,我们一般会下载城市数据,直接通过java程序一次性导入! 参数(纬度 经度 名称) 超过有效的经度纬度就会报错 ```shell 127.0.0.1:6379> geoadd china:city 116.40 39.90 beijin (integer) 1 127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai (integer) 1 127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqi (integer) 1 127.0.0.1:6379> geoadd china:city 114.05 22.52 shengzhen (integer) 1 127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou 118.96 34.26 xian (integer) 2 ``` > ### geopos 获取当前定位:一定是一个坐标值 ```shell 127.0.0.1:6379> geopos china:city beijin 1) 1) "116.39999896287918091" 2) "39.90000009167092543" 127.0.0.1:6379> geopos china:city beijin chongqi 1) 1) "116.39999896287918091" 2) "39.90000009167092543" 2) 1) "106.49999767541885376" 2) "29.52999957900659211" 127.0.0.1:6379> ``` > ### geodist 两人之间的距离 ```shell 127.0.0.1:6379> geodist china:city beijin shanghai "1067378.7564" #北京上海的==直线距离,默认单位为米== 127.0.0.1:6379> geodist china:city beijin shanghai km "1067.3788" ``` > ### georadius已给定的经纬度为中心,找某一半径内的元素 我附近的人?(获得所有附近的人的地址,定位!)通过半径来查询 ```shell 127.0.0.1:6379> georadius china:city 110 30 1000 km #以100经度30纬度为中心1000km为半径的圆内城市 1) "chongqi" 2) "shengzhen" 3) "hangzhou" 4) "xian" 127.0.0.1:6379> georadius china:city 110 30 500 km 1) "chongqi" 127.0.0.1:6379> georadius china:city 110 30 500 km withdist #显示到中心位置的距离 1) 1) "chongqi" 2) "341.9374" 127.0.0.1:6379> georadius china:city 110 30 500 km withcoord #显示他人的定位信息 1) 1) "chongqi" 2) 1) "106.49999767541885376" 2) "29.52999957900659211" 127.0.0.1:6379> georadius china:city 110 30 500 km withdist withcoord count 1 #筛选出指定数量的结果 1) 1) "chongqi" 2) "341.9374" 3) 1) "106.49999767541885376" 2) "29.52999957900659211" 127.0.0.1:6379> georadius china:city 110 30 500 km withdist withcoord count 2 1) 1) "chongqi" 2) "341.9374" 3) 1) "106.49999767541885376" 2) "29.52999957900659211" 127.0.0.1:6379> georadius china:city 110 30 500 km withdist withcoord count 3 1) 1) "chongqi" 2) "341.9374" 3) 1) "106.49999767541885376" 2) "29.52999957900659211" 127.0.0.1:6379> ``` > ### georadiusbymember 找出指定城市周围的位置 ```shell 127.0.0.1:6379> georadiusbymember china:city beijin 1000 km 1) "xian" 2) "beijin" 127.0.0.1:6379> ``` > ### geohash 该命令返回长度为11的字符串 ```shell 127.0.0.1:6379> geohash china:city beijin chongqi 1) "wx4fbxxfke0" #将二维的经纬度转换为一维的字符串,如果字符串月接近,那么距离则越近 2) "wm5xzrybty0" ``` > ### geo底层实现原理 **原理其实就是zset,我们可以使用zset命令操作geo!** ```shell 127.0.0.1:6379> zrange china:city 0 -1 ##查看元素 1) "chongqi" 2) "shengzhen" 3) "hangzhou" 4) "shanghai" 5) "xian" 6) "beijin" 127.0.0.1:6379> zrem china:city beijin (integer) 1 127.0.0.1:6379> zrange china:city 0 -1 1) "chongqi" 2) "shengzhen" 3) "hangzhou" 4) "shanghai" 5) "xian" 127.0.0.1:6379> ``` ## Hyperloglog(基数统计) > Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。 > > 花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。 > > 因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。 > > 其底层使用string数据类型 什么是基数? > 数据集中不重复的元素的个数。 应用场景: 网页的访问量(UV):一个用户多次访问,也只能算作一个人。 > 传统实现,存储用户的id,然后每次进行比较。当用户变多之后这种方式及其浪费空间,而我们的目的只是计数,Hyperloglog就能帮助我们利用最小的空间完成。 ```shell ----------PFADD--PFCOUNT--------------------- 127.0.0.1:6379> PFADD myelemx a b c d e f g h i j k # 添加元素 (integer) 1 127.0.0.1:6379> type myelemx # hyperloglog底层使用String string 127.0.0.1:6379> PFCOUNT myelemx # 估算myelemx的基数 (integer) 11 127.0.0.1:6379> PFADD myelemy i j k z m c b v p q s (integer) 1 127.0.0.1:6379> PFCOUNT myelemy (integer) 11 ----------------PFMERGE----------------------- 127.0.0.1:6379> PFMERGE myelemz myelemx myelemy # 合并myelemx和myelemy 成为myelemz OK 127.0.0.1:6379> PFCOUNT myelemz # 估算基数 (integer) 17 ``` 如果允许容错,那么一定可以使用Hyperloglog ! 如果不允许容错,就使用set或者自己的数据类型即可 ! ## BitMaps(位图) > 使用位存储,信息状态只有 0 和 1 > > Bitmap是一串连续的2进制数字(0或1),每一位所在的位置为偏移(offset),在bitmap上可执行AND,OR,XOR,NOT以及其它位操作。 应用场景 签到统计、状态统计 | 命令 | 描述 | | ------------------------------------- | ------------------------------------------------------------ | | `setbit key offset value` | 为指定key的offset位设置值 | | `getbit key offset` | 获取offset位的值 | | `bitcount key [start end]` | 统计字符串被设置为1的bit数,也可以指定统计范围按字节 | | `bitop operration destkey key[key..]` | 对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。 | | `BITPOS key bit [start] [end]` | 返回字符串里面第一个被设置为1或者0的bit位。start和end只能按字节,不能按位 | ```shell ------------setbit--getbit-------------- 127.0.0.1:6379> setbit sign 0 1 # 设置sign的第0位为 1 (integer) 0 127.0.0.1:6379> setbit sign 2 1 # 设置sign的第2位为 1 不设置默认 是0 (integer) 0 127.0.0.1:6379> setbit sign 3 1 (integer) 0 127.0.0.1:6379> setbit sign 5 1 (integer) 0 127.0.0.1:6379> type sign string 127.0.0.1:6379> getbit sign 2 # 获取第2位的数值 (integer) 1 127.0.0.1:6379> getbit sign 3 (integer) 1 127.0.0.1:6379> getbit sign 4 # 未设置默认是0 (integer) 0 -----------bitcount---------------------------- 127.0.0.1:6379> BITCOUNT sign # 统计sign中为1的位数 (integer) 4 ``` # 事务 Redis的单条命令是保证原子性的,但是redis事务不能保证原子性 > Redis事务本质:一组命令的集合。 > > ----------------- 队列 set set set 执行 ------------------- > > 事务中每条命令都会被序列化,执行过程中按顺序执行,不允许其他命令进行干扰。 > > - 一次性 > - 顺序性 > - 排他性 > > ------ > > 1. Redis事务没有隔离级别的概念 > 2. Redis单条命令是保证原子性的,但是事务不保证原子性! ## Redis事务操作过程 - 开启事务(`multi`) - 命令入队 - 执行事务(`exec`) 所以事务中的命令在加入时都没有被执行,直到提交时才会开始执行(Exec)一次性完成 ```shell 127.0.0.1:6379> multi # 开启事务 OK 127.0.0.1:6379> set k1 v1 # 命令入队 QUEUED 127.0.0.1:6379> set k2 v2 # .. QUEUED 127.0.0.1:6379> get k1 QUEUED 127.0.0.1:6379> set k3 v3 QUEUED 127.0.0.1:6379> keys * QUEUED 127.0.0.1:6379> exec # 事务执行 1) OK 2) OK 3) "v1" 4) OK 5) 1) "k3" 2) "k2" 3) "k1" ``` 取消事务(`discurd`) ```shell 127.0.0.1:6379> multi OK 127.0.0.1:6379> set k1 v1 QUEUED 127.0.0.1:6379> set k2 v2 QUEUED 127.0.0.1:6379> DISCARD # 放弃事务 OK 127.0.0.1:6379> EXEC (error) ERR EXEC without MULTI # 当前未开启事务 127.0.0.1:6379> get k1 # 被放弃事务中命令并未执行 (nil) ``` ## 事务错误 代码语法错误(编译时异常)所有的命令都不执行 ```shell 127.0.0.1:6379> multi OK 127.0.0.1:6379> set k1 v1 QUEUED 127.0.0.1:6379> set k2 v2 QUEUED 127.0.0.1:6379> error k1 # 这是一条语法错误命令 (error) ERR unknown command `error`, with args beginning with: `k1`, # 会报错但是不影响后续命令入队 127.0.0.1:6379> get k2 QUEUED 127.0.0.1:6379> EXEC (error) EXECABORT Transaction discarded because of previous errors. # 执行报错 127.0.0.1:6379> get k1 (nil) # 其他命令并没有被执行 ``` 代码逻辑错误 (运行时异常) **其他命令可以正常执行 ** >>> 所以不保证事务原子性 ```shell 127.0.0.1:6379> multi OK 127.0.0.1:6379> set k1 v1 QUEUED 127.0.0.1:6379> set k2 v2 QUEUED 127.0.0.1:6379> INCR k1 # 这条命令逻辑错误(对字符串进行增量) QUEUED 127.0.0.1:6379> get k2 QUEUED 127.0.0.1:6379> exec 1) OK 2) OK 3) (error) ERR value is not an integer or out of range # 运行时报错 4) "v2" # 其他命令正常执行 # 虽然中间有一条命令报错了,但是后面的指令依旧正常执行成功了,没有进行回滚。 # 所以说Redis单条指令保证原子性,但是Redis事务不能保证原子性。 ``` ## 监控 悲观锁: - 很悲观,认为什么时候都会出现问题,无论做什么都会加锁 乐观锁: - 很乐观,认为什么时候都不会出现问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过这个数据 - 获取version - 更新的时候比较version 使用`watch key`监控指定数据,相当于乐观锁加锁。 > 正常执行 ```shell 127.0.0.1:6379> set money 100 # 设置余额:100 OK 127.0.0.1:6379> set use 0 # 支出使用:0 OK 127.0.0.1:6379> watch money # 监视money (上锁) OK 127.0.0.1:6379> multi OK 127.0.0.1:6379> DECRBY money 20 QUEUED 127.0.0.1:6379> INCRBY use 20 QUEUED 127.0.0.1:6379> exec # 监视值没有被中途修改,事务正常执行 1) (integer) 80 2) (integer) 20 ``` > 测试多线程修改值,使用watch可以当做redis的乐观锁操作(相当于getversion) 我们启动另外一个客户端模拟插队线程。 线程1: ```shell 127.0.0.1:6379> watch money # money上锁 OK 127.0.0.1:6379> multi OK 127.0.0.1:6379> DECRBY money 20 QUEUED 127.0.0.1:6379> INCRBY use 20 QUEUED 127.0.0.1:6379> # 此时事务并没有执行 ``` 模拟线程插队,线程2: ```shell 127.0.0.1:6379> INCRBY money 500 # 修改了线程一中监视的money (integer) 600 ``` 回到线程1,执行事务: ```shell 127.0.0.1:6379> EXEC # 执行之前,另一个线程修改了我们的值,这个时候就会导致事务执行失败 (nil) # 没有结果,说明事务执行失败 127.0.0.1:6379> get money # 线程2 修改生效 "600" 127.0.0.1:6379> get use # 线程1事务执行失败,数值没有被修改 "0" ``` > 解锁获取最新值,然后再加锁进行事务。 > > `unwatch`进行解锁。 注意:每次提交执行exec后都会自动释放锁,不管是否成功 # Redis.conf ```shell # 单位, 对大小写不敏感 # 1k => 1000 bytes # 1kb => 1024 bytes # 1m => 1000000 bytes # 1mb => 1024*1024 bytes # 1g => 1000000000 bytes # 1gb => 1024*1024*1024 bytes # # units are case insensitive so 1GB 1Gb 1gB are all the same. ################################## INCLUDES ################################### # 可以包含其他配置文件 # include /path/to/local.conf # include /path/to/other.conf ################################## NETWORK ##################################### # 绑定的本地IP bind 127.0.0.1 # 是否是受保护模式 protected-mode yes # 端口号 port 6379 ################################# GENERAL ##################################### # 是否开启守护进程,即为后台运行 daemonize no # 管理守护进程 supervised no # 如果以后台方式允许,就需要执行Pid进程文件 pidfile /var/run/redis_6379.pid # 日志级别 # Specify the server verbosity level. # This can be one of: # debug (a lot of information, useful for development/testing) # verbose (many rarely useful info, but not a mess like the debug level) # notice (moderately verbose, what you want in production probably) 生成环境使用 # warning (only very important / critical messages are logged) loglevel notice # 日志的文件名 logfile "" # 默认的数据库数量,默认16个 databases 16 # 是否显示redis的logo always-show-logo yes ################################ 快照 ################################ # 持久化,在规定时间执行多少次操作,则会持久化到文件.rdb .aof # 没有持久化,数据断电即失去 # 900秒内,至少有一个key进行了修改,进行持久化操作 save 900 1 save 300 10 save 60 10000 # 之后会执行自己的 # 持久化出错,是否需要继续工作 stop-writes-on-bgsave-error yes # 是否压缩rdb文件,需要消耗CPU资源 rdbcompression yes # 是否校验rdb文件,如果出错,会自动继续修复 rdbchecksum yes # The filename where to dump the DB dbfilename dump.rdb # 持久化文件生成的目录 dir ./ ################################# 主从复制 ################################# # 配置主机的ip+端口 # replicaof # 配置主机的密码 # masterauth # 当 replica 与 master 失去连接或者主从复制在进行时,replica 可以有两种不同的设置: # yes(默认值),则 replica 仍将响应客户端请求,可能会有过期数据,或者如果这是第一次同步,则数据集可能为空。 # no , replica 将对所有请求命令(但不包含 INFO, replicaOF, AUTH, PING, SHUTDOWN, REPLCONF, ROLE, CONFIG, SUBSCRIBE, UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE, PUBLISH, PUBSUB, COMMAND, POST, HOST: and LATENCY)返回 SYNC with master in progress 的错误。 # 是否是只读模式 replica-read-only yes ################################## 安全 ################################### # 设置redis密码,默认是空值 # requirepass foobared ################################### 客户端限制 #################################### # 最大的客户端数量 # maxclients 10000 ############################## 内存设置 ################################ # 最大内存容量 # maxmemory # 内存达到上限的处理策略 1、volatile-lru:只对设置了过期时间的key进行LRU(默认值) 2、allkeys-lru : 删除lru算法的key 3、volatile-random:随机删除即将过期key 4、allkeys-random:随机删除 5、volatile-ttl : 删除即将过期的 6、noeviction : 永不过期,返回错误 # maxmemory-policy noeviction ############################## AOF的配置 ############################### # 默认不开启AOF,默认使用rdb持久化,大部分情况下rdb完全够用! appendonly no # 持久化的文件名字 appendfilename "appendonly.aof" # 同步策略 # 每次修改就会同步 # appendfsync always # 每秒会同步,有可能丢失1s数据 appendfsync everysec # 不执行sync,操作系统自己同步 # appendfsync no ``` # redis持久化 *** 面试和工作,持久化都是重点! Redis 是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中 的数据库状态也会消失。所以 Redis 提供了持久化功能! ## 持久化—RDB 在主从复制中,rdb就是备用了!从机上面 ![image-20220518101146275](assets/image-20220518101146275.png) 在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快 照文件直接读到内存里。 Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程 都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。 这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。我们默认的就是 RDB,一般情况下不需要修改这个配置! 有时候在生产环境我们会将这个文件进行备份! rdb保存的文件是dump.rdb都是在我们的配置文件中快照中进行配置的! ### 触发机制 1. save的规则满足的情况下,会自动触发rdb原则 2. 执行flushall命令,也会触发我们的rdb原则 3. 退出redis,也会自动产生rdb文件 ### 如何恢复rdb文件 1.只需要将rdb文件放在redis启动目录就可以,redis启动时就会自动检测dum.rdb文件,恢复其中数据 2.rdb文件存放的位置 ### 优缺点 **优点:** 1. 适合大规模的数据恢复 2. 对数据的完整性要求不高 **缺点:** 1. 需要一定的时间间隔进行操作,如果redis意外宕机了,这个最后一次修改的数据就没有了。 2. fork进程的时候,会占用一定的内容空间 ## 持久化—AOF 将我们的所有命令都记录下来,history,恢复的时候就把这个文件全部在执行一遍! ![image-20220518102710826](assets/image-20220518102710826.png) 以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件 但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件 的内容将写指令从前到后执行一次以完成数据的恢复工作 **Aof保存的是 appendonly.aof 文件** `appendonly no yes`则表示启用AOF 默认是不开启的,我们需要手动配置,然后重启redis,就可以生效了! 如果这个aof文件有错位,这时候redis是启动不起来的,我需要修改这个aof文件 redis给我们提供了一个工具`redis-check-aof --fix` > 优点和缺点 ```bash appendonly yes # 默认是不开启aof模式的,默认是使用rdb方式持久化的,在大部分的情况下,rdb完全够用 appendfilename "appendonly.aof" # appendfsync always # 每次修改都会sync 消耗性能 appendfsync everysec # 每秒执行一次 sync 可能会丢失这一秒的数据 # appendfsync no # 不执行 sync ,这时候操作系统自己同步数据,速度最快 ``` 优点 1. 每一次修改都会同步,文件的完整性会更加好 2. 每秒同步一次,可能会丢失一秒的数据 3. 从不同步,效率最高 缺点 1. 相对于数据文件来说,aof远远大于rdb,修复速度比rdb慢! 2. Aof运行效率也要比rdb慢,所以我们redis默认的配置就是rdb持久化 ## RDB和AOP选择 ### RDB 和 AOF 对比 | | RDB | AOF | | ---------- | ------ | ------------ | | 启动优先级 | 低 | 高 | | 体积 | 小 | 大 | | 恢复速度 | 快 | 慢 | | 数据安全性 | 丢数据 | 根据策略决定 | ### 如何选择使用哪种持久化方式? 一般来说, 如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。 如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。 有很多用户都只使用 AOF 持久化, 但并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快。 ![img](assets/1363376-20210622212732123-1297981656.png) # redis发布订阅 Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。 Redis 客户端可以订阅任意数量的频道。 订阅/发布消息图: 第一个:消息发送者, 第二个:频道 第三个:消息订阅者! ![image-20220518103759533](assets/image-20220518103759533.png) 下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系: ![在这里插入图片描述](assets/20200513215523258.png) 当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端: ![image-20220518103925266](assets/image-20220518103925266.png) ## 命令 | 命令 | 描述 | | ---------------------------------------- | ---------------------------------- | | `PSUBSCRIBE pattern [pattern..]` | 订阅一个或多个符合给定模式的频道。 | | `PUNSUBSCRIBE pattern [pattern..]` | 退订一个或多个符合给定模式的频道。 | | `PUBSUB subcommand [argument[argument]]` | 查看订阅与发布系统状态。 | | `PUBLISH channel message` | 向指定频道发布消息 | | `SUBSCRIBE channel [channel..]` | 订阅给定的一个或多个频道。 | | `SUBSCRIBE channel [channel..]` | 退订一个或多个频道 | ## 示例 ```shell ------------订阅端---------------------- 127.0.0.1:6379> SUBSCRIBE sakura # 订阅sakura频道 Reading messages... (press Ctrl-C to quit) # 等待接收消息 1) "subscribe" # 订阅成功的消息 2) "sakura" 3) (integer) 1 1) "message" # 接收到来自sakura频道的消息 "hello world" 2) "sakura" 3) "hello world" 1) "message" # 接收到来自sakura频道的消息 "hello i am sakura" 2) "sakura" 3) "hello i am sakura" --------------消息发布端------------------- 127.0.0.1:6379> PUBLISH sakura "hello world" # 发布消息到sakura频道 (integer) 1 127.0.0.1:6379> PUBLISH sakura "hello i am sakura" # 发布消息 (integer) 1 -----------------查看活跃的频道------------ 127.0.0.1:6379> PUBSUB channels 1) "sakura" ``` ## 原理 每个 Redis 服务器进程都维持着一个表示服务器状态的 redis.h/redisServer 结构, 结构的 pubsub_channels 属性是一个字典, 这个字典就用于保存订阅频道的信息,其中,字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端。 ![image-20220518104417240](assets/image-20220518104417240.png) 客户端订阅,就被链接到对应频道的链表的尾部,退订则就是将客户端节点从链表中移除。 ## 缺点 1. 如果一个客户端订阅了频道,但自己读取消息的速度却不够快的话,那么不断积压的消息会使redis输出缓冲区的体积变得越来越大,这可能使得redis本身的速度变慢,甚至直接崩溃。 2. 这和数据传输可靠性有关,如果在订阅方断线,那么他将会丢失所有在短线期间发布者发布的消息。 ## 应用 1. 消息订阅:公众号订阅,微博关注等等(起始更多是使用消息队列来进行实现) 2. 多人在线聊天室。 稍微复杂的场景,我们就会使用消息中间件MQ处理。 # Redis主从复制 ## 概念 主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(Master/Leader),后者称为从节点(Slave/Follower), 数据的复制是单向的!只能由主节点复制到从节点(主节点以写为主、从节点以读为主)。 默认情况下,每台Redis服务器都是主节点,一个主节点可以有0个或者多个从节点,但每个从节点只能由一个主节点。 ## 作用 1. **数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余的方式。** 2. **故障恢复:当主节点故障时,从节点可以暂时替代主节点提供服务,是一种服务冗余的方式** 3. **负载均衡:在主从复制的基础上,配合读写分离,由主节点进行写操作,从节点进行读操作,分担服务器的负载;尤其是在多读少写的场景下,通过多个从节点分担负载,提高并发量。** 4. **高可用基石:主从复制还是哨兵和集群能够实施的基础。** ## **为什么使用集群** 1. **单台服务器难以负载大量的请求** 2. **单台服务器故障率高,系统崩坏概率大** 3. **单台服务器内存容量有限。** ## 环境配置 我们在讲解配置文件的时候,注意到有一个`replication`模块 (见Redis.conf中第8条) 查看当前库的信息:`info replication` ```shell 127.0.0.1:6379> info replication # Replication role:master # 角色 connected_slaves:0 # 从机数量 master_replid:3b54deef5b7b7b7f7dd8acefa23be48879b4fcff master_replid2:0000000000000000000000000000000000000000 master_repl_offset:0 second_repl_offset:-1 repl_backlog_active:0 repl_backlog_size:1048576 repl_backlog_first_byte_offset:0 repl_backlog_histlen:0 ``` 既然需要启动多个服务,就需要多个配置文件。每个配置文件对应修改以下信息: - 端口号 - pid文件名 - 日志文件名 - rdb文件名 启动单机多服务集群: ## 一主二从配置 默认情况下,每台Redis服务器都是主节点;我们一般情况下只用配置从机就好了! 认老大!一主(79)二从(80,81) 使用`SLAVEOF host port`就可以为从机配置主机了。 有树状模型和普通的1对多模型 ![image-20220520153051951](assets/image-20220520153051951.png) ![image-20220520153115865](assets/image-20220520153115865.png) ## 使用规则 1. 从机只能读,不能写,主机可读可写但是多用于写。 ```shell 127.0.0.1:6381> set name sakura # 从机6381写入失败 (error) READONLY You can't write against a read only replica. 127.0.0.1:6380> set name sakura # 从机6380写入失败 (error) READONLY You can't write against a read only replica. 127.0.0.1:6379> set name sakura OK 127.0.0.1:6379> get name "sakura" ``` 2. 当主机断电宕机后,默认情况下从机的角色不会发生变化(此时从机不能进行读操作) ,集群中只是失去了写操作,当主机恢复以后,又会连接上从机恢复原状。 3. 当从机断电宕机后,若不是使用配置文件配置的从机,再次启动后作为主机是无法获取之前主机的数据的,若此时重新配置称为从机,又可以获取到主机的所有数据。这里就要提到一个同步原理。 4. 第二条中提到,默认情况下,主机故障后,不会出现新的主机,有两种方式可以产生新的主机: - 从机手动执行命令`slaveof no one`,这样执行以后从机会独立出来成为一个主机 - 使用哨兵模式(自动选举) > 如果没有老大了,这个时候能不能选择出来一个老大呢?手动! 如果主机断开了连接,我们可以使用`SLAVEOF no one`让自己变成主机!其他的节点就可以手动连接到最新的主节点(手动)!如果这个时候老大修复了,那么久重新连接! ## 复制原理 Slave 启动成功连接到 master 后会发送一个sync同步命令 Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行 完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。 全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。 增量复制:Master 继续将新的所有收集到的修改命令依次传给slave,完成同步 但是只要是重新连接master,一次完全同步(全量复制)将被自动执行! 我们的数据一定可以在从机中 看到! # 哨兵模式 (自动选举老大的模式) *** ## 概述 主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel(哨兵) 架构来解决这个问题。 谋朝篡位的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。 哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。 ![image-20220520152007648](assets/image-20220520152007648.png) 这里的哨兵有两个作用 - 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。 - 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。 然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。![image-20220520152042159](assets/image-20220520152042159.png) 假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象称为**主观下线**。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover故障转移操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为**客观下线** ## 优缺点 优点: 1、哨兵集群,基于主从复制模式,所有的主从配置优点,它全有 2、 主从可以切换,故障可以转移,系统的可用性就会更好 3、哨兵模式就是主从模式的升级,手动到自动,更加健壮! 缺点: 1、Redis 不好在线扩容的,集群容量一旦到达上限,在线扩容就十分麻烦! 2、实现哨兵模式的配置其实是很麻烦的,里面有很多选择! ## 配置 ```shell # Example sentinel.conf # 哨兵sentinel实例运行的端口 默认26379 port 26379 # 哨兵sentinel的工作目录 dir /tmp # 哨兵sentinel监控的redis主节点的 ip port # master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。 # quorum 配置多少个sentinel哨兵统一认为master主节点失联 那么这时客观上认为主节点失联了 # sentinel monitor sentinel monitor mymaster 127.0.0.1 6379 2 # 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码 # 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码 # sentinel auth-pass sentinel auth-pass mymaster MySUPER--secret-0123passw0rd # 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒 # sentinel down-after-milliseconds sentinel down-after-milliseconds mymaster 30000 # 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行同步, # 这个数字越小,完成failover所需的时间就越长,但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。 # 可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。 # sentinel parallel-syncs sentinel parallel-syncs mymaster 1 # 故障转移的超时时间 failover-timeout 可以用在以下这些方面: #1. 同一个sentinel对同一个master两次failover之间的间隔时间。 #2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。 #3. 当想要取消一个正在进行的failover所需要的时间。 #4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了 # 默认三分钟 # sentinel failover-timeout sentinel failover-timeout mymaster 180000 # SCRIPTS EXECUTION #配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。 #对于脚本的运行结果有以下规则: #若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10 #若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。 #如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。 #一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。 #通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,一个是事件的类型,一个是事件的描述。如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。 #通知脚本 # shell编程 # sentinel notification-script sentinel notification-script mymaster /var/redis/notify.sh # 客户端重新配置主节点参数脚本 # 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。 # 以下参数将会在调用脚本时传给脚本: # # 目前总是“failover”, # 是“leader”或者“observer”中的一个。 # 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的 # 这个脚本应该是通用的,能被多次调用,不是针对性的。 # sentinel client-reconfig-script sentinel client-reconfig-script mymaster /var/redis/reconfig.sh # 一般都是由运维来配置! ``` # Redis缓存穿透和雪崩 *** 服务的高可用问题! 在这里我们不会详细的区分析解决方案的底层! Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。 ![image-20220520164337367](assets/image-20220520164337367.png) ## 缓存穿透(查不到) ### 概念 缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中(秒杀!),于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。 ### 解决方案 布隆过滤器 布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力; ![image-20220520164434639](assets/image-20220520164434639.png) ## 缓存击穿(量太大,缓存过期) ### 概述 这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。 当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。 ### 解决方案 **设置热点数据永不过期** 从缓存层面来看,没有设置过期时间,所以不会出现热点 key 过期后产生的问题。 **加互斥锁** 分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。 ![image-20220520164601716](assets/image-20220520164601716.png) ## 缓存雪崩 ### 概念 缓存雪崩,是指在某一个时间段,缓存集中过期失效。Redis 宕机! 产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。 ![image-20220520164627927](assets/image-20220520164627927.png) 其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。 ### 解决方案 **redis高可用** 这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。(异地多活!) **限流降级**(在SpringCloud讲解过!) 这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。 **数据预热** 数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。