redis基础
redis 默认有16个数据库,默认使用第0个,可以切换数据库
select 3 //切换第四个数据库
查看当前数据库的大小
dbsize
查看所有的key
keys *
设置键值对
set key value
移动key到1库
move key 1
判断某个key是否存在(返回1表示存在,返回0表示不存在)
exists key
为key设置过期时间
expire key 10 //10秒后过期
查看key剩余多长时间过期
ttl key
获取值
get key
查看key所存储value的类型
type key
清空当前库
flushdb
清空所有库
flushall
redis的默认端口号是6379(女明星名字的缩写)
redis是单线程的,因为redis是基于内存操作的,所以CPU不是redis性能瓶颈,Redis的性能瓶颈根据机器的内存和网络带宽有关,既然可以用单线程的化那么就用单线程了。
redis五大数据类型
String(字符串)
应用场景:value处了是string还可以是数字
1、计数器(incr/decr)
2、统计多单位数量(mset/mget+incr/decr)
3、粉丝数,关注数(incr/decr)
4、对象缓存存储(expire/setex)
set key1 v1
get key1
exists key1
keys *
append key1 "hello" //如果key1不存在则创建一个key-value,如果存在,将value的值和hello拼接 返回 v1hello
strlen key1 //获取key1对应value的值的长度 返回7(v1hello)
自动加减一(一般用于浏览量的计算)
set views 0
incr views
get views //1
decr views
decr views
get views //-1
自动加减n(步长)
incrby views 10
get views //-1+10=9
decrby views 5
get views //9-5=4
获取value的一部分值
set key1 "hello,world"
getrange key1 0 3 //获取0-3[闭区间]的字符 hell
getrange key1 0 -1 //获取所有的字符 hello,world
替换value的一部分
set key1 "hello,world"
setrange key1 1 xx //将hello,world从第一个下标位置开始替换为xx
get key1 //返回 hxxlo,world
高级
setex (set with expire) #设置key-value的同时设置过期时间
setex key1 30 "hello"
setnx (set if not exits) #不存在时设置(分布式锁时常用)
setnx key2 "redis" //返回1成功
setnx key2 "mongo" //返回0失败
批量操作
mset k1 v1 k2 v2 k3 v3 //返回ok 同时设置多个值
keys *
"k1"
"k2"
"k3"
mget k1 k2 k3 //同时获取多个值
"v1"
"v2"
"v3"
msetnx k1 v1 k4 v4 //返回0,创建失败,虽然k4不存在但是k1已经存在,现在创建失败证明了msetnx是原子性操作
巧妙实战
set user:1 {name:"zhangsan",age:12} //设置一个user:1对象的值为json字符串对象
//可以改成下面这种 user:{id}:{field}
mset user:1:username "zhangsan" user:1:age 12
先get后set
//getset key value 如果不存在key,则返回nil并将key对应的值改成value供下回返回;如果存在key,则返回当先key对应的值,将key-value保存供下次返回
getset db redis //(nil)
get db //"redis"
getset db "mongo" //"redis"
gitset db "kafka" //"mongo"
List
在redis里面,我们可以把list玩成栈、队列、阻塞队列。
所有list命令都是以l开头的
它实际上是一个链表 left和right都可以插入和删除值
如果key不存在,创建新的链表
如果key存在,新增内容
如果移除链表中所有的value,这时候就变成了空链表,也代表不存在
两边插入或者改动值时,效率最高,中间元素相对来说效率较低
用途:
消息队列
队列(两边操作相反)
栈 (两边操作一致)
存取
//插入 每次都会插入头部
lpush list one //返回1 表示list集合的大小
lpush list two //2
lpush list three //3
//取 后进去的先出来(类似栈)
lrange list 0 -1
"three"
"two"
"one"
lrange list 0 1
"three"
"two"
rpush list four //插入尾部
lrange list 0 -1
"three"
"two"
"one"
"four"
移除
lpop和rpop
lrange list 0 -1
"three"
"two"
"one"
"four"
lpop list //"three" 返回移除的元素
rpop list //"four"
lrange list 0 -1
"two"
"one"
规律:lpush和lpop或者 rpush和rpop相同方向的操作类似栈
lpush和rpop或者 rpush和lpop相反方向类似队列
获取指定下标的值(redis中list的下标从0开始)
lindex list 1
"one"
lindex list 0
"two"
返回列表的长度
Llen list //2 里面就俩数据 "two" "one"
移除列表中指定的值
flushdb
lpush list "one"
lpush list "two"
lpush list "three"
lpush list "three"
lrange list 0 -1 //
"three"
"three"
"two"
"one"
lrem list 1 one //移除list列表中从左面第一个one的值
lrem list 2 three //移除list列表中从左面两个three的值
截取list一部分(list被改变了,只剩下截取的部分)
ltrim list 0 2 //从左面截取list中0-2下标value
"three"
"three"
"two"
移除列表最后一个元素并移动到新的列表中
flushdb
rpush mylist "hello"
rpush mylist "hello1"
rpush mylist "hello2"
rpoplpush mylist newlist
lrange mylist 0 -1
"hello"
"hello1"
lrange newlist 0 -1
"hello2"
lset 将列表中指定下标的值替换另一个值(相当于更新操作)前提:列表和下标都存在
flushdb
exists list //返回0 表示不存在list
lset list 0 item //将list集合中0号下标存item,很显然会报错,因为list不存在,更别说下标的事了
lpush list values 将values存储到list集合中的0号下标
lrange list 0 0 //显示0号位置的值
lset list 0 item //将list集合中0号下标存item 返回ok
lset list 1 other //报错,因为1号下标没有创建
将某一个具体的value插入列表中某个元素的前面或者后面
flushdb
rpush list "hello"
rpush list "world"
linsert list before world hi //从左面在world之前插入一个hi 返回3代表插入后list的长度
lrange list 0 -1
"hello"
"hi"
"world"
linsert list after world new
lrange list 0 -1
"hello"
"hi"
"world"
"new"
Set(集合)无序
set中的值是不能重复的
set命令都是以s开头的
添加和查询
sadd myset "hello" //返回1表示插入成功,0表示插入失败(因为重复的值是不能插入的,可能会返回0)
sadd myset "world"
sadd myset "helloworld"
//查看
smembers myset
"hello"
"helloworld"
"world"
判断某个值是否在set集合中
sismember myset hello //返回1表示存在,0表示不存在
判断set集合的长度
scard myset //返回3
移除set集合中某个元素的值
srem myset "hello" //返回1表示移除成功,0表示移除失败
scard myset //2 返回set集合的长度
smembers myset
"helloworld"
"world"
随机从set中抽选出一个值
sadd myset "hello"
sadd myset "liu"
sadd myset "long"
smembers myset
"liu"
"long"
"hello"
"helloworld"
"world"
srandmember myset //随机返回一个值
srandmember myset 2 //随机返回set集合中的两个值
随机删除一个set中的值
spop myset //返回移除的值
随机将set1中的值移动到set2中
sadd set1 "hello"
sadd set1 "world"
sadd set2 "xixi"
sadd set2 "hehe"
smove set1 set2 "hello"
smembers set1
"world"
smembers set2
"hello"
"xixi"
"hehe"
微博、B站中的共同关注(交集)
数字集合类
差集
smembers myset
"liu"
"long"
"hello"
"helloworld"
"world"
smembers set1
"world"
sdiff myset set1 //以前面为基础减去公共的部分就是差集
"liu"
"long"
"hello"
"helloworld"
交集
sinter myset set1
"world"
并集
sunion key1 key2
"hello"
"world"
"xixi"
"hehe"
Hash(哈希)
key-Map(集合)
hash操作都是以h开头的
存值和取值
hset myhash field1 "hello" //返回1表示成功
hget myhash field1 //"hello" 返回查询的结果
//连续设置多个值
hmset myhash field1 "xixi" field2 "hehe" //返回ok表示设置成功
//一次性获取所有的value
hmget myhash field1 field2
"xixi"
"hehe"
//一次性返回所有的field和value
hgetall myhash
"field1"
"xixi"
"field2"
"hehe"
删除hash中的一个key-value
hdel myhash field1 //ok表示删除成功
hgetall myhash
"field2"
"world"
查看hash中的键值对个数
hlen myhash //1 返回的是key-value的个数
判断hash中是否有某个key
hexist myhash field2 //1表示有
hexist myhash field3 //0表示没有
查看当前所有的key
hkeys myhash
"fiekd2"
查看当前所有的value
hvals myhash
"world"
hash中某个字段加上/减去某个值
hset myhash field3 5 //1 表示插入成功
hincrby myhash field3 1 //6 返回插入后的结果值
hincrby myhash field3 -1 //5 ==》hdecrby myhash field3 1
hsetnx(如果不存在才可以创建)
hsetnx myhash field4 "hello" //1
hsetnx myhash field4 "world" //0 不存在key时才添加key-value
hash变更的书库 user :1 (name,age)
一般用于用户信息之类的、经常变更的信息
hash更适合对象的存储,string更适合字符串的存储
Zset(有序集合)
在set的基础上增加了一个值,zset k1 score v1,score越小优先级越高
zset命令都是以z开头的
添加和查看
zadd myset 1 one //1 1表示添加一个值
zadd myset 2 two 3 three //2 添加多个值
zrange myset 0 -1
"one"
"two"
"three"
用score排序
// 添加三个用户
zadd salary 2500 xiaohong
zadd salary 5000 zhangsan
zadd salary 500 kuangshen
// 按照薪水排序 zrangebyscore key min max
zrangebyscore salary -inf +inf // -∞到+∞里的所有值从小到大排序
"kuangshen"
"xiaohong"
"zhangsan"
zrangebyscore salary -inf 2500 // -∞到2500里进行排序
"kuangshen"
"xiaohong"
zrangebyscore salary -inf +inf withscore // -∞到+∞里的所有值从小到大排序,并且附带成绩
"kuangshen"
"500"
"xiaohong"
"2500"
"shangsan"
"5000"
zrevrange salary 0 -1 //倒排,从大到小排序
"shangsan"
"xiaohong"
"kuangshen"
移除元素
zrange salary 0 -1
"kuangshen"
"xiaohong"
"zhangsan"
zrem salary "xiaohong" //1表示移除成功
zrange salary 0 -1
"kuangshen"
"zhangsan"
查看有序集合中的个数
zcard salary //"kuangshen" "zhangsan"
2
计算score范围内的有多少个元素
//zcount key min max 获取指定区间的成员数量
flush db
zadd myset 1 hello 2 world 3 kuangshen
srange myset 0 -1
"hello"
"world"
"kaungshen"
zcount myset 1 2 //2 2表示符合区间的有两个元素
zcount myset 1 3 //3
用途:
存储成绩表,工资表排序
普通消息1表示,重要消息2表示 ,带权重判断
排行耪排名应用实现 topN实现
三种特殊数据类型
geospatial(地理位置)
用途:朋友的定位,附近的人,打车距离计算
redis的geo在redis3.2版本就推出了,这个功能可以计算地理位置的信息,两地之间的距离,方圆几里的人
只有6个命令
geoadd
geodist
geohash
geopos
georadius
georadiusbymember
添加
//geoadd key 经度 纬度 名称 两级(南极、北极)无法添加 下面这么多数据一般都是用程序添加,而不是手动添加
geoadd china:city 116.40 39.90 beijing
geoadd china:city 121.47 31.23 shanghai
geoadd chine:city 106.50 29.53 chongqing 114.05 22.54 shenzhen 120.16 30.24 hangzhou 108.96 34.26 xian
从key里获取给定位置的经度和纬度
单位:
- m 表示单位为米
- km 表示单位为km
- mi 表示单位为英里
- ft 表示单位为英尺
geopos china:city beijing
"116.399999896"
"39.900000"
geopos china:city beijing chongqing
两人之间的距离
geodist china:city beijing shanghai km //(默认是米) 北京到上海的直线距离
"1067.3788"
找附近的人
georadius key 经度 纬度 半径//以给定的经度纬度为中心,通过半径查询
georadius china:city 110 30 1000 km
"chongqing"
"xian"
"shenzhen"
"hangzhou"
georadius china:city 110 30 500 km withdist //另外显示出距离
"chongqing"
"341.9374"
"xian"
"483.8340"
georadius china:city 110 30 500 km withcoord //另外显示经纬度
"chongqing"
"106.96001"
"29.5299995"
"xian"
"108.960001"
"34.2599999"
//获得指定数量的人
georadius china:city 110 30 500 km withcoord count 1//另外显示经纬度
"chongqing"
"106.96001"
"29.5299995"
上面是以坐标形式查找附近的人的,下面可以以元素的名称来查找
georadiusbymember china:city beijing 1000 km // 找出距离北京1000km的城市
"beijing"
"xian"
geohash命令:返回一个或者多个位置元素的Geohash表示(了解即可)
//将经纬度(二维)转换为(一维)11位的字符串
geohash china:city beijing chongqing //如果两个字符串越像,那么距离越近
"wx4fbxxfke0"
"wm5xzrybty0"
geo的底层其实就是zset,所以查看和删除可以用zset命令
zrange china:city 0 -1
"chongqing"
"xian"
"shenzhen"
"hangzhou"
"shanghai"
"beijing"
zrem china:city beijing
zrange china:city 0 -1
"chongqing"
"xian"
"shenzhen"
"hangzhou"
"shanghai"
hyperloglog
A(1,3,5,7,8,7) B(1,3,5,7,8)
基数就是集合中不重复元素的个数 ,所以A、B两个集合的基数都是5 可接受误差为1
hyperloglog 在redis2.8.9更新了这个数据结构
redis hyperloglog 基数统计的算法
优点:占用内存是固定的,2^64不同的元素,只占用12kb的内存
用途:网站访问的次数
传统的做法,set保存用户访问的id,然后计算set集合的长度,这个方式如果保存大量用户的id时会比较麻烦,会浪费大量的内存,我们最终的目的为了计数,而不是保存这些id,在允许出错的0.81%的前提下,我们可以用hyperloglog来做,如果不允许出错就只能用set。
pfadd mykey a b c d e f g h i j //创建第一组元素
pfcount mykey //统计第一组元素数量
10
pfadd mykey2 i j z x c v b n m
pfcount mykey2
9
pfmerge mykey3 mykey mykey2 //合并两组集合(并集)
pfcount mykey3
15
bitmap(位存储)
统计用户信息(活跃/不活跃、登录/未登录、打卡/未打卡),两个状态都可以使用bitmap
bitmap 位图,是一种数据结构,操作都是以二进制记录的,只有0和1两个状态
365天=365bit 1字节=8bit 一个用户一年打卡的记录只占用46个字节左右
使用bitmap来记录周一到周日的打卡
setbit sign 0 1 //1代表打卡 0代表没打卡 周一打卡
setbit sign 1 0 //周二未打卡
setbit sign 2 0 //周三未打卡
setbit sign 3 1
setbit sign 4 1
setbit sign 5 0
setbit sign 6 0
查看周四是否打卡
getbit sign 3 //1 表示周四打卡了
统计打卡的天数
bitcount sign //3 表示全部数据中只有三天打卡了
事务
事务的本质:一组命令的集合
mysql中redis的特点:原子性、一致性、隔离性、持久性
redis中事务的特点:一次性、顺序性、排他性,redis的事务没有原子性和隔离级别的概念
所有的命令在事务中,并没有直接被执行,只有发起执行命令时才会执行!Exec
redis单条命令保证原子性的,但是事务不保证原子性
- 开启事务(multi)
- 命令入队(…)
- 执行事务(exec)
multi //ok 开启事务
set k1 v1 //queue 命令入队
set k2 v2
get v2
set k3 v3
exec //执行事务
"ok"
"ok"
"v2"
"ok"
放弃事务
multi
set k1 v1
set k2 v2
set k4 v4
//取消事务
discard
get k4
(nil)
编译时异常(代码有问题,命令出错),事务中的所有命令都不会被执行
multi //开启事务
set k1 v1 //命令入队
set k2 v2
getset k2 //(错误的命令)
(error) ERR wrong number of arguments for "getset" command
set k3 v3
exec //执行事务
(error)EXECABORT Transaction discarded because of previous errors
get k3 //所有的命令都不会执行
(nil)
运行时异常(1/0)如果事务队列中存在语法错误,那么执行命令的时候,其它命令可以正常执行,错误的命令排除异常
set k1 "v1"
multi
incr k1 //字符串不能自增
set k2 v2
set k3 v3
get v3
exec
(error) ERR value is not an integer or out of range
OK
OK
"v3"
redis实现乐观锁(监控 Watch)
悲观锁:很悲观,认为什么时候都会有问题,无论做什么都加锁
乐观锁:很乐观,认为什么时候都不会有问题,所以不会加锁,更新数据的时候去判断一下再次期间是否有人修改过这个数据。
- 获取version
- 更新的时候比较version
正常执行成功
set money 100 //设置金钱100
set out 0 //设置开销0
watch money //监视money
multi //事务正常结束,数据期间没有发生任何变动,这个时候正常执行成功
decrby money 20
incrby out 20
exec
测试多线程修改值,使用watch可以充当redis的乐观锁操作
watch money //第一个线程监控money
multi
decrby money 10 //第一个线程消费10
incrby out 10
//这个时候没有执行事务,第二个线程修改了money
incrby money 1000 //第二个线程直接给money增加1000
exec //第一个线程执行事务
(nil) 修改失败
怎么解决
//1、先放弃监控(先解锁)
unwatch
//2、获取最新的money(再次获取最新锁)
watch money
//3、开启事务,进入队列,执行事务。。。
说明:SpringBoot2.x之后,原来使用jedis被替换成lettuce了
jedis:采用的直连,多个线程操作的话是不安全的,如果想要避免不安全,可以使用jedis pool,更像BIO模式。
lettuce:采用netty,实例可以在多个线程中共享,不存在线程不安全的情况,可以减少线程数据了,更像NIO模式。
redis持久化
默认是rdb(Redis DataBase)持久化的,aof(Append Only File)默认不开启
redis是内存数据库,如果不将内存中的数据保存到磁盘中那么一旦服务器进程退出,服务器中的数据库状态也会消失,所以redis提供了持久化功能。
rdb:在指定时间间隔内将内存中的数据集体写入磁盘,也就是Snapshot快照,他恢复时是将快照文件直接读入内存中。
Redis会单独创建(fork)一个子进程进行持久化,会先将数据写入一个临时文件中,待持久化进程都结束了,再用这个临时文件替换上次的持久化文件,整个过程中,主进程是不进行任何的IO操作的,这就确保了极高的性能。如果要进行大规模的数据恢复,且对于戴护具恢复的完整性不是很敏感,那RDB方式要比AOF方式更加高效,RDB的缺点就是最后一次持久化的数据可能丢失,我们默认使用的就是RDB,一般情况下不需要修改这个配置。
-
优点
1、适合大规模的数据恢复
2、对数据的完整性要求不是很高
-
缺点
1、需要一定的时间间隔进程操作,如果redis意外宕机了,这个最后一次修改的数据就没有了
2、fork子进程的时候会占用一定的内存空间
aof:将我们所有的操作命令都记录下来,恢复的时候就把记录的命令都在执行一遍。
以日志的形式记录每个写操作,将redis执行过的所有指令记录下来(读操作不记录),换言之,redis重启之后就会根据日志文件的内容将写指令从前到后在执行一次,以恢复原理的数据。
aof的策略
1、appendfsync always //每次修改都会sync,消耗性能
2、appendfsync everysec //每秒执行一次sync,可能会丢失1s的数据
3、sppendfsync no //不执行sync,这个时候操作系统自己同步数据,速度最快
-
优点
1、每一次修改都会同步,文件完整性会更加好
2、每秒同步一次,可能会丢失一秒以内的数据,但是性能相对来说比较好
3、从不同步,性能最好
-
缺点
1、相对于数据文件来说,aof文件远远大于rdb,修复的速度也比rdb慢
2、aof运行速率也比rdb慢,所以redis默认就是rdb持久化
redis的发布订阅
Redis发布订阅(pub/sub)是一种消息通信模式,发送者(pub)发送消息,订阅者(sub)接收消息
消息订阅模型一定要有的三个角色
1、消息发送者 2、频道 3、消息订阅者
订阅端
subscribe kuangshenshuo //订阅一个频道kuangshenshuo
Reading messages... (predd Ctrl-C to quit)
1)"subscribe"
2)"kuangshenshuo"
3)(integer) 1
//等待读取信息
1)"message" //消息
2)"kuangshenshuo" //消息来自哪个频道
3)"hello,kuangshen" //消息的内容
1)"message"
2)"kuangshenshuo"
3)"hello,redis"
发布端
publish kuangshenshuo "hello,kuangshenshuo" //发布消息"hello,kuangshenshuo"到频道kuangshenshuo
(integer) 1
publish kuangshenshuo "hello,redis"
(integer) 1
使用场景
1、实时消息系统
2、实时聊天(频道当作聊天室,将信息回显给所有人即可)
3、订阅,关注系统
复杂的场景一般使用mq来做,毕竟redis主要做缓存的。
主从复制
查看当前的连接信息
info replication
主从复制一般至少需要三台服务器,我们以6379为主机,6380和6381为从机来测试
选取主节点只需要在从节点输入下面命令即可
slaveof 127.0.0.1(master的ip) 6379(master的port)
主机可以写,从机不能写只能读,主机中所有的信息和数据都会被从机自动保存。
测试:主机断开连接,从机依旧连接主机的,但是没有写操作了,这个时候如果主机回来了,从机依旧可以直接获取主机写的信息
如果只用的是命令行来配置的主从,这个时候如从机重启了就会重新变为主机,只要变回从机,立马就会从主机中同步数据
复制原理
Slave启动成功后连接到master后会发送一个sync命令,master接收到命令后启动后台的存盘进程,同时收集所有接收到的用于修改数据集的命令,在后台进程执行完毕后,master将传送整个数据文件到slave,完成一次全量同步。
全量同步:slave服务在接收到数据库文件后,将其存盘并加载到内存中
增量复制:master继续将新的所有收集到的修改命令依次传递给slave,完成同步。
但是只要是重新连接master,一次完全同步(全同步)将自动执行,我们在主机中修改的数据在从机中一定可以看到。
如果主机断开了连接,我们可以使用slave no one 让自己变成主机。
哨兵模式
主从复制技术的方法是:当主服务器宕机后,需要手动把一台服务器切换为主服务器,这就需要人工的干预,费时费力,还会造成一段时间内的服务不可用,这不是一种推荐的方式,更多时候,我们会考虑哨兵模式,redis从2.8的时候正式提出了sentinel(哨兵)来解决上面的问题
哨兵模式能够自动的监控后台主机是否出故障,如果出故障了要根据投票数自动将从机切换为主机
哨兵模式是一种特殊的模式,首先redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,他会独立运行,其原理是哨兵通过发送命令,等待redis服务器响应,从而监控运行的多个redis实例
哨兵的配置文件sentinel.conf
sentinel monitor myredis 127.0.0.1 6379 //sentinel monitor 被监控的名称(随便起) 主节点的ip 主节点的port num(多少个哨兵认为master挂了,master才算真的挂了)
启动哨兵
redis-sentinel ./sentinel.conf
哨兵模式:如果master挂了,这个时候就会自动从slave节点中选择一个当作master,以后当master在恢复的时候也只能当这个master的slave了
-
优点
1、哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
2、主从可以切换,故障可以转移,系统的可用性会更好
3、哨兵模式就是主从模式的升级,手动到自动,更加健壮
-
缺点
1、redis不好实现在线扩容,集群容量一旦到达上限,在线扩容就会十分麻烦
缓存穿透和雪崩
缓存穿透:(查询一个不存在的数据)用户想要查询一个数据,发现redis中没有,也就是缓存没有命中,于是就去持久层数据库中查询,发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,而这些请求都打到持久层数据库就会给数据库造成很大的压力,这就叫作缓存穿透。
解决:布隆过滤器、缓存空对象
缓存击穿:(一个热点key过期)是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就会穿破缓存直接请求数据库。
当某个key在过期的瞬间,有大量的并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新的数据并写入缓存,会导致数据库瞬间压力过大。
解决:1、从缓存层面来看,没有设置过期时间的话就不会出现热点key过期的问题。
2、使用分布式锁,保证对于每个key同时只有一个线程去访问后端服务,其它线程没有获得分布式锁的权限,因此,只需要等待即可。
缓存雪崩:(大量热点key过期)是指在某一个时间段缓存集中失效或者redis宕机
解决:1、数据预热,把可能的数据先访问一遍,这样部分可能大量访问就会增加到缓存中,在即将发生的大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间尽可能均匀。