- 本文参考:
- 弄清楚NoSQL
- Redis入门
- Redis事务
- 虽然中间有一条命令报错了,但是后面的指令依旧正常执行成功了。
- 所以说Redis单条指令保证原子性,但是Redis事务不能保证原子性。
- Jedis
- SpringBoot整合Redis
- Redis.conf
- 持久化
- Redis发布与订阅
- Redis主从复制
- Replication
- Example sentinel.conf
- 哨兵sentinel实例运行的端口 默认26379
- 哨兵sentinel的工作目录
- 哨兵sentinel监控的redis主节点的 ip port
- master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符”.-_”组成。
- quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
- sentinel monitor
- 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
- 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
- sentinel auth-pass
- 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
- sentinel down-after-milliseconds
- 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
- sentinel parallel-syncs
- 故障转移的超时时间 failover-timeout 可以用在以下这些方面:
- 1. 同一个sentinel对同一个master两次failover之间的间隔时间。
- 2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
- 3.当想要取消一个正在进行的failover所需要的时间。
- 4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
- 默认三分钟
- sentinel failover-timeout
- SCRIPTS EXECUTION
- 配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
- 对于脚本的运行结果有以下规则:
- 若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
- 若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
- 如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
- 一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
- 通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,
- 这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,
- 一个是事件的类型,
- 一个是事件的描述。
- 如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
- 通知脚本
- sentinel notification-script
- 客户端重新配置主节点参数脚本
- 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
- 以下参数将会在调用脚本时传给脚本:
- 目前
总是“failover”, 是“leader”或者“observer”中的一个。 - 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
- 这个脚本应该是通用的,能被多次调用,不是针对性的。
- sentinel client-reconfig-script
本文参考:
博客:https://blog.csdn.net/DDDDeng_/article/details/108118544
B站狂神说Java:https://www.bilibili.com/video/BV1S54y1R7SB?p=7&spm_id_from=pageDriver
弄清楚NoSQL
为什么使用Nosql(架构演变)
1.单机Mysql时代
90年代,一个网站的访问量一般不会太大,单个数据库完全够用。随着用户增多,网站会出现以下问题:
- 数据量增加到一定程度,单机数据库就放不下了
- 数据的索引(B+ Tree), 一个机器内存也存放不下
- 访问量变大后(读写混合),一台服务器承受不住
2.Memcached(缓存) + Mysql + 垂直拆分(读写分离)
网站80%的情况都是在读,每次都要去查询数据库的话就十分的麻烦!所以说我们希望减轻数据库的压力,我们可以使用缓存来保证效率!
优化过程经历了以下几个过程:
- 优化数据库的数据结构和索引(难度大)
- 文件缓存,通过IO流获取比每次都访问数据库效率略高,但是流量爆炸式增长时候,IO流也承受不了
MemCache, 当时最热门的技术,通过在数据库和数据库访问层之间加上一层缓存,第一次访问时查询数据库,将结果保存到缓存,后续的查询先检查缓存,若有直接拿去使用,效率显著提升。
3.分库分表 + 水平拆分 + Mysql集群
4.如今最近的年代(结合Nosql)
如今信息量井喷式增长,各种各样的数据出现(用户定位数据,图片数据等),大数据的背景下关系型数据库(RDBMS)无法满足大量数据要求。Nosql数据库就能轻松解决这些问题。
目前一个基本的互联网项目对比传统的RDBMS和NOSQL**
RDBMS(Relational Database Management System),一般指关系型数据库管理系统
NOSQL(Not Only SQL) 不仅仅是SQL,泛指非关系型数据库。
传统的RDBMS- 结构化组织
- sql
- 数据和关系都存储在表中 row /column
- 数据定义语言
- 严格的一致性
- 基础的事务
NOSQL
- 不仅仅是数据
- 没有固定查询语言
- 很多的存储方式,列存储、文档存储、图形存储(社交关系)
- 可以不用满足严格一致性,数据是可以有误差的,要保证的是最终一致性
- CAP定理,和BASE理论
- 高性能、高可用、高可扩展
总结为什么要用Nosql**
- Web2.0的诞生,用户的个人信息、社交网络、地理位置、用户自己生产的数据、用户日志等数据量爆发式的增长,要存储的数据格式多样,传统的关系型数据库很难对付,尤其是超大规模的高并发的社区。
- Nosql数据类型是多样性的,以键值对来控制Map< String , Object >,不需要事先设计数据库,不需要设计键值对,随取随用。数据之间没有关系就很好扩展和解耦。
- 也是为了高性能(比如:Redis官方的数据读取速度每秒11w次,写8w次)。
- 真正在公司中实践一定是:NOSQL+RDBMS一起使用才是最好的。
阿里巴巴框架演进
前四代架构
第五代架构改进
- 敏捷开发
- 极限编程
- 业务快速增长,每天都要上线大量的小需求
- 应用系统日益膨胀,耦合恶化,架构越来越复杂,带来更高的开发成本,如何保持业务开发的敏捷性
- 开放,提升网站的开放性,吸引第三方开发者加入网站的建设
- 体验,网站并发压力快速增长,用户对体验提出了更高的要求
- 使用了各种数据库,这么多种类型的数据库导致数据架构非常复杂,要简化架构,增加一层就行,像是jdbc一样
- 商品中的信息存在不同的数据库中
- 商品的基本信息(Mysql / Oracle。淘宝早些年就去IOE了,去掉IBM小型机、Oracle数据库、EMC存储设备)
- 商家的描述、评论(文档型数据库MongoDB)
- 图片(分布式文件系统:FastDFS,TFS,HDFS,GFS,OSS…)
- 商品的关键字(搜索引擎:elasticsearch, Isearch, solr)
- 商品热门的波段信息(缓存数据库:Redis 、Tair 、Memacache)
- 商品的交易,外部支付接口(第三方应用)
大型互联网应用问题:
- 数据类型太多
- 数据源繁多,经常重构
- 数据要改造,大面积改造
阿里的解决方案:
统一数据服务层UDSL,在网站应用集群和底层数据源之间,构建一层代理,统一数据层。
- 模型数据映射
- 实现 业务模型 各属性 与 底层不同类型数据源的模型数据映射
- 统一的查询和更新API
- 提供了基于业务模型的统一的查询和更新的API,简化网站应用跨不同数据源的开发模式
- 性能优化
- 设计了一套统一的DSL,提供了统一的增删改查的API,开发速度问题是解决了,但是性能还是问题
- 网站数据庞大,只能缓存热点数据,解决方案,开发热点缓存平台,提供UDSL作为缓存系统
Nosql的四大分类
1.KV键值对
- 新浪:Redis
- 美团:Redis+Tair
-
2.文档型数据库(Bson格式和Json格式)
MongoDB
- MongoDB是一个基于分布式文件存储的数据库,C++编写,用来处理大量的文档
- MongoDB是一个介于关系型数据库和非关系型中间的产品,非关系型数据库中功能最丰富的,最像关系型数据库的
-
3.列存储数据库
HBase
-
4.图形关系数据库
不是用来存放图形的,是用来寸关系的,朋友圈社交,广告推荐
- Neo4j,InfoGrid
Redis入门
概述
Redis是什么?
Redis(Remote Dictionary Server ),即远程字典服务。是一个开源的使用C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
Redis能该干什么?
- 内存存储、持久化,内存是断电即失的,所以需要持久化(RDB、AOF)
- 高效率、用于高速缓冲
- 发布订阅系统
- 地图信息分析
- 计时器、计数器(eg:浏览量)
- 其他…
特性
- 多样的数据类型
- 持久化
- 集群
- 事务
Linux安装Redis
- 下载安装包!redis-5.0.7.tar.gz, 上传到Linux系统
- 解压Redis的安装包!
tar -zxvf redis-5.0.7.tar.gz
程序一般放在/opt
目录下
3.基本环境安装yum install gcc-c++
#然后进入解压后redis目录下执行 make
# 然后执行 make install
4.redis默认安装路径 /usr/local/bin
5.将redis的redis.conf配置文件复制到程序安装目录 /usr/local/bin/kconfig
下(先新建kconfig文件夹)
6.redis默认不是后台启动的,需要修改配置文件!
7.通过制定的配置文件启动redis服务, 安装bin目录下执行 redis-server ../redis配置文件名
8.使用 redis-cli -p 端口号
连接指定的端口号测试,Redis的默认端口:6379
9.查看redis进程是否开启ps -ef|grep redis
10.关闭Redis服务 shutdown
11.再次查看进程是否存在
12.后面我们会使用单机多Redis启动集群测试
Redis基本命令
1.查看Redis进程是否启动
#主要用前两个
ps -ef | grep redis
ps -aux | grep redis
netstat -tunple | grep 6379
lsof -i :6379
2.启动Redis
#server服务端
1.前台启动
./redis-server & (说明:输入./redis-server启动命令记得要在后面加&,这样ctrl+c退出之后就不会中断启动操作。 ./表示本路径下)
ctrl+c 退出(不中断程序)
2.后台启动(通过配置文件启动)
./redis-server kconfig/redis.conf (说明:没报错就表示后台启动成功)
#client客户端
./redis-cli -p 6379 启动
ctrl+c 或 exit 退出
3.关闭Redis服务
方式一:
./redis-cli -p 6379 然后进去 shutdown, exit
方式二:
通过ps -ef | grep redis 或 ps -aux | grep redis查看Redis服务的PID,再通过 kill -9 PID 来关闭服务(例如:kill -9 6933)
4.Redis工具
redis-benchmark #redis性能测试工具
redis-check-aof aof #文件修复工具
redis-check-dump rdb #文件检查工具
测试性能
redis-benchmark: Redis官方提供的性能测试工具,参数选项如下:
安装bin目录下执行:redis-benchmark -[options]
简单测试:
# 测试:100个并发连接 100000请求redis-benchmark -h localhost -p 6379 -c 100 -n 100000
Redis基础知识
redis默认有**16**个数据库, 默认使用的第0个 #**vim redis.conf 查看一下**
16个数据库为:DB 0~DB 15 ,当前默认使用DB 0 ,可以使用select n
切换到DB n,dbsize
可以查看当前数据库的大小,与key数量相关。
127.0.0.1:6379> config get databases # 命令行查看数据库数量databases
1) "databases"
2) "16"
127.0.0.1:6379> select 8 # 切换数据库 DB 8
OK
127.0.0.1:6379[8]> dbsize # 查看数据库大小
(integer) 0
# 不同数据库之间 数据是不能互通的,并且dbsize 是根据库中key的个数。
127.0.0.1:6379> set name sakura
OK
127.0.0.1:6379> SELECT 8
OK
127.0.0.1:6379[8]> get name # db8中并不能获取db0中的键值对。
(nil)
127.0.0.1:6379[8]> DBSIZE
(integer) 0
127.0.0.1:6379[8]> SELECT 0
OK
127.0.0.1:6379> keys *
1) "counter:__rand_int__"
2) "mylist"
3) "name"
4) "key:__rand_int__"
5) "myset:__rand_int__"
127.0.0.1:6379> DBSIZE # size和key个数相关
(integer) 5
keys *
:查看当前数据库中所有的key。flushdb
:清空当前数据库中的键值对。flushall
:清空所有数据库的键值对。
Redis是单线程的,Redis是基于内存操作的。
所以Redis的性能瓶颈不是CPU, 而是机器内存和网络带宽。那么为什么Redis的速度如此快呢,性能这么高呢?QPS达到10W+
Redis为什么单线程还这么快?**
- 误区1:高性能的服务器一定是多线程的?
- 误区2:多线程(CPU调度:上下文会切换!)一定比单线程效率高?
- 核心:
- Redis是将所有的数据放在内存中的,所以说使用单线程去操作效率就是最高的,多线程(CPU上下文会切换:耗时的操作!),对于内存系统来说,如果没有上下文切换效率就是最高的,多次读写都是在一个CPU上的。在内存存储数据情况下,单线程就是最佳的方案。
补充:
- Redis中的数据结构是专门进行设计的,数据结构简单,对数据操作也简单;
- 使用多路I/O复用模型,非阻塞IO(多路”指的是多个网络连接,“复用”指的是复用同一个线程);
- Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
Redis命令使用手册:https://www.redis.net.cn/order/
五大数据类型
Redis是一个开源(BSD许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs等数据类型。内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。Redis-key
在redis中无论什么数据类型,在数据库中都是以key-value形式保存,通过进行对Redis-key的操作,来完成对数据库中数据的操作。
常用的命令:
exists key
:判断键是否存在del key
:删除键值对move key db
:将键值对移动到指定数据库expire key second
:设置键值对的过期时间type key
:查看value的数据类型 ```shell 127.0.0.1:6379> keys # 查看当前数据库所有key (empty list or set) 127.0.0.1:6379> set name qinjiang # set key OK 127.0.0.1:6379> set age 20 OK 127.0.0.1:6379> keys 1) “age” 2) “name” 127.0.0.1:6379> move age 1 # 将键值对移动到指定数据库 (integer) 1 127.0.0.1:6379> EXISTS age # 判断键是否存在 (integer) 0 # 不存在 127.0.0.1:6379> EXISTS name (integer) 1 # 存在 127.0.0.1:6379> SELECT 1 OK 127.0.0.1:6379[1]> keys * 1) “age” 127.0.0.1:6379[1]> del age # 删除键值对 (integer) 1 # 删除个数
127.0.0.1:6379> set age 20 OK 127.0.0.1:6379> EXPIRE age 15 # 设置键值对的过期时间
(integer) 1 # 设置成功 开始计数 127.0.0.1:6379> ttl age # 查看key的过期剩余时间 (integer) 13 127.0.0.1:6379> ttl age (integer) 11 127.0.0.1:6379> ttl age (integer) 9 127.0.0.1:6379> ttl age (integer) -2 # -2 表示key过期,-1表示key未设置过期时间
127.0.0.1:6379> get age # 过期的key 会被自动delete (nil) 127.0.0.1:6379> keys * 1) “name”
127.0.0.1:6379> type name # 查看value的数据类型 string
**关于`TTL`命令,**通过TTL命令返回key的过期时间,一般来说有3种:
1. 当前key没有设置过期时间,所以会返回-1.
1. 当前key有设置过期时间,而且key已经过期,所以会返回-2.
1. 当前key有设置过期时间,且key还没有过期,故会返回key的正常剩余时间.
**关于重命名`RENAME`和`RENAMENX`**
- `RENAME key newkey`修改 key 的名称
- `RENAMENX key newkey`仅当 newkey 不存在时,将 key 改名为 newkey
<a name="2vHiy"></a>
### 字符串(String)命令
| **命令** | **描述** | **示例** |
| --- | --- | --- |
| [Redis Setnx 命令](https://www.redis.net.cn/order/3552.html) | 只有在 key 不存在时设置 key 的值。 | redis 127.0.0.1:6379> SETNX KEY_NAME VALUE |
| [Redis Getrange 命令](https://www.redis.net.cn/order/3546.html) | 返回 key 中字符串值的子字符 | 参考手册 |
| [Redis Mset 命令](https://www.redis.net.cn/order/3555.html) | 同时设置一个或多个 key-value 对。 | redis 127.0.0.1:6379>MSET key1 value1 key2 value2 .. keyN valueN |
| [Redis Setex 命令](https://www.redis.net.cn/order/3551.html) | 将值 value 关联到 key ,并将 key 的过期时间设为 seconds (以秒为单位)。 | redis 127.0.0.1:6379>SETEX KEY_NAME TIMEOUT VALUE |
| [Redis SET 命令](https://www.redis.net.cn/order/3544.html) | 设置指定 key 的值 | redis 127.0.0.1:6379>SET KEY_NAME VALUE |
| [Redis Get 命令](https://www.redis.net.cn/order/3545.html) | 获取指定 key 的值。 | redis 127.0.0.1:6379>GET KEY_NAME |
| [Redis Getbit 命令](https://www.redis.net.cn/order/3548.html) | 对 key 所储存的字符串值,获取指定偏移量上的位(bit)。 | 参考手册 |
| [Redis Setbit 命令](https://www.redis.net.cn/order/3550.html) | 对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。 | 参考手册 |
| [Redis Decr 命令](https://www.redis.net.cn/order/3561.html) | 将 key 中储存的数字值减一。 | 参考手册 |
| [Redis Decrby 命令](https://www.redis.net.cn/order/3562.html) | key 所储存的值减去给定的减量值(decrement) 。 | 参考手册 |
| [Redis Strlen 命令](https://www.redis.net.cn/order/3554.html) | 返回 key 所储存的字符串值的长度。 | 参考手册 |
| [Redis Msetnx 命令](https://www.redis.net.cn/order/3556.html) | 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。 | 参考手册 |
| [Redis Incrby 命令](https://www.redis.net.cn/order/3559.html) | 将 key 所储存的值加上给定的增量值(increment) 。 | 参考手册 |
| [Redis Incrbyfloat 命令](https://www.redis.net.cn/order/3560.html) | 将 key 所储存的值加上给定的浮点增量值(increment) 。 | 参考手册 |
| [Redis Setrange 命令](https://www.redis.net.cn/order/3553.html) | 用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始。 | redis 127.0.0.1:6379>SETRANGE KEY_NAME OFFSET VALUE |
| [Redis Psetex 命令](https://www.redis.net.cn/order/3557.html) | 这个命令和 SETEX 命令相似,但它以毫秒为单位设置 key 的生存时间,而不是像 SETEX 命令那样,以秒为单位。 | 参考手册 |
| [Redis Append 命令](https://www.redis.net.cn/order/3563.html) | 如果 key 已经存在并且是一个字符串, APPEND 命令将 value 追加到 key 原来的值的末尾。 | redis 127.0.0.1:6379>APPEND KEY_NAME NEW_VALUE |
| [Redis Getset 命令](https://www.redis.net.cn/order/3547.html) | 将给定 key 的值设为 value ,并返回 key 的旧值(old value)。 | redis 127.0.0.1:6379>GETSET KEY_NAME VALUE |
| [Redis Mget 命令](https://www.redis.net.cn/order/3549.html) | 获取所有(一个或多个)给定 key 的值。 | redis 127.0.0.1:6379> MGET KEY1 KEY2 .. KEYN |
| [Redis Incr 命令](https://www.redis.net.cn/order/3558.html) | 将 key 中储存的数字值增一。 | 参考手册 |
**String类似的使用场景:value除了是字符串还可以是数字,用途举例:**
- 计数器
- 统计多单位的数量:uid:123666:follow 0
- 粉丝数
- 对象存储缓存
<a name="eFZnT"></a>
### 哈希(Hash) 命令
Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。<br />Set就是一种简化的Hash,只变动key,而value使用默认值填充。可以将一个Hash表作为一个对象进行存储,表中存放对象的信息。
| **命令** | **描述** | **示例** |
| --- | --- | --- |
| [Redis Hmset 命令](https://www.redis.net.cn/order/3573.html) | 同时将多个 field-value (域-值)对设置到哈希表 key 中。 | redis 127.0.0.1:6379> HMSET KEY_NAME FIELD1 VALUE1 ...FIELDN VALUEN |
| [Redis Hmget 命令](https://www.redis.net.cn/order/3572.html) | 获取所有给定字段的值 | redis 127.0.0.1:6379> HMGET KEY_NAME FIELD1...FIELDN |
| [Redis Hset 命令](https://www.redis.net.cn/order/3574.html) | 将哈希表 key 中的字段 field 的值设为 value 。 | redis 127.0.0.1:6379> HSET KEY_NAME FIELD VALUE |
| [Redis Hgetall 命令](https://www.redis.net.cn/order/3567.html) | 获取在哈希表中指定 key 的所有字段和值 | redis 127.0.0.1:6379> HGETALL KEY_NAME |
| [Redis Hget 命令](https://www.redis.net.cn/order/3566.html) | 获取存储在哈希表中指定字段的值/td> | redis 127.0.0.1:6379> HGET KEY_NAME FIELD_NAME |
| [Redis Hexists 命令](https://www.redis.net.cn/order/3565.html) | 查看哈希表 key 中,指定的字段是否存在。 | redis 127.0.0.1:6379> HEXISTS KEY_NAME FIELD_NAME |
| [Redis Hincrby 命令](https://www.redis.net.cn/order/3568.html) | 为哈希表 key 中的指定字段的整数值加上增量 increment 。 | redis 127.0.0.1:6379>HINCRBY KEY_NAME FIELD_NAME INCR_BY_NUMBER |
| [Redis Hlen 命令](https://www.redis.net.cn/order/3571.html) | 获取哈希表中字段的数量 | redis 127.0.0.1:6379> HLEN KEY_NAME |
| [Redis Hdel 命令](https://www.redis.net.cn/order/3564.html) | 删除一个或多个哈希表字段 | redis 127.0.0.1:6379> HDEL KEY_NAME FIELD1.. FIELDN |
| [Redis Hvals 命令](https://www.redis.net.cn/order/3576.html) | 获取哈希表中所有值 | redis 127.0.0.1:6379> HVALS KEY_NAME FIELD VALUE |
| [Redis Hincrbyfloat 命令](https://www.redis.net.cn/order/3569.html) | 为哈希表 key 中的指定字段的浮点数值加上增量 increment 。 | redis 127.0.0.1:6379> HINCRBYFLOAT KEY_NAME FIELD_NAME INCR_BY_NUMBER |
| [Redis Hkeys 命令](https://www.redis.net.cn/order/3570.html) | 获取所有哈希表中的字段 | redis 127.0.0.1:6379> |
| [Redis Hsetnx 命令](https://www.redis.net.cn/order/3575.html) | 只有在字段 field 不存在时,设置哈希表字段的值。 | redis 127.0.0.1:6379> HSETNX KEY_NAME FIELD VALUE |
<a name="5iZh8"></a>
### 列表(List) 命令
Redis列表是简单的字符串列表,按照插入顺序排序。**你可以添加一个元素到列表的头部(左边)或者尾部(右边)**<br />一个列表最多可以包含 232 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。<br />首先我们列表,可以经过规则定义将其变为队列、栈、双端队列等:<br />![](https://cdn.nlark.com/yuque/0/2021/png/22103819/1626436193934-efa9bbd7-3085-4c48-88ab-6e8420f4d6b2.png#align=left&display=inline&height=174&margin=%5Bobject%20Object%5D&originHeight=306&originWidth=1119&size=0&status=done&style=none&width=638)<br />正如图Redis中List是可以进行双端操作的,所以命令也就分为了LXXX和RLLL两类,有时候L也表示List例如LLEN
| **命令** | **描述** | **示例** |
| --- | --- | --- |
| [Redis Lindex 命令](https://www.redis.net.cn/order/3580.html) | 通过索引获取列表中的元素 | redis 127.0.0.1:6379> LINDEX KEY_NAME INDEX_POSITION |
| [Redis Rpush 命令](https://www.redis.net.cn/order/3592.html) | 在列表中添加一个或多个值 | redis 127.0.0.1:6379> RPUSH KEY_NAME VALUE1..VALUEN |
| [Redis Lrange 命令](https://www.redis.net.cn/order/3586.html) | 获取列表指定范围内的元素 | redis 127.0.0.1:6379> LRANGE KEY_NAME START END |
| [Redis Rpoplpush 命令](https://www.redis.net.cn/order/3591.html) | 移除列表的最后一个元素,并将该元素添加到另一个列表并返回 | redis 127.0.0.1:6379> RPOPLPUSH SOURCE_KEY_NAME DESTINATION_KEY_NAME |
| [Redis Blpop 命令](https://www.redis.net.cn/order/3577.html) | 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 | redis 127.0.0.1:6379> BLPOP LIST1 LIST2 .. LISTN TIMEOUT |
| [Redis Brpop 命令](https://www.redis.net.cn/order/3578.html) | 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 | redis 127.0.0.1:6379> BRPOP LIST1 LIST2 .. LISTN TIMEOUT |
| [Redis Brpoplpush 命令](https://www.redis.net.cn/order/3579.html) | 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 | redis 127.0.0.1:6379> BRPOPLPUSH LIST1 ANOTHER_LIST TIMEOUT |
| [Redis Lrem 命令](https://www.redis.net.cn/order/3587.html) | 移除列表元素 | redis 127.0.0.1:6379> LREM KEY_NAME COUNT VALUE |
| [Redis Llen 命令](https://www.redis.net.cn/order/3582.html) | 获取列表长度 | redis 127.0.0.1:6379> LLEN KEY_NAME |
| [Redis Ltrim 命令](https://www.redis.net.cn/order/3589.html) | 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。 | redis 127.0.0.1:6379> LTRIM KEY_NAME START STOP |
| [Redis Lpop 命令](https://www.redis.net.cn/order/3583.html) | 移出并获取列表的第一个元素 | redis 127.0.0.1:6379> LPOP KEY_NAME |
| [Redis Lpushx 命令](https://www.redis.net.cn/order/3585.html) | 将一个或多个值插入到已存在的列表头部 | redis 127.0.0.1:6379> LPUSHX KEY_NAME VALUE1.. VALUEN |
| [Redis Linsert 命令](https://www.redis.net.cn/order/3581.html) | 在列表的元素前或者后插入元素 | redis 127.0.0.1:6379> LINSERT KEY_NAME BEFORE EXISTING_VALUE NEW_VALUE |
| [Redis Rpop 命令](https://www.redis.net.cn/order/3590.html) | 移除并获取列表最后一个元素 | redis 127.0.0.1:6379> RPOP KEY_NAME |
| [Redis Lset 命令](https://www.redis.net.cn/order/3588.html) | 通过索引设置列表元素的值 | redis 127.0.0.1:6379> LSET KEY_NAME INDEX VALUE |
| [Redis Lpush 命令](https://www.redis.net.cn/order/3584.html) | 将一个或多个值插入到列表头部 | redis 127.0.0.1:6379> LPUSH KEY_NAME VALUE1.. VALUEN |
| [Redis Rpushx 命令](https://www.redis.net.cn/order/3593.html) | 为已存在的列表添加值 | redis 127.0.0.1:6379> RPUSHX KEY_NAME VALUE1..VALUEN |
Hash变更的数据user : name age,尤其是用户信息之类的,经常变动的信息!**Hash更适合于对象的存储,Sring更加适合字符串存储!**
<a name="XgdgS"></a>
### Redis 集合(Set) 命令
Redis的Set是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。Redis 中 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
| **命令** | **描述** | **示例** |
| --- | --- | --- |
| [Redis Sunion 命令](https://www.redis.net.cn/order/3606.html) | 返回所有给定集合的并集 | redis 127.0.0.1:6379>SUNION KEY KEY1..KEYN |
| [Redis Scard 命令](https://www.redis.net.cn/order/3595.html) | 获取集合的成员数 | redis 127.0.0.1:6379>SCARD KEY_NAME |
| [Redis Srandmember 命令](https://www.redis.net.cn/order/3604.html) | 返回集合中一个或多个随机数 | redis 127.0.0.1:6379>SRANDMEMBER KEY [count] |
| [Redis Smembers 命令](https://www.redis.net.cn/order/3601.html) | 返回集合中的所有成员 | redis 127.0.0.1:6379>SMEMBERS KEY VALUE |
| [Redis Sinter 命令](https://www.redis.net.cn/order/3598.html) | 返回给定所有集合的交集 | redis 127.0.0.1:6379>SINTER KEY KEY1..KEYN |
| [Redis Srem 命令](https://www.redis.net.cn/order/3605.html) | 移除集合中一个或多个成员 | redis 127.0.0.1:6379>SREM KEY MEMBER1..MEMBERN |
| [Redis Smove 命令](https://www.redis.net.cn/order/3602.html) | 将 member 元素从 source 集合移动到 destination 集合 | redis 127.0.0.1:6379>SMOVE SOURCE DESTINATION MEMBER |
| [Redis Sadd 命令](https://www.redis.net.cn/order/3594.html) | 向集合添加一个或多个成员 | redis 127.0.0.1:6379>SADD KEY_NAME VALUE1..VALUEN |
| [Redis Sismember 命令](https://www.redis.net.cn/order/3600.html) | 判断 member 元素是否是集合 key 的成员 | redis 127.0.0.1:6379>SISMEMBER KEY VALUE |
| [Redis Sdiffstore 命令](https://www.redis.net.cn/order/3597.html) | 返回给定所有集合的差集并存储在 destination 中 | redis 127.0.0.1:6379>SDIFFSTORE DESTINATION_KEY KEY1..KEYN |
| [Redis Sdiff 命令](https://www.redis.net.cn/order/3596.html) | 返回给定所有集合的差集 | redis 127.0.0.1:6379>SDIFF FIRST_KEY OTHER_KEY1..OTHER_KEYN |
| [Redis Sscan 命令](https://www.redis.net.cn/order/3608.html) | 迭代集合中的元素 | redis 127.0.0.1:6379>SSCAN KEY [MATCH pattern] [COUNT count] |
| [Redis Sinterstore 命令](https://www.redis.net.cn/order/3599.html) | 返回给定所有集合的交集并存储在 destination 中 | redis 127.0.0.1:6379>SINTERSTORE DESTINATION_KEY KEY KEY1..KEYN |
| [Redis Sunionstore 命令](https://www.redis.net.cn/order/3607.html) | 所有给定集合的并集存储在 destination 集合中 | redis 127.0.0.1:6379>SUNIONSTORE DESTINATION KEY KEY1..KEYN |
| [Redis Spop 命令](https://www.redis.net.cn/order/3603.html) | 移除并返回集合中的一个随机元素 | redis 127.0.0.1:6379>SPOP KEY |
<a name="RgEuA"></a>
### 有序集合(sorted set) 命令
不同的是,每个元素都会关联一个double类型的分数(score)。redis正是通过分数来为集合中的成员进行从小到大的排序。<br />score相同:按字典顺序排序。有序集合的成员是唯一的,但分数(score)却可以重复。
| **命令** | **描述** | **示例** |
| --- | --- | --- |
| [Redis Zrevrank 命令](https://www.redis.net.cn/order/3625.html) | 返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序 | redis 127.0.0.1:6379>ZREVRANK key member |
| [Redis Zlexcount 命令](https://www.redis.net.cn/order/3614.html) | 在有序集合中计算指定字典区间内成员数量 | redis 127.0.0.1:6379>ZLEXCOUNT KEY MIN MAX |
| [Redis Zunionstore 命令](https://www.redis.net.cn/order/3627.html) | 计算给定的一个或多个有序集的并集,并存储在新的 key 中 | 参考手册 |
| [Redis Zremrangebyrank 命令](https://www.redis.net.cn/order/3621.html) | 移除有序集合中给定的排名区间的所有成员 | redis 127.0.0.1:6379>ZREMRANGEBYRANK key start stop |
| [Redis Zcard 命令](https://www.redis.net.cn/order/3610.html) | 获取有序集合的成员数 | redis 127.0.0.1:6379>ZCARD KEY_NAME |
| [Redis Zrem 命令](https://www.redis.net.cn/order/3619.html) | 移除有序集合中的一个或多个成员 | redis 127.0.0.1:6379>ZRANK key member |
| [Redis Zinterstore 命令](https://www.redis.net.cn/order/3613.html) | 计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中 | 参考手册 |
| [Redis Zrank 命令](https://www.redis.net.cn/order/3618.html) | 返回有序集合中指定成员的索引 | redis 127.0.0.1:6379>ZRANK key member |
| [Redis Zincrby 命令](https://www.redis.net.cn/order/3612.html) | 有序集合中对指定成员的分数加上增量 increment | redis 127.0.0.1:6379>ZINCRBY key increment member |
| [Redis Zrangebyscore 命令](https://www.redis.net.cn/order/3617.html) | 通过分数返回有序集合指定区间内的成员 | redis 127.0.0.1:6379>ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] |
| [Redis Zrangebylex 命令](https://www.redis.net.cn/order/3616.html) | 通过字典区间返回有序集合的成员 | redis 127.0.0.1:6379>ZRANGEBYLEX key min max [LIMIT offset count] |
| [Redis Zscore 命令](https://www.redis.net.cn/order/3626.html) | 返回有序集中,成员的分数值 | redis 127.0.0.1:6379>ZSCORE key member |
| [Redis Zremrangebyscore 命令](https://www.redis.net.cn/order/3622.html) | 移除有序集合中给定的分数区间的所有成员 | redis 127.0.0.1:6379>ZREMRANGEBYSCORE key min max |
| [Redis Zscan 命令](https://www.redis.net.cn/order/3628.html) | 迭代有序集合中的元素(包括元素成员和元素分值) | redis 127.0.0.1:6379>ZSCAN key cursor [MATCH pattern] [COUNT count] |
| [Redis Zrevrangebyscore 命令](https://www.redis.net.cn/order/3624.html) | 返回有序集中指定分数区间内的成员,分数从高到低排序 | redis 127.0.0.1:6379>ZREMRANGEBYSCORE key min max |
| [Redis Zremrangebylex 命令](https://www.redis.net.cn/order/3620.html) | 移除有序集合中给定的字典区间的所有成员 | redis 127.0.0.1:6379>ZREMRANGEBYLEX key min max |
| [Redis Zrevrange 命令](https://www.redis.net.cn/order/3623.html) | 返回有序集中指定区间内的成员,通过索引,分数从高到底 | redis 127.0.0.1:6379>ZREVRANGE key start stop [WITHSCORES] |
| [Redis Zrange 命令](https://www.redis.net.cn/order/3615.html) | 通过索引区间返回有序集合成指定区间内的成员 | redis 127.0.0.1:6379>ZRANGE key start stop [WITHSCORES] |
| [Redis Zcount 命令](https://www.redis.net.cn/order/3611.html) | 计算在有序集合中指定区间分数的成员数 | redis 127.0.0.1:6379>ZCOUNT key min max |
| [Redis Zadd 命令](https://www.redis.net.cn/order/3609.html) | 向有序集合添加一个或多个成员,或者更新已存在成员的分数 | redis 127.0.0.1:6379>ZADD KEY_NAME SCORE1 VALUE1.. SCOREN VALUEN |
**应用案例:**
- set排序 存储班级成绩表 ,工资表排序!
- 普通消息:1.重要消息 2.带权重进行判断
- 排行榜应用实现,取Top N测试
<a name="t4pXv"></a>
## 三大特殊数据类型
<a name="FpQ8H"></a>
### 地理位置(geo) 命令
地理位置(geospatial ), 使用经纬度定位地理坐标并用一个**有序集合zset保存**,所以zset命令也可以使用。
| **命令** | **描述** | **示例** |
| --- | --- | --- |
| [Redis GEOHASH 命令](https://www.redis.net.cn/order/3687.html) | 返回一个或多个位置元素的 Geohash 表示 | 参考手册 |
| [Redis GEOPOS 命令](https://www.redis.net.cn/order/3688.html) | 从key里返回所有给定位置元素的位置(经度和纬度) | 参考手册 |
| [Redis GEODIST 命令](https://www.redis.net.cn/order/3686.html) | 返回两个给定位置之间的距离 | 参考手册 |
| [Redis GEORADIUS 命令](https://www.redis.net.cn/order/3689.html) | 以给定的经纬度为中心, 找出某一半径内的元素 | 参考手册 |
| [Redis GEOADD 命令](https://www.redis.net.cn/order/3685.html) | 将指定的地理空间位置(纬度、经度、名称)添加到指定的key中 | 参考手册 |
| [Redis GEORADIUSBYMEMBER 命令](https://www.redis.net.cn/order/3690.html) | 找出位于指定范围内的元素,中心点是由给定的位置元素决定 | 参考手册 |
**<br />**有效经纬度**
> - 有效的经度从-180度到180度。
> - 有效的纬度从-85.05112878度到85.05112878度。
**<br />**指定单位的参数 unit 必须是以下单位的其中一个:**
- **m** 表示单位为米。
- **km** 表示单位为千米。
- **mi** 表示单位为英里。
- **ft** 表示单位为英尺。
**<br />**关于GEORADIUS的参数**
> 通过`georadius`就可以完成 **附近的人**功能
> withcoord:带上坐标
> withdist:带上距离,单位与半径单位相同
> COUNT n : 只显示前n个(按距离递增排序)
```java
----------------georadius---------------------
127.0.0.1:6379> GEORADIUS china:city 120 30 500 km withcoord withdist # 查询经纬度(120,30)坐标500km半径内的成员
1) 1) "hangzhou"
2) "29.4151"
3) 1) "120.20000249147415"
2) "30.199999888333501"
2) 1) "shanghai"
2) "205.3611"
3) 1) "121.40000134706497"
2) "31.400000253193539"
------------geohash---------------------------
127.0.0.1:6379> geohash china:city yichang shanghai # 获取成员经纬坐标的geohash表示
1) "wmrjwbr5250"
2) "wtw6ds0y300"
基数统计(HyperLogLog)命令
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。其底层使用string数据类型。
基数:数据集中不重复的元素的个数。
应用场景:
网页的访问量(UV):一个用户多次访问,也只能算作一个人。传统实现,存储用户的id,然后每次进行比较。当用户变多之后这种方式及其浪费空间,而我们的目的只是``**计数**``,Hyperloglog就能帮助我们利用最小的空间完成。
如果允许容错,那么一定可以使用Hyperloglog , 如果不允许容错,就使用set或者自己的数据类型即可 !
命令 | 描述 | 示例 |
---|---|---|
Redis Pgmerge 命令 | 将多个 HyperLogLog 合并为一个 HyperLogLog | 参考手册 |
Redis Pfadd 命令 | 添加指定元素到 HyperLogLog 中。 | 参考手册 |
Redis Pfcount 命令 | 返回给定 HyperLogLog 的基数估算值。 | 参考手册 |
----------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
位图(BitMaps)命令
使用位存储,信息状态只有 0 和 1
Bitmap是一串连续的2进制数字(0或1),每一位所在的位置为偏移(offset),在bitmap上可执行AND,OR,XOR,NOT以及其它位操作。
应用场景**
签到统计、状态统计
命令 | 描述 | 示例 |
---|---|---|
setbit |
为指定key的offset位设置值 | setbit key offset value |
getbit |
获取offset位的值 | getbit key offset |
bitcount |
统计字符串被设置为1的bit数,也可以指定统计范围按字节 | bitcount key [start end] |
bitop |
对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。 | bitop operration destkey key[key..] |
BITPOS |
返回字符串里面第一个被设置为1或者0的bit位。start和end只能按字节,不能按位 | BITPOS key bit [start] [end] |
------------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事务不能保证原子性!
Redis事务本质:一组命令的集合。
————————- 队列 set set set 执行 —————————-
事务中每条命令都会被序列化,执行过程中按顺序执行,不允许其他命令进行干扰。
- 一次性
- 顺序性
- 排他性
- Redis事务没有隔离级别的概念
- Redis单条命令是保证原子性的,但是事务不保证原子性!
事务相关命令
| 命令 | 描述 | | —- | —- | | Redis Exec 命令 | 执行所有事务块内的命令。 | | Redis Watch 命令 | 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。 | | Redis Discard 命令 | 取消事务,放弃执行事务块内的所有命令。 | | Redis Unwatch 命令 | 取消 WATCH 命令对所有 key 的监视。 | | Redis Multi 命令 | 标记一个事务块的开始。 |
Redis事务操作过程
- 开启事务(
multi
) - 命令入队
- 执行事务(
exec
)
事务中的命令在加入时都没有被执行,直到提交时才会开始执行(Exec)一次性完成。
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)-------------------------
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)
---------------------------取消事务(discurd)-------------------------
事务错误
代码语法错误(编译时异常),所有的命令都不执行
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) # 其他命令并没有被执行
代码逻辑错误 (运行时异常) ,其他命令可以正常执行 ** >>> 所以不保证事务原子性 ```java 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事务不能保证原子性。
<a name="FR07M"></a>
## 监控(Watch)
1. **悲观锁:**
- 很悲观,认为什么时候都会出现问题,无论做什么都会加锁
2. **乐观锁:**
- 很乐观,认为什么时候都不会出现问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过这个数据
- 获取version
- 更新的时候比较version
**使用****`watch key`****监控指定数据,相当于乐观锁加锁。**<br />**测试1:**正常执行
```java
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
测试2:多线程修改值,使用watch可以当做redis的乐观锁操作(相当于getversion)。我们启动另外一个客户端模拟插队线程。
------------------------------线程1------------------------------
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> # 此时事务并没有执行
------------------------------线程1------------------------------
------------------------------线程2------------------------------
127.0.0.1:6379> INCRBY money 500 # 修改了线程一中监视的money
(integer) 600
------------------------------线程2------------------------------
------------------------------回到线程1------------------------------
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"
------------------------------回到线程1------------------------------
线程解锁: unwatch
进行解锁。
注意:每次提交执行exec
后都会自动释放锁,不管是否成功!
Jedis
使用Java来操作Redis,Jedis是Redis官方推荐使用的Java连接redis的客户端。
1.导入依赖
<!--导入jredis的包-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
2.编码测试
- 连接数据库
- 修改redis的配置文件
vim /usr/local/bin/myconfig/redis.conf
- 将只绑定本地注释
- 保护模式改为 no
protected-mode no
- 允许后台运行
daemonize yes
- 修改redis的配置文件
- 注意开放端口(或关闭防火墙)
firewall-cmd --zone=public --add-port=6379/tcp --permanet #开放6379端口
systemctl restart firewalld.service #重启防火墙
redis-server myconfig/redis.conf #重启redis-server
#如果使用远程服务器,记得配置好服务器安全组设置
操作命令(TestPing.java)
public class TestPing {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.xx.xxx", 6379);
String response = jedis.ping();
System.out.println(response); // PONG
}
}
3.事务
public class TestTX {
public static void main(String[] args) {
Jedis jedis = new Jedis("39.99.xxx.xx", 6379);
JSONObject jsonObject = new JSONObject();
jsonObject.put("hello", "world");
jsonObject.put("name", "kuangshen");
// 开启事务
Transaction multi = jedis.multi();
String result = jsonObject.toJSONString();
// jedis.watch(result)
try {
multi.set("user1", result);
multi.set("user2", result);
// 执行事务
multi.exec();
}catch (Exception e){
// 放弃事务
multi.discard();
} finally {
// 关闭连接
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
jedis.close();
}
}
}
SpringBoot整合Redis
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
springboot 2.x后 ,原来使用的 Jedis 被 lettuce 替换。
- jedis:采用的直连,多个线程操作的话,是不安全的。如果要避免不安全,使用jedis pool连接池!更像BIO模式
- lettuce:采用netty,实例可以在多个线程中共享,不存在线程不安全的情况!可以减少线程数据了,更像NIO模式
Redis自动配置原理解析
关于Lettuce具体如何替换jedis?有时间可以自行阅读源码,spring.factories-->RedisAutoConfiguration-->RedisProperties,LettuceConnectionConfiguration,JedisConnectionConfiguration-->
进入对比LettuceConnectionConfiguration和JedisConnectionConfiguration
发现Jedis…—>@ConditionalOnClass注解中有两个类是默认不存在的,所以Jedis是无法生效的
再看Lettuce…—>是生效的
然后再看看@RedisAutoConfiguration
只有两个简单的Bean
- RedisTemplate
- StringRedisTemplate
分别用于操作Redis和Redis中的String数据类型。
在RedisTemplate上也有一个条件注解@ConditionalOnMissBean
,意思是没有其他的RedisTemplate配置实例,就用默认的。也就是说我们是可以对其进行定制化替换默认的bean
如何编写配置文件
了解了Redis的自动配置原理后,接下来,我们需要知道配置文件该如何编写(参考RedisProperties类里面提供的所有方法,需要什么就配置什么)
里面有一些基本的配置属性,还有一些连接池相关的配置。注意使用时一定使用Lettuce的连接池。
redis配置文件
# 配置redis
spring.redis.host=39.99.xxx.xx
spring.redis.port=6379
...
使用RedisTemplate
@SpringBootTest
class Redis02SpringbootApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
// redisTemplate 操作不同的数据类型,api和我们的指令是一样的
// opsForValue 操作字符串 类似String
// opsForList 操作List 类似List
// opsForHah
// 除了基本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务和基本的CRUD
// 获取连接对象
//RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
//connection.flushDb();
//connection.flushAll();
redisTemplate.opsForValue().set("mykey","kuangshen");
System.out.println(redisTemplate.opsForValue().get("mykey"));
}
}
测试结果:
此时我们回到Redis查看数据时候,惊奇发现全是乱码,可是程序中可以正常输出的。这时候就关系到存储对象的序列化问题,在网络中传输的对象也是一样需要序列化,否者就全是乱码。
我们默认的RedisTemplate内部是采用JDK序列化器,后续我们定制RedisTemplate就可以对其进行修改!
定制化RedisTemplate模板
我们创建一个Bean加入容器,就会触发RedisTemplate上的条件注解使默认的RedisTemplate失效。
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
// 将template 泛型设置为 <String, Object>
RedisTemplate<String, Object> template = new RedisTemplate();
// 连接工厂,不必修改
template.setConnectionFactory(redisConnectionFactory);
/*
* 序列化设置
*/
// key、hash的key 采用 String序列化方式
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// value、hash的value 采用 Jackson 序列化方式
template.setValueSerializer(RedisSerializer.json());
template.setHashValueSerializer(RedisSerializer.json());
template.afterPropertiesSet();
return template;
}
}
这样一来,只要实体类进行了序列化(实现Serializable接口),我们存什么都不会有乱码的担忧了。
Redis.conf
以下挑选常用的配置部分
网络配置
持久化规则
由于Redis是基于内存的数据库,需要将数据由内存持久化到文件中
持久化方式:RDB / AOF
持久化rdb、aof文件默认保存目录: usr/local/bin/
RDB文件相关**
AOF文件相关
主从复制**
Security模块中进行密码设置
客户端连接相关
maxclients 10000 最大客户端数量
maxmemory <bytes> 最大内存限制
maxmemory-policy noeviction # 内存达到限制值的处理策略
持久化
RDB
在指定时间间隔后,将内存中的数据集快照写入数据库 ;在恢复时候,直接读取快照文件,进行数据的恢复 ;
默认情况下, Redis 将数据库快照保存在名字为 dump.rdb的二进制文件中。文件名可以在配置文件中进行自定义。
工作原理
在进行 RDB 的时候,redis 的主线程是不会做 io 操作的,主线程会 fork 一个子线程来完成该操作;
- Redis 调用forks。同时拥有父进程和子进程。
- 子进程将数据集写入到一个临时 RDB 文件中。
- 当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。
这种工作方式使得 Redis 可以从写时复制(copy-on-write)机制中获益(因为是使用子进程进行写操作,而父进程依然可以接收来自客户端的请求。)
触发机制
- save的规则满足的情况下,会自动触发rdb原则
缺点:
- 需要一定的时间间隔进行操作,如果redis意外宕机了,这个最后一次修改的数据就没有了。
-
AOF
快照功能(RDB)并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、以及未保存到快照中的那些数据。 从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化。
如果要使用AOF,需要修改配置文件:
然后重启redis,就可以生效了!
如果这个aof文件有错误,这时候redis是启动不起来的,需要修改这个aof文件。redis给我们提供了一个修复工具redis-check-aof --fix
优缺点
优点
每一次修改都会同步,文件的完整性会更加好
- 没秒同步一次,可能会丢失一秒的数据
- 从不同步,效率最高
缺点
- 相对于数据文件来说,aof远远大于rdb,修复速度比rdb慢
- Aof运行效率也要比rdb慢,所以我们redis默认的配置就是rdb持久化
RDB 和 AOF 对比
| | RDB | AOF | | —- | :—-: | :—-: | | 启动优先级 | 低 | 高 | | 体积 | 小 | 大 | | 恢复速度 | 快 | 慢 | | 数据安全性 | 丢数据 | 根据策略决定 |
如何选择使用哪种持久化方式?
- 一般来说, 如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。
- 如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。
- 不推荐只使用 AOF 持久化这种方式。
Redis发布与订阅
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端。命令
| 命令 | 描述 | | :—-: | :—-: | |PSUBSCRIBE pattern [pattern..]
| 订阅一个或多个符合给定模式的频道。 | |PUNSUBSCRIBE pattern [pattern..]
| 退订一个或多个符合给定模式的频道。 | |PUBSUB subcommand [argument[argument]]
| 查看订阅与发布系统状态。 | |PUBLISH channel message
| 向指定频道发布消息 | |SUBSCRIBE channel [channel..]
| 订阅给定的一个或多个频道。 | |SUBSCRIBE channel [channel..]
| 退订一个或多个频道 |
示例
------------订阅端----------------------
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"
原理
pubsub_channels 属性是一个字典, 这个字典就用于保存订阅频道的信息,其中,字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端。客户端订阅,就被链接到对应频道的链表的尾部,退订则就是将客户端节点从链表中移除。
缺点
- 如果一个客户端订阅了频道,但自己读取消息的速度却不够快的话,那么不断积压的消息会使redis输出缓冲区的体积变得越来越大,这可能使得redis本身的速度变慢,甚至直接崩溃。
这和数据传输可靠性有关,如果在订阅方断线,那么他将会丢失所有在短线期间发布者发布的消息。
应用
消息订阅:公众号订阅,微博关注等等(起始更多是使用消息队列来进行实现)
- 多人在线聊天室。
Redis主从复制
概念
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(Master/Leader),后者称为从节点(Slave/Follower), 数据的复制是单向的!只能由主节点复制到从节点(主节点以写为主、从节点以读为主)。
默认情况下每台Redis服务器都是主节点,一个主节点可以有0个或者多个从节点,但每个从节点只能由一个主节点。
作用
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余的方式。
- 故障恢复:当主节点故障时,从节点可以暂时替代主节点提供服务,是一种服务冗余的方式
- 负载均衡:在主从复制的基础上,配合读写分离,由主节点进行写操作,从节点进行读操作,分担服务器的负载;尤其是在多读少写的场景下,通过多个从节点分担负载,提高并发量。
- 高可用基石:主从复制还是哨兵和集群能够实施的基础。
为什么使用集群
- 单台服务器难以负载大量的请求
- 单台服务器故障率高,系统崩坏概率大
- 单台服务器内存容量有限。
环境搭建
我们在讲解配置文件的时候,注意到有一个replication
模块 (见Redis.conf中第8条)
查看当前库的信息:info replication
``` 127.0.0.1:6379> info replicationReplication
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文件名<br />启动单机多服务集群:<br />![](https://cdn.nlark.com/yuque/0/2021/png/22103819/1626791568692-843e8809-018b-4d9e-b114-2765e487f95c.png#align=left&display=inline&height=81&margin=%5Bobject%20Object%5D&originHeight=108&originWidth=993&size=0&status=done&style=none&width=745)
<a name="Rh2J4"></a>
### 一主二从配置
默认情况下,每台Redis服务器都是主节点;==我们一般情况下只用配置从机就好了!<br />一主(79),二从(80,81)<br />使用`SLAVEOF host port`就可以为从机配置主机了<br />![](https://cdn.nlark.com/yuque/0/2021/png/22103819/1626791646729-35a2281b-3309-494e-a30b-29fd6dfc26eb.png#align=left&display=inline&height=238&margin=%5Bobject%20Object%5D&originHeight=410&originWidth=1226&size=0&status=done&style=none&width=713)<br />然后主机上也能看到从机的状态<br />![](https://cdn.nlark.com/yuque/0/2021/png/22103819/1626791687394-6fa1e4ce-bdf8-4b89-94be-531683c6539c.png#align=left&display=inline&height=329&margin=%5Bobject%20Object%5D&originHeight=378&originWidth=818&size=0&status=done&style=none&width=711)<br />我们这里是使用命令搭建,是暂时的!
**真实开发中应该在从机的配置文件中进行配置,这样的话是永久的**。<br />![](https://cdn.nlark.com/yuque/0/2021/png/22103819/1626791746932-f0065fb4-979f-4f35-bd3c-f48e41ab8abd.png#align=left&display=inline&height=197&margin=%5Bobject%20Object%5D&originHeight=253&originWidth=912&size=0&status=done&style=none&width=711)
<a name="dkxEF"></a>
### 使用规则
1. 从机只能读,不能写,主机可读可写但是多用于写。
1. 当主机断电宕机后,默认情况下从机的角色不会发生变化 ,集群中只是失去了写操作,当主机恢复以后,又会连接上从机恢复原状。
1. 当从机断电宕机后,若不是使用配置文件配置的从机,再次启动后作为主机是无法获取之前主机的数据的,若此时重新配置称为从机,又可以获取到主机的所有数据。这里就要提到一个同步原理。
1. 第二条中提到,默认情况下,主机故障后,不会出现新的主机,有两种方式可以产生新的主机:
- 从机手动执行命令`slaveof no one`,这样执行以后从机会独立出来成为一个主机
- 使用哨兵模式(自动选举)
<a name="GKZZA"></a>
## 哨兵模式(Sentinel)
手动把一台从服务器切换为主服务器,人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。(保证高可用,一般都采用多少个哨兵)<br />![](https://cdn.nlark.com/yuque/0/2021/png/22103819/1626792382072-cd4900de-108f-432a-8022-34cf9b8c77d3.png#align=left&display=inline&height=556&margin=%5Bobject%20Object%5D&originHeight=628&originWidth=780&size=0&status=done&style=none&width=690)<br />**
<a name="S04HK"></a>
### 哨兵的作用
- 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
- 当哨兵监测到master宕机,会自动将slave切换成master,然后通过**发布订阅模式**通知其他的从服务器,修改配置文件,让它们切换主机。
<a name="LD3tH"></a>
### 哨兵的核心配置
、`sentinel monitor mymaster 127.0.0.1 6379 1`
- 数字1表示 :当1个哨兵主观认为主机断开,就可以客观认为主机故障,然后开始选举新的主机。
`redis-sentinel xxx/sentinel.conf #启动哨兵模式`<br />![](https://cdn.nlark.com/yuque/0/2021/png/22103819/1626792624808-1eb36762-ea8f-4ca3-942f-8a5e5ecf4244.png#align=left&display=inline&height=437&margin=%5Bobject%20Object%5D&originHeight=582&originWidth=890&size=0&status=done&style=none&width=668)<br />此时哨兵监视着我们的主机6379,当我们断开主机后<br />![](https://cdn.nlark.com/yuque/0/2021/png/22103819/1626792682146-26043590-9819-473c-b3a0-e87d1c098e20.png#align=left&display=inline&height=391&margin=%5Bobject%20Object%5D&originHeight=736&originWidth=1255&size=0&status=done&style=none&width=666)
<a name="btBXe"></a>
### 哨兵模式优缺点
**优点**:
- 哨兵集群,基于主从复制模式,所有主从复制的优点,它都有
- 主从可以切换,故障可以转移,系统的可用性更好
- 哨兵模式是主从模式的升级,手动到自动,更加健壮
**缺点**:
- Redis不好在线扩容,集群容量一旦达到上限,在线扩容就十分麻烦
- 实现哨兵模式的配置其实是很麻烦的,里面有很多配置项
<a name="7pVF0"></a>
### 完整的哨兵模式配置文件 sentinel.conf
Example sentinel.conf
哨兵sentinel实例运行的端口 默认26379
port 26379
哨兵sentinel的工作目录
dir /tmp
哨兵sentinel监控的redis主节点的 ip port
master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符”.-_”组成。
quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
sentinel monitor
sentinel monitor mymaster 127.0.0.1 6379 1
当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
sentinel auth-pass
sentinel auth-pass mymaster MySUPER—secret-0123password
指定多少毫秒之后 主节点没有应答哨兵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无法正常启动成功。
通知脚本
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
<a name="Ju1x3"></a>
# 缓存穿透与雪崩
<a name="htdQo"></a>
### 缓存穿透(查不到)
指`缓存和数据库中都没有的数据`,导致所有的请求都打到数据库上,然后数据库还查不到(如null),造成数据库短时间线程数被打满而导致其他服务阻塞,最终导致线上服务不可用,这种情况一般来自黑客同学。
<a name="j8pSS"></a>
### 缓存击穿(量太大,缓存过期)
指`缓存中没有但数据库中有的数据`(一般是热点数据缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去查,引起数据库压力瞬间增大,线上系统卡住。<br />**解决方案:**
1. **设置热点数据永不过期:**根据实际业务情况,在Redis中维护一个热点数据表,批量设为永不过期(如top1000),并定时更新top1000数据。
1. **加互斥锁(分布式锁):**缓存击穿后,多个线程会同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁来锁住它。其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
```java
--------------------------------------加互斥锁-------------------------------------------
static Lock reenLock = new ReentrantLock();
public List<String> getData04() throws InterruptedException {
List<String> result = new ArrayList<String>();
// 从缓存读取数据
result = getDataFromCache();
if (result.isEmpty()) {
if (reenLock.tryLock()) {
try {
System.out.println("拿到锁了,从DB获取数据库后写入缓存");
// 从数据库查询数据
result = getDataFromDB();
// 将查询到的数据写入缓存
setDataToCache(result);
} finally {
reenLock.unlock();// 释放锁
}
} else {
result = getDataFromCache();// 先查一下缓存
if (result.isEmpty()) {
System.out.println("我没拿到锁,缓存也没数据,先小憩一下");
Thread.sleep(100);// 小憩一会儿
return getData04();// 重试
}
}
}
return result;
}
缓存雪崩
大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩(缓存击穿升级版)。
解决方案:
- redis高可用:多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
- 限流降级:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。
- 数据预热:我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。