文章目录
Redis入门
NoSQL
特点
- 方便扩展(数据之间没有关系,很好扩展! )
- 大数据量高性能( Redis一秒写8万次,读取11万, NoSQL的缓存记录级,是一种细粒度的缓存,性能会比较高! )
- 数据类型是多样型的! (不需要事先设计数据库!随取随用!如果是数据量十分大的表,很多人就无法设计了!)
NoSQL的四大分类
KV键值对:
- 新浪: Redis
- 美团: Redis + Tain
- 阿里、百度: Redis + memecache
文档型数据库( bson格式和json一样) :
- MongoDB ( 一般必须要掌握)
- MongoDB 是一个基于分布式文件存储的数据库, C++编写,主要用来处理大量的文档!
- MongoDB是一个个于关系型数据库和非关系型数据中中间的产品! MongoDB是非关系型数据库中功能最丰富,最像关
系型数据库的!
- ConthDB
列存储数据库
- HBase
- 分布式文件系统
图关系数据库
-
他不是存图形,放的是关系,比如:朋友圈社交网络,广告推荐!
Neo4j , InfoGrid ;
传统的RDBMS和NoSQL的区别
- 关系型数据库(RDBMS)
传统的RDBMS
-结构化组织
- SQL
-数据和关系都存在单独的表中 row col
-操作操作,数据定义语言
-严格的一致性
-基础的事务
。。。
- 非关系型数据库(NoSQL)
Nosq1
-不仅仅是数据
-没有固定的查询语言
-键值对存储,列存储,文档存储,图形数据库(社交关系)
-最终一致性,
- CAP定理和BASE(异地多活) (狂神理念: 只要学不死,就往死里学! )
-高性能,高可用,高可扩
。。。
大数据特点
大数据时代的3V :主要是描述问题的
-
海量Volume
-
多样Variety
-
实时Velocity
大数据时代的3高:主要是对程序的要求
- 高并发
- 高可拓
- 高性能(保证用户体验和性能)
Redis是什么?
Redis ( Remote Dictionary Server ) ,即远程字典服务!
是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库 ,并提供多种语言的API。
免费和开源!是当下最热门的NoSQL技术之一!也被人们称之为结构化数据库!
Redis能干嘛?
- 内存存储、持久化,内存中是断电即失、所以说持久化很重要( rdb、 aof )
- 效率高,可以用于高速缓存
- 发布订阅系统
- 地图信息分析
- 计时器、计数器(浏览量!)
- …
Redis特性和Redis的启动
特性
- 多样的数据类型
- 持久化
- 集群
- 事务
启动,关闭,查看
redis-server目录(/usr/local/bin)下输入:
redis-server myconf/redis.conf (启动redis服务端,以哪个配置启动(自己选))
redis-cli (-h,默认就是本机,可以不写) -p 6379 用哪个机器哪个端口号连redis服务
ps -ef|grep redis: 查看所有redis进程信息
shutdown:连接状态下6379,关闭redis服务(服务端和客户端都关闭)
exit:退出
测试性能
redis-benchmark是一个压力测试工具!
官方自带的性能测试工具!
redis-benchmark命令参数!|
测试: 100个并发连接1000000请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
Redis基础知识
redis默认有16个数据库
默认使用第0个
命令
set key value :设置键值
get key:获取某一个键的值
keys:查看所有键
flushdb:清空当前数库内容
flushall:清空所有数据库内容
127.0.0.1:6379> select 2 # 选择使用哪个数据库(从0开始)
OK
127.0.0.1:6379[2]> DBSIZE # 查看当前数据库的大小(大小写都可以)
(integer) 0
127.0.0.1:6379[2]>
Redis为什么单线程还这么快?
redis是单线程的
- 误区1 :高性能的服务器一定是多线程的?
- 误区2:多线程( CPU上下文会切换! ) 一定比单线程效率高!
核心:redis是将所有数据放在内存的,所以使用单线程去操作效率就是最高的,多线程(CPU上文切换:耗时的操作!!!)
对于内存系统来说,如果没有上下文切换效率就是最高的!多次读写下都是在一个CPU上的,在内存情况下,这个就是最佳方案!
Redis五大数据类型
Redis中关于Key的一些命令
127.0.0.1:6379> type name # 查看key的类型
string
127.0.0.1:6379> set age 11 # 设置一个key-value
OK
127.0.0.1:6379> del age # 删除key(成功返回1)
(integer) 1
127.0.0.1:6379> move name 1 # 移动key到两外一个数据库(成功返回1)
(integer) 1
127.0.0.1:6379> exists name(存在返回1,不存在返回0)
(integer) 0
127.0.0.1:6379> keys * (查看当前数据库的所有key值)
(empty array)
127.0.0.1:6379> set name kuangshen
OK
127.0.0.1:6379> expire name 20 # 设置key的过期时间(需要先有这个key),秒,还有一种写法setex直接设置key-value和过期时间
(integer) 1
127.0.0.1:6379> ttl name # 查看过期时间(秒)
(integer) 16
127.0.0.1:6379> ttl name
(integer) 14
127.0.0.1:6379> ttl name
(integer) 13
127.0.0.1:6379> ttl name
(integer) 12
127.0.0.1:6379> get name # 通过key获取value值
"kuangshen"
127.0.0.1:6379>
String(字符串)
127.0.0.1:6379> set name hello # 设置key-value
OK
127.0.0.1:6379> get nam
(nil)
127.0.0.1:6379> get name # 通过key获取value
"hello"
127.0.0.1:6379> keys * # 查看当前数据库下所有key
1) "name"
127.0.0.1:6379> exists name # 查看key时候存在(存在返回1,不存在返回0)
(integer) 1
# 追加字符串到key后面(返回值为字符串的长度),如果不存在这个key,就相当于set key value
127.0.0.1:6379> append name dingshuhua
(integer) 15
127.0.0.1:6379> get name
"hellodingshuhua"
127.0.0.1:6379> strlen name # 通过key获取value对应的长度
(integer) 15
127.0.0.1:6379>
------------------------------------------------------------------------------------------------------------
# 类似步长 i++!
127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views # views自增1(返回值为views的值)
(integer) 1
127.0.0.1:6379> get views # 原来是0现在变成了1
"1"
127.0.0.1:6379> incr views
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> decr views # views自减1 (返回值为views的值)
(integer) 1
127.0.0.1:6379> get views # 原来的2变成了1
"1"
127.0.0.1:6379> incrby views 5 设置自增步长,指定增量
(integer) 6
127.0.0.1:6379> incrby views 10
(integer) 16
127.0.0.1:6379> decrby views 3 设置自减步长,指定减量
(integer) 13
127.0.0.1:6379>
------------------------------------------------------------------------------------------------------------
# 字符串范围!
127.0.0.1:6379> set test kuangshen,nihao
OK
127.0.0.1:6379> getrange test 0 3 # 截取字符串[0,3]
"kuan"
127.0.0.1:6379> getrange test 0 -1 # 获取全部字符串,和get key 是一样的
"kuangshen,nihao"
127.0.0.1:6379> set test2 abcdefg
OK
# 替换!
127.0.0.1:6379> setrange test2 3 xxxxxx # 替换指定位置开始的字符串
(integer) 9
127.0.0.1:6379> get test2
"abcxxxxxx"
127.0.0.1:6379>
------------------------------------------------------------------------------------------------------------
# setex (set with expire) # 设置key并设置过期时间
# setnx (set if not exist) # 不存在再设置(在分布式锁中会经常使用!)
127.0.0.1:6379> setex key 30 value
OK
127.0.0.1:6379> ttl key
(integer) 25
127.0.0.1:6379> get key
"value"
127.0.0.1:6379> setnx key1 "redis" # 如果key1不存在,则会创建key1
(integer) 1
127.0.0.1:6379> keys *
1) "key1"
127.0.0.1:6379> ttl key1
(integer) -1
127.0.0.1:6379> setnx key1 "MongDB" # 如果key1存在,则创建失败!
(integer) 0
127.0.0.1:6379> get key1
"redis"
------------------------------------------------------------------------------------------------------------
# mset 批量设置key-value
# mget 批量获取value
127.0.0.1:6379> mset key1 value1 key2 value2 key3 value3 # 批量设置
OK
127.0.0.1:6379> keys *
1) "key3"
2) "key1"
3) "key2"
127.0.0.1:6379> mget key1 key2 key3 # 批量获取
1) "value1"
2) "value2"
3) "value3"
127.0.0.1:6379> msetnx key1 value1 key5 value5 # msetnx 是一个原子性的操作,要么一起成功,要么一起失败
(integer) 0
127.0.0.1:6379> get key5
(nil) # 失败,获取不到key5
------------------------------------------------------------------------------------------------------------
# 对象
set user:1{name:kuangshen,age:13} # 设置一个user:1 对象 值为 json字符来保存一个对象!
127.0.0.1:6379> mset user:1:name kuangshen user:1:age 13 # 相当于user:1:name是一个整体的key
OK
127.0.0.1:6379> mget user:1:name user:1:age # 用整体的key来获取
1) "kuangshen"
2) "13"
------------------------------------------------------------------------------------------------------------
127.0.0.1:6379> getset db redis # 先get再set ,没有get到就返回nil
(nil)
127.0.0.1:6379> get db # 上面set了,所以就能拿到
"redis"
127.0.0.1:6379> getset db mongdb # 先拿get就是redis ,然后set为mongdb
"redis"
127.0.0.1:6379> get db # 这边拿到的就是mongdb
"mongdb"
数据结构是相通的!
String类似的使用场景: value除了是我们的字符串还可以是我们的数字!
- 计数器
- 统计多单位的数量
- 粉丝数
- 对象缓存存储!
List(列表)
再redis里面,可以把列表当成 栈,队列,阻塞队列!
127.0.0.1:6379> lpush list one # 将一个值或多个值,插入到列表头部(左)
(integer) 1
127.0.0.1:6379> lpush list two
(integer) 2
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> lrange list 0 -1 # 获取list中的值,类似于栈,先插入的在下面
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> lrange list 0 1 # 通过区间获取具体的值
1) "three"
2) "two"
127.0.0.1:6379> rpush list right # 将一个值或多个值,插入到列表尾部(右)
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "right"
------------------------------------------------------------------------------------------------------------
# lpop 移除(左)
# rpop 移除(右)
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "right"
127.0.0.1:6379> lpop list # 移除(左)
"three"
127.0.0.1:6379> rpop list # 移除(右)
"right"
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
------------------------------------------------------------------------------------------------------------
# llen 查看list长度
127.0.0.1:6379> llen list # 查看list的长度
(integer) 2
------------------------------------------------------------------------------------------------------------
# 移除指定的值 lrem
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> lrem list 1 one # 移除1个为one的值
(integer) 1
127.0.0.1:6379> lrem list 1 two
(integer) 1
127.0.0.1:6379> lrange list 0 -1
(empty array)
127.0.0.1:6379> lpush list one one one
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "one"
2) "one"
3) "one"
127.0.0.1:6379> lrem list 2 one # 移除2个为one的值
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "one"
------------------------------------------------------------------------------------------------------------
# trim ,修剪 list ,截断!
127.0.0.1:6379> lpush list "hello" "hello1" "hello2" "hello3"
(integer) 4
127.0.0.1:6379> ltrim list 1 2 # 截取list [1,2] 只保留截取的部分,list被改变了
OK
127.0.0.1:6379> lrange list 0 -1
1) "hello2"
2) "hello1" # 可以看出原list被改变了
------------------------------------------------------------------------------------------------------------
# rpoplpush # 移除列表中最后一个元素,将他移动到新的列表中!
127.0.0.1:6379> rpoplpush list newlist # 移动list中最后一个元素到newlist中
"hello1"
127.0.0.1:6379> lrange list 0 -1
1) "hello2"
127.0.0.1:6379> lrange newlist 0 -1
1) "hello1"
------------------------------------------------------------------------------------------------------------
# lset 将列表中指定下标的值替换为另外一个值,更新操作
127.0.0.1:6379> exists list # 判断这个列表是否存在
(integer) 0
127.0.0.1:6379> lset list 0 item # 如果不存在列表更新会报错!
(error) ERR no such key
127.0.0.1:6379> lpush list value1
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "value1"
127.0.0.1:6379> lset list 0 item # 如果存在更新当前下标的值
OK
127.0.0.1:6379> lset list 1 other # 如果下表不存在也会报错!
(error) ERR index out of range
------------------------------------------------------------------------------------------------------------
# linsert # 将某个具体的value插入到列表中某个元素的前面或者后面
127.0.0.1:6379> lpush list "hello" "world"
(integer) 2
127.0.0.1:6379> linsert list after "world" "insert"
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "world"
2) "insert"
3) "hello"
小结
- 他实际上是一一个链表, before Node after ,left , right都可以插入值
- 如果key不存在,创建新的链表
- 如果key存在,新增内容
- 如果移除了所有值,空链表,也代表不存在!
- 在两边插入或者改动值,效率最高!中间元素,相对来说效率会低一 -点~
消息排队!消息队列(Lpush Rpop),栈( Lpush Lpop )
Set(集合)
set中的值是不重复的!
127.0.0.1:6379> sadd myset "hello" "world" "kuangshen" #set中添加元素
(integer) 3
127.0.0.1:6379> smembers myset # 查看指定set所有的值
1) "hello"
2) "kuangshen"
3) "world"
127.0.0.1:6379> sismember myset hello # 判断某一个值是否在set中
(integer) 1
127.0.0.1:6379> sismember myset hello33
(integer) 0
------------------------------------------------------------------------------------------------------------
127.0.0.1:6379> scard myset # 获取集合中元素的个数
(integer) 3
------------------------------------------------------------------------------------------------------------
#srem
127.0.0.1:6379> smembers myset
1) "hello"
2) "kuangshen"
3) "world"
127.0.0.1:6379> srem myset hello # 移除指定元素
(integer) 1
127.0.0.1:6379> smembers myset
1) "kuangshen"
2) "world"
------------------------------------------------------------------------------------------------------------
# 随机获取
127.0.0.1:6379> srandmember myset # 随机获取set中的1个元素
"kuangshen"
127.0.0.1:6379> srandmember myset
"hello"
127.0.0.1:6379> srandmember myset 3 # 随机获取set中的元素,指定个数为3个
1) "dingshuhua"
2) "kuangshen"
3) "world"
------------------------------------------------------------------------------------------------------------
# 删除指定的key,随机删除key
127.0.0.1:6379> spop myset # 随机删除set中的一个元素
"hello"
127.0.0.1:6379> spop myset
"world"
127.0.0.1:6379> smembers myset
1) "dingshuhua"
2) "kuangshen"
3) "ganshudan"
------------------------------------------------------------------------------------------------------------
# 将一个指定的值移动到另一个set集合
127.0.0.1:6379> sadd set1 test1 test11 test111
(integer) 3
127.0.0.1:6379> sadd set2 test2 test22 test222
(integer) 3
127.0.0.1:6379> smove set1 set2 test1 # 移动set1中的test1到set2中
(integer) 1
127.0.0.1:6379> smembers set1
1) "test111"
2) "test11"
127.0.0.1:6379> smembers set2 # 可以看出来,移动成功
1) "test1"
2) "test222"
3) "test22"
4) "test2"
------------------------------------------------------------------------------------------------------------
# 微博,b站,共同关注(并集) ,差集
127.0.0.1:6379> sadd key1 "a" "b" "c"
(integer) 3
127.0.0.1:6379> sadd key2 "b" "c" "d"
(integer) 3
127.0.0.1:6379> sdiff key1 key2 # 差集,key1比key2多出来的
1) "a"
127.0.0.1:6379> sinter key1 key2 # 交集
1) "b"
2) "c"
127.0.0.1:6379> sunion key1 key2 # 并集
1) "b"
2) "c"
3) "a"
4) "d"
Hash(哈希)
Map集合,key-map! 相当于这个值是一个map集合! 本质上和String没有太大的区别,还是一个简单的key-value!
比较适合存对象
127.0.0.1:6379> hset myhash name kuangshen # 设置一个具体的key-value
(integer) 1
127.0.0.1:6379> hget myhash name # 获取一个字段值
"kuangshen"
127.0.0.1:6379> hmset myhash age 13 sex nan # 设置多个 key-value
OK
127.0.0.1:6379> hmget myhash name sex age # 获取多个字段值
1) "kuangshen"
2) "nan"
3) "13"
127.0.0.1:6379> hgetall myhash # 获取全部的数据
1) "name"
2) "kuangshen"
3) "age"
4) "13"
5) "sex"
6) "nan"
127.0.0.1:6379> hdel myhash name # 删除hash指定的key字段,对应的value值也消失了
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "age"
2) "13"
3) "sex"
4) "nan"
------------------------------------------------------------------------------------------------------------
# hlen 查看hash中有几对key-value
127.0.0.1:6379> hlen myhash
(integer) 2
------------------------------------------------------------------------------------------------------------
127.0.0.1:6379> hexists myhash age # 查看hash中字段时候存在
(integer) 1
127.0.0.1:6379> hexists myhash ttt
(integer) 0
------------------------------------------------------------------------------------------------------------
127.0.0.1:6379> hkeys myhash # 获取所有key
1) "age"
2) "sex"
127.0.0.1:6379> hvals myhash # 获取所有value
1) "13"
2) "nan"
------------------------------------------------------------------------------------------------------------
# incr decr
127.0.0.1:6379> hset myhash key 4
(integer) 1
127.0.0.1:6379> hincrby myhash key 1 # 指定增量1
(integer) 5
127.0.0.1:6379> hincrby myhash key -1 # 指定增量-1
(integer) 4
Zset(有序集合)
在set基础上,增加了一个值,set key value || zset key score value
127.0.0.1:6379> zadd myset 1 one # 添加一个值
(integer) 1
127.0.0.1:6379> zadd myset 2 two 3 three # 添加多个值
(integer) 2
127.0.0.1:6379> zrange myset 0 -1
1) "one"
2) "two"
3) "three"
------------------------------------------------------------------------------------------------------------
# 排序
127.0.0.1:6379> zadd salary 2599 xiaoming # 添加三个用户
(integer) 1
127.0.0.1:6379> zadd salary 5499 zhangsan
(integer) 1
127.0.0.1:6379> zadd salary 1999 kuangshen
(integer) 1
127.0.0.1:6379> zrangebyscore salary -inf +inf # 显示全部用户,从小到大(按照score) -inf(负无穷) +inf(正无穷)
1) "kuangshen"
2) "xiaoming"
3) "zhangsan"
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores # 显示全部用户,并显示对应的score
1) "kuangshen"
2) "1999"
3) "xiaoming"
4) "2599"
5) "zhangsan"
6) "5499"
127.0.0.1:6379> zrangebyscore salary -inf 2500 withscores # 显示score小于2500的用户
1) "kuangshen"
2) "1999"
------------------------------------------------------------------------------------------------------------
# zrem :移除元素
127.0.0.1:6379> zrem salary xiaoming # 移除元素
(integer) 1
127.0.0.1:6379> zrange salary 0 -1 # 此时zset中就没有xiaoming
1) "kuangshen"
2) "zhangsan"
------------------------------------------------------------------------------------------------------------
127.0.0.1:6379> zadd myset 1 hello
(integer) 1
127.0.0.1:6379> zadd myset 2 world 3 kuangshen
(integer) 2
127.0.0.1:6379> zcount myset 1 2 # 获取指定区间的成员数量
(integer) 2
案列思路: set排序 存储班级成绩表,工资表排序!
普通消息:1 重要消息:2 ,带权重进行判断!
三种特殊类型
geospatial(地理位置)
朋友的定位,附近的人,打车距离计算?
Redis的Geo在Redis3.2版本就推出了!这个功能可以推算地理位置的信息,两地之间的距离,方圆几里的人!
可以查询一些测试数据: http://www.jsons.cn/lngcodeinfo/0706D99C19A781A3/
共六个命令
getadd 添加
# 规则:两极无法直接添加,一般会下载城市数据,直接通过java程序一次性导入
# 参数key 值()
127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing
(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 chongqing 114.05 22.52 shenzheng
(integer) 2
127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou 108.96 34.26 xian
(integer) 2
getpos
127.0.0.1:6379> geopos china:city beijing # 获取指定城市的经纬度
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
127.0.0.1:6379> geopos china:city chongqing
1) 1) "106.49999767541885376"
2) "29.52999957900659211"
geodist
两点之间距离!
单位:
- m表示单位为米。
- km表示单位为千米。
- mi表示单位为英里。
- ft表示单位为英尺。
127.0.0.1:6379> geodist china:city beijing chongqing km # 北京到重庆的距离(km)
"1464.0708"
127.0.0.1:6379> geodist china:city shanghai chongqing km # 上海到重庆的距离(km)
"1447.6737"
georadius 以给定的经纬度为中心,找出某一半径内的元素
我附近的人? ( 获得所有附近的人的地址.定位! ) 通过半径来查询!
获得指定数量的人, 200
所有数据都应该录入:china:city ,才会让结果更加清晰!
127.0.0.1:6379> georadius china:city 110 30 1000 km # 以110,30这个点为中心的,方圆1000km的城市
1) "chongqing"
2) "xian"
3) "shenzheng"
4) "hangzhou"
127.0.0.1:6379> georadius china:city 110 30 500 km # 以110,30这个点为中心的,方圆500km的城市
1) "chongqing"
2) "xian"
127.0.0.1:6379> georadius china:city 110 30 500 km withdist # 并加上距离
1) 1) "chongqing"
2) "341.9374"
2) 1) "xian"
2) "483.8340"
127.0.0.1:6379> georadius china:city 110 30 500 km withcoord # 并加上定位信息
1) 1) "chongqing"
2) 1) "106.49999767541885376"
2) "29.52999957900659211"
2) 1) "xian"
2) 1) "108.96000176668167114"
2) "34.25999964418929977"
127.0.0.1:6379> georadius china:city 110 30 1000 km count 1 # 筛选指定结果,只要1个结果
1) "chongqing"
georadiusbymember
# 找出位于指定元素周围的其他元素!
127.0.0.1:6379> georadiusbymember china:city beijing 1000 km # 北京方圆1000km的城市
1) "beijing"
2) "xian"
geohash 返回一个或多个位置元素的geohash
# 将二维的经纬度转换为一维的字符串,如果两个字符串越接近,那么距离越近!!
127.0.0.1:6379> geohash china:city beijing chongqing
1) "wx4fbxxfke0"
2) "wm5xzrybty0"
geo 底层的实现原理其实就是Zset!可以用Zset的命令来操作geo!
127.0.0.1:6379> zrange china:city 0 -1 # 查找所有元素
1) "chongqing"
2) "xian"
3) "shenzheng"
4) "hangzhou"
5) "shanghai"
6) "beijing"
127.0.0.1:6379> zrem china:city beijing # 删除指定的元素
(integer) 1
Hyperloglog
什么是基数?
A {1,2,2,3,3,5} 基数 = 4 相当于不重复的元素个数
简介
Redis 2.8.9版本就更新了Hyperloglog数据结构!
Redis Hyperloglog基数统计的算法!
优点:占用的内存是固定, 2^64不同的元素的技术,只需要废12KB内存!如果要从内存角度来比较的话Hyperloglog首选!
网页的UV (一个人访问一个网站多次,但是还是算作一个人! )
传统的方式,set 保存用户的id ,然后就可以统计set中的元素数量作为标准判断!
这个方式如果保存大量的用户id ,就会比较麻烦!我们的目的是为了计数,而不是保存用户id ;
0.81%错误率!统计UV任务,可以忽略不计的!
测试使用
127.0.0.1:6379> pfadd key1 a a b c d e f g g g g # 创建第一组元素 key1
(integer) 1
127.0.0.1:6379> pfcount key1 # 统计key1元素的基数
(integer) 7
127.0.0.1:6379> pfadd key2 i j k l m n n n n n # 创建第二组元素 key2
(integer) 1
127.0.0.1:6379> pfcount key2
(integer) 6
127.0.0.1:6379> pfmerge key3 key1 key2 # 合并两组 key1+key2 -- > key3
OK
127.0.0.1:6379> pfcount key3 # 并集的结果(个数)
(integer) 13
Bitmap
位存储
统计用户信息,活跃,不活跃!登录、未登录!打卡, 365打卡!两个状态的,都可以使用Bitmaps !
Bitmap位图,数据结构!都是操作二进制位来进行记录,就只有0和1两个状态!
365天=365bit 1字节=8bit 46 个字节左右!|
# 使用bitmap来记录 周一到周日的打卡
127.0.0.1:6379> setbit sign 0 1 # 周一
(integer) 0
127.0.0.1:6379> setbit sign 1 1
(integer) 0
127.0.0.1:6379> setbit sign 2 0
(integer) 0
127.0.0.1:6379> setbit sign 3 1 # 周四
(integer) 0
127.0.0.1:6379> setbit sign 4 1
(integer) 0
127.0.0.1:6379> setbit sign 5 1
(integer) 0
127.0.0.1:6379> setbit sign 6 0 # 周日
(integer) 0
127.0.0.1:6379> getbit sign 6 # 查看某一天是否打卡
(integer) 0
127.0.0.1:6379> getbit sign 5
(integer) 1
127.0.0.1:6379> bitcount sign # 查看这周的打卡记录,就可以看到是否全勤
(integer) 5
事务
Redis事务本质: 一组命令的集合!一个事务中的所有命令都会被序列化,在事务执行过程的中,会按照顺序执行!
一次性、顺序性、排他性!执行一-些列的命令!
------ 队列set set set执行------
Redis事务没有没有隔离级别的概念!
所有的命令在事务中,并没有直接被执行!只有发起执行命令的时候才会执行! Exec
Redis单条命令是保存原子性的,但是事务不保证原子性!
redis的事务:
-
开启事务( multi )
-
命令入队( …一些命令集合 )
-
执行事务( exec )
正常执行事务!
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379(TX)> set key1 value1
QUEUED
127.0.0.1:6379(TX)> set key2 value2
QUEUED
127.0.0.1:6379(TX)> set key3 value3
QUEUED
127.0.0.1:6379(TX)> exec # 执行事务
1) OK
2) OK
3) OK
放弃事务!
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379(TX)> set key4 value4
QUEUED
127.0.0.1:6379(TX)> set key5 value5
QUEUED
127.0.0.1:6379(TX)> discard # 取消事务
OK
127.0.0.1:6379> get key4 # 因为取消了事务(事务队列中的命令都不会执行),所以查询不到key4
(nil)
编译型异常(代码有问题 ! 命令有错 ! ) , 事务中所有的命令都不会执行 !
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set key1 value1
QUEUED
127.0.0.1:6379(TX)> set key2 value2
QUEUED
127.0.0.1:6379(TX)> set key3 value3
QUEUED
127.0.0.1:6379(TX)> getset key3 # 错误的命令!
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379(TX)> set key4 value4
QUEUED
127.0.0.1:6379(TX)> exec # 执行事务报错!
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get key4 # 所有的命令都不会执行1
(nil)
运行时异常 ( 1/0), 如果事务队列中存在语法性,那么执行命令的时候,其他命令是可以正常执行的,错误命令抛出异常!
127.0.0.1:6379> set key1 "value1"
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incr key1
QUEUED
127.0.0.1:6379(TX)> set key2 value2
QUEUED
127.0.0.1:6379(TX)> get key2
QUEUED
127.0.0.1:6379(TX)> exec
1) (error) ERR value is not an integer or out of range # 虽然第一条命令错了,但是依然能执行成功
2) OK
3) "value2"
127.0.0.1:6379> get key2
"value2"
监控! Watch
悲观锁:
- 很悲观,认为什么时候都会出问题,无论做什么都会加锁!
乐观锁:
-
很乐观,认为什么时候都不会出问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过这个数据,
-
获取version
-
更新的时候比较version
Redis监视测试 !
正常执行成功!
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money # 监视money对象
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 20
QUEUED
127.0.0.1:6379(TX)> incrby out 20
QUEUED
127.0.0.1:6379(TX)> exec # 事务正常结束,数据期间没有发生变动,这个时候正常执行成功!
1) (integer) 80
2) (integer) 20
测试多线程修改值,使用watch可以当作Redis的乐观锁操作 !
127.0.0.1:6379> watch money # 监视 money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby money 10
QUEUED
127.0.0.1:6379(TX)> incrby out 10
QUEUED
127.0.0.1:6379(TX)> exec # 执行之前另外一个线程,修改了我们的值,这个时候,就会导致事务执行失败 ! 如果money这个值在事务提交之前没有被改变,则这个事务会执行成功
(nil)
# 还没提交前,这一个线程,已经修改了money的值
127.0.0.1:6379> get money
"80"
127.0.0.1:6379> set money 1000
OK
Jedis
我们要使用Java来操作Redis
什么是Jedis是Redis官方推荐的java连接开发工具!使用ava操作Redis中间件!如果你要使用java操作redis ,那么一定要
对Jedis十分的熟悉!
关于Jedis连接远程Redis(阿里云)
-
阿里云安全组开放6379端口
-
Linux开启防火墙,不拦截6379这个端口,关闭防火墙
- systemctl status firewalld : 查看防火墙状态
-
systemctl start firewalld:开启防火墙
- systemctl stop firewalld: 关闭防火墙
-
firewall-cmd --zone=public --add-port=6379/tcp --permanent :不拦截6379这个端口,即开放6379这个端口
-
Redis启动的配置文件redis.conf
- 注释bind127.0.0.1,没有请求访问的ip限制了(原来只能在本地访问)
- protected-mode改为no,即可以开启远程访问
- daemonize 设置为 yes 表明要在后台运行
测试远程Redis的连接
出现PONG说明连接成功 !
Jedis部分方法,和原生的Redis一模一样
SpringBoot整合
SpringBoot操作数据: spring-data jpa jdbc mongodb redis !
SpringData也是和SpringBoot齐名的项目!
说明:在SpringBoot2.x之后,原来底层使用的jedis被替换为了lettuce?
jedis :采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的,使用jedis pool 连接池! BIO
lettuce :采用netty ,实例可以再多个线程中进行共享,不存在线程不安全的情况!可以减少线程数据了,更像Nio 模式
引入SpringData-redis依赖
RedisTemplate模板操控redis
在企业中,所有的对象都需要序列化,传输对象
为了方便开发,一般会自己定义RedisTemplate模板 一般使用**<String,Object>**,方便开发
企业中还会自己编写RedisUtils工具类,方便开发,原生的需要先 (例如:opsForValue(),操控String类型的) 创建对象,然后点方法,工具类,直接点方法调用
Redis.conf详解
网络
bind 127.0.0.1 #绑定的ip
protected-mode yes #保护模式
port 6379 #端口设置
通用GENERAL
daemonize yes #以守护进程的方式运行,默认是no,我们需要自己开启为yes!
pidfile /var/run/redis_ 6379.pid # 如果以后台的方式运行,我们就需要指定一个pid文件!
1og1eve1 notice
1ogfile "" #日志的文件位置名
databases 16 #数据库的数量,默认是16个数据库
always-show-1ogo yes #是否总是显示LOGO
快照
持久化,在规定的时间内,执行了多少次操作,则会持久化到文件.rdb. aof
redis是内存数据库,如果没有持久化,那么数据断电及失!
#如果900s内,如果至少有一个1 key进行了修改,我们及进行持久化操作
save 900 1
#如果300s内,如果至少10 key进行 了修改,我们及进行持久化操作
save 300 10
#如果60s内,如果至少10000 key进行了修改,我们及进行持久化操作
save 60 10000
#学习持久化,会自己定义这个测试!
stop-writes-on-bgsave-error yes # 持久化如果出错,是否还需要继续工作!
rdbcompression yes #是否压缩rdb文件,需要消耗一些cpu资源!
rdbchecksum yes #保存rdb 文件的时候,进行错误的检查校验!
dir ./ # rdb文件保存的目录!
SECURITY安全
可以在这里设置redis的密码,默认是没有密码!
127.0.0.1:6379> ping
PONG
127.0.0. 1:6379> config get requirepass #获取redis的密码
1) "requirepass"
2) ""
127.0.0.1:6379> config set requirepass "123456" # 设置redis的密码
0K
127.0.0.1:6379> config get requirepass # 发现所有命令都没有权限了
(error) NOAUTH Authentication required .
127.0.0.1:6379> ping
(error) NOAUTH Authentication required.
127.0.0.1:6379> auth 123456 # 使用密码进行登录!
OK
127.0.0. 1:6379> config get requirepass
3) "requi repass"
4) "123456 "
限制CLIENTS
maxclients 10000 # 设置能连接上redi s的最大客户端的数量
maxmemory <bytes> # redis配置最大的内存容量
maxmemory-policy noeviction # 内存到达上限之后的处理策略
1、volatile-Tru: 只对设置了过期时间的key进行LRU (默认值)
2、al1keys-1ru :删除1ru算法的key
3、volatile-random: 随机删除即将过期key
4、al1keys-random: 随机删除
5、volatile-tt1 :删除即将过期的
6、noeviction :永不过期,返回错误 # 默认
APPEND ONLY 模式 aof配置
appendonly no #默认是不开启aof模式的,默认是使用rdb方式持久化的,在大部分所有的情况下,rdb完全够用!
appendfilename "appendonly.aof" #持久化的文件的名字
# appendfsync always #每次修改都会sync。 消耗性能
appendfsync everysec #每秒执行一次sync, 可能会丢失这1s的数据!
# appendfsync no #不执行sync, 这个时候操作系统自己同步数据,速度最快!
Redis持久化
Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所
以Redis提供了持久化功能!
什么是RDB
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。
Redis会单独创建( fork )一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文
件替换上次持久化好的文件。整个过程中,主进程是不进行任何I0操作的。这就确保了极高的性能。如果需要进行大规模数据的恢
复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢
失。
默认就是RDB,一般不需要修改配置
rdb保存的文件是dump.rdb都是在我们的配置文件中快照中进行配置的!
rdb触发机制
- 配置文件中的save保存时间和执行次数
优点:
-
适合大规模的数据恢复!
-
对数据的完整性要不高!
缺点:
-
需要一定的时间间隔进程操作!如果redis意外宕机了,这个最后一次修改数据就没有的了!
-
fork进程的时候,会占用一定的内容空间! !
AOF(Append Only File)
将我们的所有命令都记录下来, history ,恢复的时候就把这个文件全部在执行一遍!
什么是AOF
以日志的形式来记录每个写操作, 将Redis执行过的所有指令记录下来(读操作不记录) , 只许追加文件但不可以改写文件, redis
启动之初会读取该文件重新构建数据,换言之, redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复
工作
默认是不开启的,我们需要手动进行配置!我们只需要将appendonly改为yes就开启了aof !
重启, redis就可以生效了!
如果这个aof文件有错位,这时候redis 是启动不起来的吗,我们需要修复这个aof文件
redis给我们提供了一个工具 redis-check-aof --fix(被修复的那一条写操作也会丢失)
appendonly no #默认是不开启aof模式的,默认是使用rdb方式持久化的,在大部分所有的情况下,rdb完全够用!
appendfilename "appendonly.aof" #持久化的文件的名字
# appendfsync always #每次修改都会sync。 消耗性能
appendfsync everysec #每秒执行一次sync, 可能会丢失这1s的数据!
# appendfsync no #不执行sync, 这个时候操作系统自己同步数据,速度最快!
如果aof文件大于64m,太大了! fork一个新的进程来将我们的文件进行重写!
重写就是把能合并的命令合并,比如多次操作一个key,就可以合并成一个,无效的key的操作就可以全部删除。
优点和缺点
优点:
- 每一次修改都同步,文件的完整会更加好!
- 每秒同步一次,可能会丢失一秒的数据
- 从不同步,效率最高的!
缺点:
- 相对于数据文件来说, aof远远大于rdb ,修复的速度也比rdb慢!
- Aof 运行效率也要比rdb慢,所以我们redis默认的配置就是rdb持久化!
Redis发布订阅
Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。微信、微博、关注系统!
Redis客户端可以订阅任意数量的频道。
订阅/发布消息图:
第一个:消息发送者,第二个:频道第三个:消息订阅者!
命令
这些命令被广泛用于构建即时通信应用,比如网络聊天室(chatroom)和实时广播、实时提醒等。
测试
订阅端:
# 会一直监听订阅的频道,一旦发布端口发布消息,订阅端会立马收到消息
127.0.0.1:6379> subscribe kuangshenshuo # subscribe 频道名
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "kuangshenshuo"
3) (integer) 1
# 等待读取推送的信息
1) "message" # 消息
2) "kuangshenshuo" # 频道名
3) "hello,success!!" # 消息的具体内容
发布端:
127.0.0.1:6379> publish kuangshenshuo "hello,success!!" # publish 频道名 消息
(integer) 1
127.0.0.1:6379
Redis主从复制
概念
主从复制,是指将一台Redis服务器的数据 ,复制到其他的Redis服务器。前者称为主节点(master/leader) ,后者称为从节点
(slave/follower) ;数据的复制是单向的,只能由主节点到从节点。Master以写为主 , Slave以读为主。
默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点) ,但一个从节点只能有一个主节点。
主从复制的作用主要包括:
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
- 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
- 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接
主节点,读Redis数据时应用连接从节点) , 分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大
大提高Redis服务器的并发量。 - 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
主从复制,读写分离! 80% 的情况下都是在进行读操作!减缓服务器的压力!架构中经常使用! 一主二从!
环境配置
只配置从库,不用配置主库! 默认都是master(主库)
127.0.0.1:6379> info replication
# Replication
role:master # 角色,主任
connected_slaves:0 # 从库,0
master_failover_state:no-failover
master_replid:6b4de07bc9a56c0b9f05f633a8215bfdfe16db63
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
搭建一个伪集群
复制3个配置文件,然后修改对应的信息
1、端口
2、pid 名字
3、log文件名字
4、dump.rdb 名字
主机可以写,从机只能读
真实的从主配置应该在配置文件中配置,这样的话是永久的,我们这里使用的是命令,暂时!
slaveof ip prot # 从属于哪一个主人,这是暂时的,命令
主机断开连接,从机依旧连接到主机的,但是没有写操作,这个时候,主机如果回来了,从机依旧可以直接获取到主机写的信息!,主机依旧是主机
从机断开:如果是使用命令行,来配置的主从,这个时候如果重启了,就会变回主机!只要变为从机,立马就会从主机中获取值!
复制原理
Slave启动成功连接到master后会发送一个sync同步命令
Master接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后, master将传送
整个数据文件到slave ,并完成一次完全同步。
全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
增量复制: Master继续将新的所有收集到的修改命令依次传给slave ,完成同步(主机操作新增数据的时候)
但是只要是重新连接master ,一次完全同步 (全量复制)将被自动执行
如果主机断开了连接,我们可以使用slaveof no one让自己变成主机!其他的节点就可以手动连接到最新的这个主节点(手
动),哨兵模式没出来之前,这时候原来的主机回来,手下就没有从机的
哨兵模式
(自动选举老大的模式) 原来是手动
主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成
一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel (哨
兵)架构来解决这个问题。
谋朝篡位的自动版,能够后台监控主机是否故障,如果故障了根据投票数****自动将从库转换为主库。
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通
过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
作用:
-
通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
-
当哨兵监测到master宕机,会自动将slave切换成master ,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它
们切换主机。
然而一个哨兵进程对Redis服务器进行监控,可能会出现问题(哨兵宕机了),为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
假设主服务器宕机工哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个
现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结
果由一个哨兵发起,进行failover[故障转移]操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切
换主机,这个过程称为客观下线。
1、配置哨兵配置文件sentinel.conf
# sentine1 monitor 被监控的名称(随意写) host port 1 监测这个端口(主机),它也知道主机下有哪些从机,一旦主机崩了,从机就会被选为主机
sentine1 monitor myredis 127.0.0.1 6379 1
启动: server.sentinel 配置文件路径 (跟启动redis服务和redis客户端差不多)
这里的1是指只要有1个哨兵认为主服务器已经下线,主服务器就会被判定为客观下线
如果主机此时回来了,只能归并到新的主机下,当做从机,这就是哨兵模式的规则!
哨兵模式
优点:
- 哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
- 主从可以切换,故障可以转移,系统的可用性就会更好
- 哨兵模式就是主从模式的升级,手动到自动,更加健壮!
缺点:
- Redis 不好在线扩容的,集群容量一旦到达上限,在线扩容就十分麻烦!
- 实现哨兵模式的配置其实是很麻烦的,里面有很多选择
Redis的缓存穿透和雪崩
服务器的高可用问题
Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。 其中,最要害的
问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。
缓存穿透(查不到)
概念
缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发
现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中( 秒杀 ),于是都去请求了持久层数据库。这会给持久层数据库造成很
大的压力,这时候就相当于出现了缓存穿透。
解决方案
布隆过滤器
布隆过滤器是一 种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存
储系统的查询压力;
缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护
了后端数据源;
但是这种方法会存在两个问题:
- 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
- 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会
有影响。
缓存击穿(量太大了,缓存过期!)
概述
这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一一个点进行访问,当
这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数
据,并且回写缓存,会导使数据库瞬间压力过大。
解决方案:
设置热点数据永不过期本
从缓存层面来看,没有设置过期时间,所以不会出现热点key过期后产生的问题。
加互斥锁
分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要
等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。
缓存雪崩(缓存失效,Redis宕机)
概念
缓存雪崩,是指在某一个时间段,缓存集中过期失效。例如:Redis宕机
产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购 ,这波商品时间比较集中的放入了缓
存,假设缓存一个小时。 那么到了凌晨一点钟的时候 ,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库
上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂
掉的情况。
双十一 :停掉一些服务,(保证主要的服务可用!) 服务降级,(例如:暂时关闭退款服务)
解决方案
redis高可用
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis ,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集
群。
限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查
询数据和写缓存,其他线程等待。
数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即
将发生大并发访问前手动触发加载缓存不同的key ,设置不同的过期时间,让缓存失效的时间点尽量均匀。
来源于:遇见狂神说,Redis教学视频
视频链接: 【狂神说Java】Redis最新超详细版教程通俗易懂.