Redis基础知识
redis默认有16个数组库,默认使用第0个数据库 命令: select:进行切换数据库 flushdb:清除当前数据 flushall:清空所有数据库
redis在6之前是单线程的,它的瓶颈在于内存和带宽 redis为什么单线程还这么快? 1、误区1:高性能的服务器一定是多线程的? 2、误区2:多线成(cpu上下文会切换!)一定比单线程效率高?
cpu->内存->硬盘的速度要有所了解 核心:redis是将所有的数据全部放在内存中的,所以说使用单线程去操作效率就是最高的,多线程(CPU上下文会切换!,耗时),对于内存系统来说如果没有上下文切换的效率就是高!多次读写都是在一个cpu上的,在内存情况下,这个就是最佳的方案。
Redis-key
keys *:查看所有的key set xx xx:设置key exists xx:判断当前的key是否存在 move xx 1:移动当前key到1数据库中 get xx:获取key expire xx 10:设置key的过期时间为10s ttl:查看当前key的剩余时间 type:查看当前key的类型
String(字符串): APPEND key ”xxx“:追加字符串,如果当前key不存在则相当于setkey incr key:加1 decr key:减1 incrby key 10:key1的数值加10 decrby key 10:key1的数值减10
================================================== #字符串范围 range getrange key 0 3:截取key的第0到第3个字符
#替换 setrange key 1 xx:替换key中第一个字符串开始为xx
127.0.0.1:6379> get key
"abc"
127.0.0.1:6379> setrange key 1 xx
(integer) 3
127.0.0.1:6379> get key
"axx"
setex(set with expire) #设置过期时间
127.0.0.1:6379> setex key1 5 "abcas"
OK
127.0.0.1:6379> ttl key1
(integer) 1
127.0.0.1:6379> get key1
(nil)
setnx(set fi not exist) #不存在设置
127.0.0.1:6379> setnx key1 "redis"
(integer) 1
127.0.0.1:6379> get key1
"redis"
127.0.0.1:6379> setnx key1 "asd"
(integer) 0
127.0.0.1:6379> get key1
"redis"
mset:同时设置多个值
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3
OK
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
3) "k3"
mget: msetnx:原子性操作,要么一起成功要么一起失败
127.0.0.1:6379> keys *
1) "k2"
2) "k1"
3) "k3"
127.0.0.1:6379> msetnx k1 v1 k4 v4
(integer) 0
127.0.0.1:6379> get k4
(nil)
==================================================
getset:先get再set
127.0.0.1:6379> getset key abc
(nil)
127.0.0.1:6379> get key
"abc"
127.0.0.1:6379> getset key aaa
"abc"
127.0.0.1:6379> get key
"aaa"
数据结构是相同的 String类似的使用场景:value除了是我们的字符串还可以是数字! 比如:一个视频的播放量
List
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
1) "three"
2) "two"
3) "one"
可以看出list的底层是链表:拿出来的时候是倒序排列
==================================================
rpush:从list右边插入 lpush:从list左边插入 lrange:通过区间获取具体的值
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:移除list的第一个元素 rpop:移除list的最后一个元素
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"
lindex:通过下标获取list的某一个值
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> lindex list 1
"one"
127.0.0.1:6379> lindex list 0
"two"
llen:返回列表长度
==================================================
127.0.0.1:6379> llen list
(integer) 2
==================================================
lrem:移除list中指定个数的value,精确匹配
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "three"
3) "two"
4) "one"
127.0.0.1:6379> lrem list 1 one
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "three"
3) "two"
127.0.0.1:6379> lrem list 2 three
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "two"
==================================================
trim:通过下标,截取指定的长度(该操作是截断,代表被截断的value不再存在于list中)
127.0.0.1:6379> lpush mylist "hello1"
(integer) 1
127.0.0.1:6379> lpush mylist "hello2"
(integer) 2
127.0.0.1:6379> lpush mylist "hello3"
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "hello3"
2) "hello2"
3) "hello1"
127.0.0.1:6379> ltrim mylist 1 2
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "hello2"
2) "hello1"
==================================================
rpop lpush:移动list的最后一个元素,并添加该元素到新的list
127.0.0.1:6379> lpush mylist "hello1"
(integer) 1
127.0.0.1:6379> lpush mylist "hello2"
(integer) 2
127.0.0.1:6379> lpush mylist "hello3"
(integer) 3
127.0.0.1:6379> rpoplpush mylist myotherlist
"hello1"
127.0.0.1:6379> lrange myotherlist 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 value
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "value"
127.0.0.1:6379> lset list 0 item
OK
127.0.0.1:6379> lrange list 0 -1
1) "item"
==================================================
linsert:将某个具体的value插入到list中某个元素的前面
127.0.0.1:6379> lrange list 0 -1
1) "world"
2) "hello"
127.0.0.1:6379> LINSERT list before hello abc
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "world"
2) "abc"
3) "hello"
127.0.0.1:6379> LINSERT list after hello xxx
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "world"
2) "abc"
3) "hello"
4) "xxx"
小结: 1.它实际上是一个双向链表 2.如果key不存在,创建新的链表 3.如果key存在,新增内容 4.如果移除了所有值,空链表,也表示不存在 在两边插入或者改动值,效率更高!中间元素,相对来说效率会低一点
==================================================
set
set中的值不能重复!
==================================================
sadd:添加元素 SMEMBERS:查看set的所有值 SISMEMBER:查看set中是否存在指定值
127.0.0.1:6379> sadd set hello
(integer) 1
127.0.0.1:6379> sadd set hello2
(integer) 1
127.0.0.1:6379> sadd set hello3
(integer) 1
127.0.0.1:6379> SMEMBERS set
1) "hello"
2) "hello3"
3) "hello2"
127.0.0.1:6379> SISMEMBER set hello
(integer) 1
127.0.0.1:6379> SISMEMBER set hello4
(integer) 0
================================================== scard:获取set中的个数
127.0.0.1:6379> sadd set hello #hello已存在,不能重复添加
(integer) 0
127.0.0.1:6379> sadd set hello4
(integer) 1
127.0.0.1:6379> scard set
(integer) 4
================================================== srem:移除set中的指定元素
127.0.0.1:6379> SMEMBERS set
1) "hello"
2) "hello4"
3) "hello3"
4) "hello2"
127.0.0.1:6379> srem set hello
(integer) 1
127.0.0.1:6379> SMEMBERS set
1) "hello4"
2) "hello3"
3) "hello2"
==================================================
set是无序不重复集合,抽随机 SRANDMEMBER:随机抽选出指定个数的元素
127.0.0.1:6379> SRANDMEMBER set
"hello2"
127.0.0.1:6379> SRANDMEMBER set
"hello3"
127.0.0.1:6379> SRANDMEMBER set
"hello2"
127.0.0.1:6379> SRANDMEMBER set 2
1) "hello2"
2) "hello4"
127.0.0.1:6379> SRANDMEMBER set 2
1) "hello3"
2) "hello4"
================================================== spop:随机移除一个元素
127.0.0.1:6379> SMEMBERS set
1) "hello4"
2) "hello3"
3) "hello2"
127.0.0.1:6379> spop set
"hello4"
127.0.0.1:6379> spop set
"hello2"
================================================== smove:移动一个set中的指定元素到另一个set中
127.0.0.1:6379> sadd set h1
(integer) 1
127.0.0.1:6379> sadd set h2
(integer) 1
127.0.0.1:6379> sadd set h3
(integer) 1
127.0.0.1:6379> smove set myset h1
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "h1"
127.0.0.1:6379> SMEMBERS set
1) "h2"
2) "h3"
==================================================
b站,微博的共同关注(并集) 数字集合类: 差集 SDIFF 交集 SINTER 并集 SUNION
127.0.0.1:6379> sadd key1 a
(integer) 1
127.0.0.1:6379> sadd key1 b
(integer) 1
127.0.0.1:6379> sadd key1 c
(integer) 1
127.0.0.1:6379> sadd key2 c
(integer) 1
127.0.0.1:6379> sadd key2 d
(integer) 1
127.0.0.1:6379> sadd key2 e
(integer) 1
SDIFF:以第一个set为例与第二个set对比,返回不同的value(差集)
127.0.0.1:6379> SDIFF key1 key2
1) "a"
2) "b"
==================================================
SINTER:以第一个set为例与第二个set对比,返回相同的value(交集)
127.0.0.1:6379> SINTER key1 key2
1) "c"
==================================================
SUNION:以第一个set为例与第二个set对比,返回所有不重复的value(差集)
127.0.0.1:6379> SUNION key1 key2
1) "a"
2) "c"
3) "e"
4) "b"
5) "d"
==================================================
Hash(哈希) Map集合,key-map集合!本质和String类型没有太大差别,还是一个简单的key-value
127.0.0.1:6379> hset hash filed1 "hello" #添加key-value (filed1-hello)
(integer) 1
127.0.0.1:6379> hget hash filed1 #获取指定key的value
"hello"
127.0.0.1:6379> hmset hash filed1 world filed2 abc
OK
127.0.0.1:6379> hmget hash filed1 filed2
1) "world"
2) "abc"
127.0.0.1:6379> hgetall hash #获取所有key-value
1) "filed1"
2) "world"
3) "filed2"
4) "abc"
================================================== hdel:删除hash指定的key-value字段,
127.0.0.1:6379> hdel hash filed1
(integer) 1
127.0.0.1:6379> hgetall hash
1) "filed2"
2) "abc"
================================================== hlen:获取hash表的所有字段
127.0.0.1:6379> hset hash field1 hello field2 abc
(integer) 2
127.0.0.1:6379> hgetall hash
1) "field1"
2) "hello"
3) "field2"
4) "abc"
127.0.0.1:6379> hlen hash
(integer) 2
================================================== HEXISTS:判断hash中指定key字段是否存在
127.0.0.1:6379> HEXISTS hash field1
(integer) 1
================================================== hkeys:只获取指定hash的所有字段的key hvals:只获取指定hash的所有字段的value
127.0.0.1:6379> hkeys hash
1) "field1"
2) "field2"
127.0.0.1:6379> hvals hash
1) "hello"
2) "abc"
==================================================
hincrby:把指定hash的field自增一个值数值 注意:hash没有hdecrby方法(自减方法,如果要自减则用hincrby方法在数值那里加上一个减号)
hsetnx:和String一样,判断是否存在该hash字段,存在则不创建,不存在则创建
127.0.0.1:6379> hset hash field3 5
(integer) 1
127.0.0.1:6379> hincrby hash field3 1
(integer) 6
127.0.0.1:6379> hincrby hash field3 -1
(integer) 5
127.0.0.1:6379> hsetnx hash field4 xxx
(integer) 1
127.0.0.1:6379> hsetnx hash field4 xxx
(integer) 0
==================================================
hash变更的数据user name age,尤其是是用户信息之类的,经常变动的信息! hash更适合于对象的存储,String更加适合字符串存储!
Zset(有序集合)
在set的基础上,增加了一个值,set k1 v1 | zset k1 score1 v1
127.0.0.1:6379> zadd set 1 one
(integer) 1
127.0.0.1:6379> zadd set 2 two
(integer) 1
127.0.0.1:6379> zrange set 0 -1
1) "one"
2) "two"
================================================== zrangebyscore:按参数排序
127.0.0.1:6379> zadd salary 2500 xiaohong
(integer) 1
127.0.0.1:6379> zadd salary 5000 zhangsan
(integer) 1
127.0.0.1:6379> zadd salary 500 dong
(integer) 1
127.0.0.1:6379> zrangebyscore salary -inf +inf
1) "dong"
2) "xiaohong"
3) "zhangsan"
================================================== zrangebyscore xx withscores:把详细信息也获取到 注意zrangebyscore只能从小到大不能大到小
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores
1) "dong"
2) "500"
3) "xiaohong"
4) "2500"
5) "zhangsan"
6) "5000"
==================================================
zrem:移除元素
127.0.0.1:6379> zrange salary 0 -1
1) "dong"
2) "xiaohong"
3) "zhangsan"
127.0.0.1:6379> zrem salary dong
(integer) 1
127.0.0.1:6379> zrange salary 0 -1
1) "xiaohong"
2) "zhangsan"
==================================================
zcard:获取有序集合中的个数
==================================================
三种特殊数据类型
geospatial地理空间
朋友的定位,附近的人,打车距离计算? Redis的Geo在Redis3.2版本就推出了!这个功能可以推算出地理位置信息,两地之间的距离,方圆几里的人 http://www.jsons.cn/lngcode/ 只有六个命令 GEOADD GEODIST GEOHASH GEOPOS GEORADIUS GEORADIUSBYMEMBER
geoadd:添加地理位置 注意:规则:两级无法直接添加,我们一般会下载城市数据,直接通过java程序一次性导入! 参数:key 值(经度、纬度、名称,不能乱换位置)
127.0.0.1:6379> geoadd china:city 114.05 22.52 shenzhen
(integer) 1
127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou
(integer) 1
127.0.0.1:6379> geoadd china:city 108.96 34.26 xian
(integer) 1
==================================================
geopos:获取指定城市的经度纬度 获取当前定位,一定是个坐标值!
127.0.0.1:6379> geopos china:city shenzhen
1) 1) "114.04999762773513794"
2) "22.5200000879503861"
127.0.0.1:6379> geopos china:city shenzhen hangzhou
1) 1) "114.04999762773513794"
2) "22.5200000879503861"
2) 1) "120.1600000262260437"
2) "30.2400003229490224"
==================================================
geodist: 两人之间的距离: 单位: m 表示单位为米。 km 表示单位为千米。 mi 表示单位为英里。 ft 表示单位为英尺。
127.0.0.1:6379> geodist china:city shenzhen hangzhou
"1052108.2563"
127.0.0.1:6379> geodist china:city shenzhen hangzhou km
"1052.1083"
==================================================
georadius: 附近的人?(获取所有附近的人的地址,定位!)
127.0.0.1:6379> georadius china:city 110 30 1000 km #显示指定坐标方圆1000km的城市
1) "xian"
2) "shenzhen"
3) "hangzhou"
127.0.0.1:6379> georadius china:city 110 30 1000 km withdist #显示到指定坐标距离的位置
1) 1) "xian"
2) "483.8340"
2) 1) "shenzhen"
2) "924.6408"
3) 1) "hangzhou"
2) "977.5143"
127.0.0.1:6379> georadius china:city 110 30 1000 km withdist withcoord #显示定位信息
1) 1) "xian"
2) "483.8340"
3) 1) "108.96000176668167114"
2) "34.25999964418929977"
2) 1) "shenzhen"
2) "924.6408"
3) 1) "114.04999762773513794"
2) "22.5200000879503861"
3) 1) "hangzhou"
2) "977.5143"
3) 1) "120.1600000262260437"
2) "30.2400003229490224
127.0.0.1:6379> georadius china:city 110 30 1000 km withdist withcoord count 1 #筛选指定结果
1) 1) "xian"
2) "483.8340"
3) 1) "108.96000176668167114"
2) "34.25999964418929977"
==================================================
GEORADIUSBYMEMBER:找出指定元素周围的其他元素
127.0.0.1:6379> GEORADIUSBYMEMBER china:city shenzhen 1000 km
1) "shenzhen"
================================================== geohash:返回一个或多个位置元素(返回结果为11个字符的geohash字符串)
127.0.0.1:6379> geohash china:city shenzhen hangzhou
1) "ws10578st80"
2) "wtmkn31bfb0"
geo的底层其实就是Zset!我们可以通过Zset命令来操作geo
127.0.0.1:6379> zrange china:city 0 -1
1) "xian"
2) "shenzhen"
3) "hangzhou"
127.0.0.1:6379> zrem china:city xian
(integer) 1
127.0.0.1:6379> zrange china:city 0 -1
1) "shenzhen"
2) "hangzhou"
Hypoerloglog
什么是基数? A{1,3,5,7,8,7} B{1,3,5,7,8} 基数(不重复的元素)=> 5个,可以接受误差
简介 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 key a b c d e f g h i j
(integer) 1
#统计第一组元素数量
127.0.0.1:6379> pfcount key
(integer) 10
127.0.0.1:6379> pfadd key2 i j z x c v b n m
(integer) 1
127.0.0.1:6379> pfcount key2
(integer) 9
#合并key和key2结果为key3
127.0.0.1:6379> pfmerge key3 key key2
OK
#合并相同元素后得到最后结果
127.0.0.1:6379> pfcount key3
(integer) 15
Bitmap
位存储: 统计用户信息,活跃,不活跃!登录、未登录!打卡,365打卡!两个状态的,都可以使用Bitmaps !
Bitmaps位图,数据结构!都是操作二进制位来进行记录,就只有0和1两个状态
例子:一周中打卡的天书 1为:打卡,0:没打卡
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 0
(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 1
(integer) 0
查看某一天是否打卡?
127.0.0.1:6379> getbit sign 3
(integer) 0 #没打卡
127.0.0.1:6379> getbit sign 4
(integer) 1 #打卡
统计当周打卡的天数的记录
127.0.0.1:6379> bitcount sign
(integer) 5
Redis基本事务
Redis单条命令保证原子性,但是事务不保证原子性! Redis事务的本质:一组命令的集合!一个事务中的所有命令都会被序列化,在事务执行过程中,会按照顺序执行! 一次性、顺序性、排他性! Redis事务没有隔离级别的概念! 所有的命令在事务中,并没有直接被执行!只有发起执行命令的时候才会执行!Exec
redis的事务: 1.开启事务(multi) 2.命令入队() 3.执行事务()
正常执行事务:
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK
2) OK
3) "v2"
4) OK
放弃事务:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> discard #取消事务
OK
127.0.0.1:6379> get k4 #因为取消了事务,所以没有k4存在
(nil)
编译型异常(代码有问题!命令有错),事务中所有的命令都不会被执行!
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> getset k3
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k4
(nil)
127.0.0.1:6379> get k1
(nil)
运行时异常(1/0),如果事务队列中存在语法性,那么执行命令的时候,其他命令可以正常执行 错误命令抛出异常
127.0.0.1:6379> set k1 "v1"
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incr k1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> get k3
QUEUED
127.0.0.1:6379(TX)> exec
1) (error) ERR value is not an integer or out of range
2) OK
3) OK
4) "v3"
悲观锁: 很悲观,什么时候都会出问题,无论做什么都会加锁! 乐观锁: 很乐观,什么时候都不会出问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过这个数据
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
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
Jedis
Java操作Redis 什么是Jedis:官方推荐的Java连接开发工具!使用Java操作Redis中间件 测试: 1、导入依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.6.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
2、编码测试: 2.1:连接数据库 2.2.:操作命令 2.3:断开连接
SpringBoot整合Jedis
源码分析
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
整合测试: 1、导入依赖 2、配置连接 3、测试
package com.dong;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
@SpringBootTest
class Redis02SpringbootApplicationTests {
@Autowired
RedisTemplate redisTemplate;
@Test
void contextLoads() {
redisTemplate.opsForValue().set("key","dong");
System.out.println(redisTemplate.opsForValue().get("key"));
}
}
Redis.conf详解
启动的时候,就通过配置文件来启动!
1、配置文件对单位大小写不敏感 2、可以包含其他配置文件 3、网络配置 bind 127.0.0.1 #绑定的ip protected-mode yes #保护模式 port 6379 #端口设置
4、通用GENERAL daemonize yes #以守护进程的方式运行,默认是no,我们需要自己开启为yes !
pidfile /var/run/redis_6379.pid #如果以后台的方式运行,我们就需要指定一个pid 文件! #日志 #specify the server verbosity leve1. #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 " " #日志的文件位置名 databases 16 #数据库的数量,默认是 16个数据库always-show-logo yes #是否总是显示logo
5、快照配置 持久化,在规定的时间内,执行了多少次操作,则会持久化到文件.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文件保存的目录!
REPLICATION 复制,跟主从复制有关
SERCURITY安全 可以在这里设置redis的密码,默认是没有密码!
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) ""
127.0.0.1:6379> config set requirepass "123456"
OK
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "123456"
127.0.0.1:6379> auth 123456
OK
限制 CLIENTS maxclients 10000 #设置能连接上redis的最大客户端的数量 maxmemory #redis 配置最大的内存容量 maxmemory-policy noeviction # 内存到达上限之后的处理策略 1、volatile-lru:只对设置了过期时间的key进行LRU(默认值) 2、allkeys-lru :删除lru算法的key 3、volatile-random:随机删除即将过期key 4、a1lkeys-random:随机删除 5、volatile-ttl :删除即将过期的 6、noeviction :永不过期,返回错误
APPEND ONLY模式 aof配置 appendonly no #默认是不开启aof模式的,默认是使用rdb方式持久化的,在大部分所有的情况下,rdb完全够 用! appendfilename “appendon1y.aof” #持久化的文件的名字
#appendfsync always#每次修改都会 sync。消耗性能
appendfsync everysec #每秒执行一次 sync,可能会丢失这1s的数据!
#appendfsync no #不执行 sync,这个时候操作系统自己同步数据,速度最快!
Redis持久化 面试和工作,持久化都是重点
Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以 Redis提供了持久化功能!
RDB(Redis DataBase) 在主从复制中,rdb就是备用的,在从机这块
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。 Redis会单独创建(( fork )一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。默认使用RDB,一般情况下不需要修改
RDB保存的文件时 dump.rdb 1、save的规则满足的情况下,会自动触发rdb规则 2、执行flushall命令,也会触发我们的rdb规则! 3、退出redis,也会产生rdb 文件! 备份就自动生成一个dump.rdb
如何恢复rdb文件 1、只需要将rdb文件放在我们redis启动目录就可以,redis启动的时候会自动检查dump.rdb恢复其中的数据! 2、查看需要存在的位置
127.0.0.1:6379> config get dir
1) "dir"
2) "/usr/local/bin"
结论:rdb默认配置已经够用了, 优点: 1、适合大规模的数据恢复! 2、对数据的完整性要不高!
缺点: 1、需要一定的时间间隔进程操作!如果redis意外宕机了,这个最后一次修改数据就没有的了! 2、fork进程的时候,会占用一定的内存空间!!
AOF(Append Only File)
将我们的所有命令都记录下来,history,恢复的时候就把这个文件全部在执行一遍!
以日志的形式来记录每个写的操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
只需要把配置文件的appendonly-no 改为yes即开启,其他默认配置则不需要改!重启redis就可以生效
如果这个aof文件有错位,这时候redis是启动不起来的吗,我们需要修复这个aof文件redis 给我们提供了一个工具redis-check-aof --fix
优点和缺点 重写规则说明: 如果aof文件大于64m,它fork一个新的进程来将文件进行重写 优点: 1、每一次修改都同步,文件的完整会更加好! 2、每秒同步一次,可能会丢失一秒的数据 3、从不同步,效率最高 缺点: 1、相对于数据文件来说,aof远远大于rdb,修复的速度也比rdb慢 2、aof运行效率也不要比rdb慢,所以我们redis默认的配置就是rdb持久化!
扩展 1、RDB持久化方式能够在指定的时间间隔内对你的数据进行快照存储
2、AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以Redis 协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。
3、只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化
4、同时开启两种持久化方式 1.在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。 2.RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件,那要不要只使用AOF呢?作者建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有AOF可能潜在的Bug,留着作为一个万一的手段。
5、性能建议
1.因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save 9001这条规则。
2.如果Enable AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了,代价一是带来了持续的IO,二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上,默认超过原大小100%大小重写可以改到适当的数值。
3.如果不Enable AOF,仅靠Master-Slave Repllcation实现高可用性也可以,能省掉一大笔IO,也减少了rewrite时带来的系统波动。代价是如果MasterlSlave同时倒掉,会丢失十几分钟的数据,启动脚本也要比较两个Master/Slave 中的RDB文件,载入较新的那个,微博就是这种架构。
Redis发布订阅
Redis 发布订阅(pub/sub)是一种消息通信模式︰发送者(pub)发送消息,订阅者(sub)接收消息。 Redis客户端可以订阅任意数量的频道。 订阅/发布消息图︰ 测试 先订阅subscribe
127.0.0.1:6379> SUBSCRIBE dong
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "dong"
3) (integer) 1
然后再开一个远程连接进行发布 publish
127.0.0.1:6379> PUBLISH dong "hello"
(integer) 1
发送成功后查看订阅端的结果
127.0.0.1:6379> SUBSCRIBE dong
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "dong"
3) (integer) 1
1) "message"
2) "dong"
3) "hello"
Redis主从复制
概念 主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(masterleader),后者称为从节点(slave/follower);数据的复制是单向的,只能由主节点到从节点。Master以写为主,Slave以读为主。
默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
主从复制的作用主要包括: 1、数据冗余∶主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。 2、故障恢复∶当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。 3、负载均衡︰在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。 4、高可用基石∶除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的(宕机),原因如下: 1、从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大; 2、从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G,也不能将所有内存用作Redis存储内存,一般来说,单台Redis最大使用内存不应该超过20G。 电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是"多读少写"。对于这种场景,我们可以使如下这种架构︰
主从复制,读写分离! 80%的情况下都是在进行读操作!减缓服务器的压力!架构中经常使用!一主二从
环境配置
查看配置信息
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:d005fcaf5f804bc6efe39059f0ff4d725bd3ecea
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
一主二从
默认情况下,美抬Redis服务器都是主节点;我们一般情况只用配置从机就好了!我们需要手动设置一台机器为主机 slaveof 127.0.0.1(对应的ip) 6379(主机端口号)
127.0.0.1:6380> slaveof 127.0.0.1 6379
OK
127.0.0.1:6380> info replication
# Replication
role:slave #当前角色
master_host:127.0.0.1 #主机ip
master_port:6379 #主机端口号
master_link_status:up
master_last_io_seconds_ago:2
master_sync_in_progress:0
slave_repl_offset:0
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:bcb4fa81209751864bc0a1290542132ad6099179
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:0
主机只写
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> keys *
1) "k1"
从机只读
127.0.0.1:6380> keys *
1) "k1"
127.0.0.1:6380> set k2 v2
(error) READONLY You can't write against a read only replica.
127.0.0.1:6380> get k1
"v1"
如果主机断开连接,从机依旧是连接到主机,但还是没有写入操作。如果主机又连回来了,从机依然可以读取到主机写入的信息。 如果使用命令行配置的主从机,从机断开连接默认变回主机,但只要变回从机,依然可以读取到主机的内容
Slave启动成功连接到 master后会发送—个sync命令
Master接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。
全量复制︰而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步。但是只要是重新连接master,一次完全同步(全量复制)将被自动执行
哨兵模式
概述 主从切换技术的方法是︰当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel (哨兵)架构来解决这个问题。
谋朝篡位的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
这里的哨兵有两个作用 1.通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。 2.当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。
然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。 假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的车果由一个哨兵发起,进行failover[故障转移]操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。
测试 状态:一主二从 1、先配置sentinel.conf
#sentinel monitor 被监控的名称 host port 1
sentinel monitor myredis 127.0.0.1 6379 1
后面的这个数字1,代表主机挂了,slave投票看让谁接替成为主机,票数最多的,就会成为主机 !
2、启动哨兵
[root@localhost bin]# redis-sentinel kconfig/sentinel.conf
13159:X 22 Jul 2021 15:06:38.137 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
13159:X 22 Jul 2021 15:06:38.137 # Redis version=6.2.4, bits=64, commit=00000000, modified=0, pid=13159, just started
13159:X 22 Jul 2021 15:06:38.137 # Configuration loaded
13159:X 22 Jul 2021 15:06:38.138 * Increased maximum number of open files to 10032 (it was originally set to 1024).
13159:X 22 Jul 2021 15:06:38.138 * monotonic clock: POSIX clock_gettime
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 6.2.4 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in sentinel mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 26379
| `-._ `._ / _.-' | PID: 13159
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | https://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
13159:X 22 Jul 2021 15:06:38.157 # Sentinel ID is 5fccdbe702e14bf784488a8e4bc30819c83f8427
13159:X 22 Jul 2021 15:06:38.157 # +monitor master myredis 127.0.0.1 6379 quorum 1
13159:X 22 Jul 2021 15:06:38.158 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
13159:X 22 Jul 2021 15:06:38.159 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
如果主机断开连接,哨兵模式自动后台进行选举,推票数最高的从机作为主机。但是如果主机再次重新连接回来,并不会取代之前的主机,反而成为新主机的从机。
优点: 1、哨兵集群,基于主从复制模式,所有的主从配置优点,它全有 2、主从可以切换,故障可以转移,系统的可用性就会更好 3、哨兵模式就是主从模式的升级,手动到自动,更加健壮! 缺点: 1、Redis 不好啊在线扩容的,集群容量一旦到达上限,在线扩容就十分麻烦! 2、实现哨兵模式的配置其实是很麻烦的,里面有很多选择!
烧饼模式全部配置:
# 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 <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 1
# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000
# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
这个数字越小,完成failover所需的时间就越长,
但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs <master-name> <numslaves>
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 <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,
#这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,
#一个是事件的类型,
#一个是事件的描述。
#如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无法正常启动成功。
#通知脚本
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh
# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# 目前<state>总是“failover”,
# <role>是“leader”或者“observer”中的一个。
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
Redis缓存穿透和雪崩(高频)
缓存穿透的概念 缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造:大的压力,这时候就相当于出现了缓存穿透。
布隆过滤器 对所有可能查询的参数以Hash的形式存储,以便快速确定是否存在这个值,在控制层先进行拦截校验,校验不通过直接打回,减轻了存储系统的压力。
2.缓存空对象 一次请求若在缓存和数据库中都没找到,就在缓存中方一个空对象用于处理后续这个请求。
1、如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键; 2、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响
缓存击穿(量太大,缓存过期) 概述: 这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。
解决方案 1.设置热点数据永不过期 这样就不会出现热点数据过期的情况,但是当Redis内存空间满的时候也会清理部分数据,而且此种方案会占用空间,一旦热点数据多了起来,就会占用部分空间。
2.加互斥锁(分布式锁) 在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。保证同时刻只有一个线程访问。这样对锁的要求就十分高。
缓存雪崩
缓存概念
大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。
解决方案
redis高可用 这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群
限流降级 这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
数据预热 数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
狂神说Java 视频:https://www.bilibili.com/video/BV1S54y1R7SB?p=36&spm_id_from=pageDriver
|