不仅仅是为了面试与工作去学习,要出于兴趣----
如果在使用Typora的时候,出现全屏的情况,按一下F11即可
我们现在是大数据时代,一般的数据库无法进行分析处理,现在至少需要springboot、springcloud, 压力会越来越大,一定要逼着自己学习,这是在社会上的生存法则。
单机MySQL年代
用户通过访问APP,app到达DAL(数据库访问层),然后再去访问MySQL;原来90年代的时候,单个数据库足够了;更多的是静态网页Html。
瓶颈:
Memcached(缓存)+MySQL+垂直拆分(读写分离)
我们可能有多个数据库,让一台数据库承担写的角色,其他的承担读的角色。然而网站80%是在读,所以每次去查询,就会麻烦,所以我们希望减轻数据库的压力,因此使用缓存(Cache)来保证效率!
发展过程:优化数据结构和索引–>文件缓存(IO操作)–>Memcached(以前最为热门的技术)
大厂不缺人,缺少人才。
分库分表+水平拆分+MySQL集群
如今最近的年代
如今信息量井喷式增长,各种各样的数据出现(用户定位数据,图片数据等),大数据的背景下关系型数据库(RDBMS)无法满足大量数据要求。Nosql数据库就能轻松解决这些问题。
目前一个基本的互联网项目
Nosql
Nosql,不仅仅是数据库(SQL)
关系型数据库:表格,行,列
Nosql泛指费关系型数据库,在超大规模的高并发下,诞生了当今Nosql的快速发展趋势,Redis是发展最快的技术
”Boss招聘“,
很多数据类型(用户的个人信息,社交网络,地理位置,这些数据类型的存储,不需要一个固定的格式),不需要多月的操作就能横向拓展! 例如:Map <String, Object>,使用键值对来控制
Nosql特点
了解:3V 和 3高
大数据时代的3V:主要是描述问题
大数据时代的3高:主要是对程序的要求
在公司中实践:Nosql+RDBMS 一起使用才是最强的,阿里巴巴的架构演进!
真正在公司中的实践:NoSQL + RDBMS 一起使用才是最强的。
推荐阅读:阿里云的这群疯子https://yq.aliyun.com/articles/653511
# 商品信息 - 一般存放在关系型数据库:Mysql,阿里巴巴使用的Mysql都是经过内部改动的。 # 商品描述、评论(文字居多) - 文档型数据库:MongoDB # 图片 - 分布式文件系统 FastDFS - 淘宝:TFS - Google: GFS - Hadoop: HDFS - 阿里云: oss # 商品关键字 用于搜索 - 搜索引擎:solr,elasticsearch - 阿里:Isearch 多隆 # 商品热门的波段信息 - 内存数据库:Redis,Memcache # 商品交易,外部支付接口 - 第三方应用
Redis是什么?
Redis(Remote Dictionary Server),即远程字典服务!
是一个开源的使用ANSI C语言编写、支持网络、科技与内UC那一刻持久化的日志型、KV数据库,并提供多种语言的API。
免费开源,当下最热门的NoSQL 技术之一,也被人们称之为结构化数据库!
Redis能够干啥?
Redis 特性
需要用到的东西
下载安装包Releases · microsoftarchive/redis (github.com)
解压到我们自己的环境下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fnI3DeaL-1626518757387)(C:\Users\AW\Desktop\Redis学习.assets\image-20210707162553436.png)]
双击运行服务
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-buKNwv6Y-1626518757389)(C:\Users\AW\Desktop\Redis学习.assets\image-20210707162652797.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wz9nXyGp-1626518757390)(C:\Users\AW\Desktop\Redis学习.assets\image-20210707162915789.png)]
Redis-benchmar:是一个压力测试工具
redis-benchmark命令参数,可以来简单地测试一下;
#测试:100个并发连接 10000请求 redis-benchmark -h localhost -p 6379 -c 100 -n 100000
具体的在服务器上演示的代码:
#启动redis的链接 redis-server /wkconfig/redis.conf #启动我们的链接 redis-cli-p 6379 #进入到我们固有的目录下面,然后看看是否存在 cd /usr/local/bin ls #执行我们的测试 redis-benchmark -h localhost -p 6379 -c 100 -n 100000
通过测试,可以帮助我们理解redis能够有多快
redis默认有16个数据库(databases=16)
默认使用的是第0个,
可以使用select +数字,来切换到所要前往的数据库!
#查看数据库 select 3 #查看DB大小 DBSIZE #给当前的数据库设置值 set name wk select 7 DBSIZE get name #可见,在其他数据库中是没有的 select 3 get name #这里就能够得到我们的值:wk #查看当前数据库中的key keys * #结果会出现我们设置的wk所属于的key(“name”) #清空全部数据库的内容 FLUSHALL #清空当前库 flushdb keys * #如此操作之后,就会发现没有了 selcet 0 keys * #会发现,在默认的数据库之中,会存在一些默认的key
为什么redis选择6379,是开发者的时候,作为一个明星的名字的缩写(粉丝效应)
明白我们的Redis是很快的,官方表示,Redis是基于内存操作,CPU不是redis的性能瓶颈,是依据机器的带宽,既然可以使用单线程来实现,所以就使用了单线程了
为啥呢么单线程这么快?
(前言:Redis是C语言写的,官方提供的数据为100000+的QPS,所以这个不比Memecache差)
误区:
1.高性能的服务器不一定是多线程的
2.多线程不一定比单线程效率高,其中多线程食欲CPU的调度有关的(CPU>内存>硬盘的速度)
3.redis是将所有的数据全部放在内存中的,所以说使用单线程操作的效率就是最高的,而多线程需要的是(cpu的上下文切换的操作),而对于内存系统来说,如果没有上下文切换,那么效率就是最高的!!
多次的读写都是在一个CPU上的,所以单线程的就是最佳的方案
步骤,先打开redis,然后测试连接;
FLUSHALL,将数据库全部清空,尽量要用英文
EXISTS +key的名字 ,我们现在讲解的所有命令,一定要全部记住
现在我们看一下服务器上原生态使用这些命令
#将位于0,1,2...中的1号移除掉 move name 1 #设置我们名字的过期时间 ketys * get name EXPIRE name 10 #意思:命令将会在10后停止 ttl name # 说哦名当前执行,还剩余的时间,可以应用在我们的缓存方面上做用户的Cokkie, #查看当前key的类型 type +key的名字
后面如果遇到不知道的命令,就去官网查看即可
基本的命令
#设置了一个key,是key1;它的值是v1 set key1 v1 get key1 keys * #判断是否存在key1 EXISTS key1 #在字符串后面去追加东西(返回值是字符串的长度) APPEND key1 "hello" #如果当前的key不存在,将相当于set key get key1 #截取字符串,获取其长度(返回值也是长度) STRLEN key1
#设置浏览量的问题 set views 0 #每一个用户来就加一 incr views #阅读量减一 decr views #步长(一次性增加或减少很多个) INCRBY views 10 DECRBY views 10
#字符串的范围range set key1 "hello,wk" GETRANGE key1 0 3 #这个表示截取了0,1,2,3位置的字符串 GETRANGE key1 0 -1 #表示查看所有的字符串 set key2 abcdey SETRANGE key2 1 xx #表示将原字符串中的b改成了xx #效率很高,甚至可以远远大过当前的数据库
#setex (set with expire) #设置过期时间 #setnx (set if not exist)#不存在设置 # 在分布式锁中会常常使用 setex key3 30 "hello" #设置了新的key,然后这个key将在30s之后过期 ttl key3 #用来查看这个key还有多长时间过期 setnx mykey "redis" #设置值,如果没有值,就覆盖
#mset 同时设置多个值 #mget 同时获得多个值 mset k1 v1 k2 v2 #这样可以一次性设置多个值 mget k1 k2 #将对应的值批量拿去出来了 msetnx k1 v1 k4 v4 #msetnx是一个验字型的操作,要么一起成功,要么一起失败 #对象 set user:1 {name:zhangsan,age:3} #设置一个user:1对象 职位 json字符串来保存一个对象 mset user:1:name zhangsan user:1:age 2 mget user:1:name user:1:age #先get再set getset db redis #输出结果表示,没有redis的db; get db #显示,我们有了,因为前面的语句在get完了之后会set getset db mongodb #这个时候,会先get原来的redis,然后在把redis更新成mongodb
数据结构是想通的,未来会学习 Jedis,我们现在的key,会会变成方法
String类似的使用场景:value除了是我们的字符串还可以是我们的后数字
是一种基本的数据类型,是一种列表,可以变成一个栈
在Redis里,可以把List变成 栈,队列,阻塞队列
注意:Redis不区分大小写
#所有的List命令都是用L开头的 #LPUSH 是将一个值或者是多个值,插入到列表的头部(就是左边) LPUSH list one LPUSH list two LPUSH list three #lrange是查询获取其中的值 LRANGE list 0 -1 #这种表示是获取全部的值,结果会出现three two one LRANGE list 0 1 #结果是three two #Rpush是将一个值或者多个值放在最后面(右边) Rpush list right #POP移除一个元素 LPOP #左移除(移除列表的第一个元素,即标号是1的元素) RPOP #右移除(移除最后一个元素) #lindex通过下表获得List中的值 lindex list 0 #Llen获取其长度 Llen list #返回列表中的长度 #Lrem移除指定的值 lrem list 1 one #移除列表中的一个one lrem list 1 three#移除列表中的两个three #ltrim修剪操作 ltrim mylist 1 2 #结果是:如果有四个元素,0 1 2 3,那么结果就变成了保留原来中间的两个下标为1 2 的元素 #rpoplpush #移除列表中的最后一个元素,并将其添加到另一个列表中 rpoplpush mylist myotherlist#结果是在mylist中没有了最后一个元素,然后被添加到了myotherlist中了 #lset设置值,相当于update lset list 0 item #如果没有list这个集合,就会报错;下标不存在也不存在,如果存在,就会替换当前值 #linsert 从一个key的前面或后面插入一个指定的值 linsert mylist before "world" "other" #就会在world的前面,插入了 linsert mylist after "world" "hello"
小结
消息排队!消息队列 (从左边进,从右边拿Lpush, Rpop )栈(从左边进去,从左边拿Lpush lpop)
也是一个集合,而且在Set中值不能够重复
都是以S开头的
#sadd向集合中添加元素 sadd myset "hello" sadd myset "wk的回忆" sadd myset "lovelike" #Smembers 查看所有的元素 SMEMBERS myset #Sismember 判断是否是其中的成员 SISMEMBER myset hello #Scard 获取set集合中的内容元素个数 scard myset #Srem 移除集合中的某个值 Srem myset hello #set 是无序不重复集合,抽随机 #Srandmemert SRANDMEMBER myset #spop 随机删除指定的key spop myset #Smove 将某个元素从哪里移动到那里 Smove myset myset2 "wkwk" #共同关注!(并集) #数字集合类,差集、交集、并集) sadd key1 a sadd key1 b sadd key2 a sadd key2 c #差集 Sdiff key1 key2 #交集 (共同好友,微博) Sinter key1 key2 #并集 Sunion key1 key2
Map集合,key-Value!这时候这个值就是一个map集合,本质和string类型没有区别
#以H开头 Hset myhash field1 wk Hget myhash field1 #hmset同时设置多个值 hmset myhash field1 hello field2 world #hmget同时获取多个值 hmget myhash field1 fild2 #hgetall获取全部的数据 hgetall myhash #hdel 删除hash制定的key字段,对应的value值也就会消失了 hdel myhash field1 #获取所有的值 hgetall myhash #Hlen 查看都烧纸 hlen myhash #Hexists 判断hash中指定字段是否存在 HExists myhash field1 Hexists myhash field3 #只获得所有的field hkeys myhash #只获得所有的value hvals myhash #HIncr Hdecr Hincrby myhash field3 1 hincrby myhahs field3 -1 #Hsetnx 如果不存在,就创建;如果存在的话就报错 Hsetnx myhash field4 hello Hsetnx myhash field4 world
hash 变更的数据 user name age ,尤其是用户信息之类的,经常变动的信息!hash更适合于对象的存储,String 更加适合字符串存储
有序集合,可以排序,在set的基础上,正加了一个值,set k1 v1
zadd myset 1 one #添加一个或多个值 zadd myset 2 two 3 three #排序 zadd salary 2500 xiaohong zadd salary 50000 zhangsan zadd salary 500 wjwj ZRANGEBYCORE salary -inf +inf #会按照字母由小到大的顺序排序 inf表示无穷的意思 Zrevrange salary 0 -1 # 从大到小排序 ZRANGEBYCORE salary -inf 2500 withscores# 会将小于2500的值,输出来 ZRANGEBYCORE salary -inf +inf withscores #将从小到大输出来 #zrem移除元素 zrange salary 0 -1 zrem salary xiaohong zrange salary 0 -1 #查询元素的个数 zcard salary #zcount 获取指定区间的元素数量 zadd myset 1 hello zadd myset 2 world 3 wkwk zcount myset 1 3 # 获取指定区间的元素数量
按理思路:set 排序 存储班级成绩表,工资表排序
普通消息,1重要信息 2 带权重进行判断
排行榜应用实现,取Top N设置
朋友的定位,附近的人,打车距离计算,城市地理经纬度查询
Redis的Geo可以查询一些测试数据 在Redis3.2版本就推出了
相关命令
# geoadd添加地理位置 # 规则:地理上的两级无法直接泰诺健啊,我们一般会下载城市数据,直接通过java程序一次性导入 # 参数:key geoadd china:city 116.40 39.90 beijing geoadd china:city 121.47 31.23 shanghai geoadd china:city 105.20 89.23 chongqing 114.05 22.52 shenzhen geoadd china:city 120.16 30.34 hangzhou 108.67 23.14 xi`an #geopos 获取指定城市的经纬度,获得当前定位 GEOPOS china:city beijing #GEOdist 查看两个城市之间的距离 GEODIST China:city beijing shanghai km #GEORADIUS以给定的经纬度为中心,找出某一半径内的元素 GEORADIUS china:city 110 30 1000 km #以110 30点为中心的,半径为1000km的城市 GEORADIUS china:city 110 30 500 km withdist #查看半径为500km内的城市及其之间的距离 GEORADIUS china:city 110 30 500 km withcoord #查询半径500km内的城市之间点的坐标 GEORADUIS china:city 110 30 500 km withdist withcoord count 2 #筛选出指定的结果 #找出位于指定元素周围的其他元素 GEORADIUSBYMEMBER china:city beijing 1000 km #GEOHASH 命令,返回一个或多个位置元素的Geohash表示(该命令返回11个自负的Geohash字符串) #将而为的经纬度转换为一味地字符串,如果两个字符串越是相像,那么则距离越近 GEOhash china:city beijing chongqing #Zrange 查询 zrange china:city 0 -1 #Zrem 移除 Zrem china:city beijing
GEO底层的实现原理其实就是Zset,
基数:不重复的元素,可以接受误差
简介
更新了Hyperloglog数据结构
Redis hyperloglog 技术统计的算法
优点:占用的内存是固定的,2^64的不同元素的基数,只需要占用12KB内存,如果要从内存角度来比较,Hyperloglog是首选的
网页的UV(一个人放那个问一个网站多次,但是还是算作一个人)
传统的方式:set保存用户的id,然后就可以统计set中的元素数量作为标准判断。这个昂视如果保存大量的用户id,就会编辑哦麻烦,我们的目的是为了技术,而不是保存用户id
0.81% 的错误率,来统计UV任务,可以忽略不计的
# PFadd mykey a b c d e f g j PFcount mykey #结果是8个 PFadd mykey2 i j z x c v b n m PFcount mykey2 #下面的操作是将mykey 和 mykey2 合并成了mykey3 PFmerge mykey3 mykey mykey2 PFcount mykey3 #只会输出不同的元素的个数
如果允许容错,呢么一定可以使用Hyperloglog
如果不允许容错,就是用set或者自己的数据类型即可
位存储
统计用户信息(活跃,不活跃,登录,未登录),统计疫情情况
Bitmaps位图,数据结构,都是操作二进制位来进行记录,就只有0和1两个状态
365 天 = 365 bit 一字节=八比特
测试
#设置七天之内是否登陆 setbit sign 0 1 setbit sign 1 1 setbit sign 2 0 setbit sign 3 1 setbit sign 4 0 setbit sign 5 1 setbit sign 6 0 #查看某一天是否登录 getbit sign 3 #统计登录的天数 bitcount sign
Redis单条命令是保存原子性的,但是事务不保证原子性(要么同时成功,要么同时失败,原子性!)
Redis事物本质:一组命令的集合!一个数物种的所有命令都会被序列化,在事务执行过程中,会按照顺序执行!
一次性、顺序性、排他性!执行一些列的命令
正常执行事务!
#开启事务 multi #列出(Queued)相关的命令,让命令入对 set k1 v1 set k2 v2 get k2 set k3 v3 #一次性执行上面的所有命令 exec multi set k1 v1 set k2 v2 set k4 v4 discard #取消事务,上面的队列就不会执行了
编译型异常(代码有问题!命令有错),事务中所有的命令都不会被执行
运行时异常(例如:1除以0),如果事务队列中存在语法性,那么执行命令的时候,其他命令是可以正常执行的
悲观锁:
乐观锁:
#银行支出记录 Multi set money 100 set out 0 watch money Decrby money 20 INcrby out 20 exec
测试多线程修改值的时候,使用watch可以当做乐观锁操作
watch money#检视money multi DECRBY money 10 INCRBY out 10 exec#在另一个线程中修改了值,发现结果失败
如果修改失败,就修改最新的解锁,解 锁
Unwatch#如果发现事务执行失败,就先解锁 Watch money#获取最新的值,再次检视,select version multi Decrby money 1 Incrby money 1 exec#比对监视的值是否发生了变化,如果咩有变化,那么可以执行成功,如果便了就执行失败
什么系Jedis?
知其然并知其所以然,慢慢来就很快
测试
建立一个空项目,wk-redis
在里面建立一个新的maven的module
导入相应的依赖
<!-- 导入Jedis包--> <dependencies> <!-- https://mvnrepository.com/artifact/redis.clients/jedis --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.6.1</version> </dependency> <!--导入fastjson--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.76</version> </dependency> </dependencies>
见上面的五大数据类型 String Set Zset Hash List
Springboot操作数据:spring-data jpa jdbc mongodb redis
springData也是和Springboot起名的项目
说明:在springboot之后,我们原来是用的jedis被替换为了lettuce
jedis:底层采用的是智联的操作,多个线程操作的话,数不安全的,如果想要避免不安全,使用jedis pool连接池!更像BIO模式
lettuce:底层采用netty,实例可以再多个线程中进行共享,不存在线程不安全的情况!可以减少线程数据了,更像NIO模式
默认的RedisTemplate没有过多的设置,redis对象都是需要序列化,两个泛型都是object类型,我们需要强制转换 <String, Object>
由于String是redis中最常使用的类型,所以说单独提出来一个Bean
整合测试一下
导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
配置连接
#配置redis spring.redis.host=127.0.0.1 spring.redis.port=6379
测试
@Autowired private RedisTemplate redisTemplate; @Test void contextLoads() { //redisTemplate. 操作不用的数据类型,点出来的api跟我们是一样的 //opsForValue 类似String //opsForList 操作List 类似String //opsForHash //opsForGeo //opsForSet //opsForZSet //opsForHyperLogLog //除了基本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如食物,和基本的CRUD // 获取redis的链接对象 // RedisConnection connection = redisTemplate.getConnectionFactory().getConnection(); // connection.flushDb(); // connection.flushAll(); redisTemplate.opsForValue().set("mykey","哇卡哇卡java"); System.out.println(redisTemplate.opsForValue().get("mykey")); }
编写我们自己写的API
//编写我们自己的配置类RedisTemplate @Bean public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)throws UnknownHostException{ RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); return template; }
RedisTemplate模板,可以直接使用
@Configuration public class RedisConfig { //编写我们自己的配置类RedisTemplate //下方是固定的一个模板,在企业中可以直接使用 @Bean @SuppressWarnings("all") public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { //为了开发方便,使用<String, Object> RedisTemplate<String, Object> template = new RedisTemplate(); template.setConnectionFactory(redisConnectionFactory); //Jacson序列化配置 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(mapper); //string的序列化 StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); //key采用String的序列化形式 template.setKeySerializer(stringRedisSerializer); //hash的key应采用String的序列化方式 template.setHashKeySerializer(stringRedisSerializer); //value序列化方式采用jackson template.setValueSerializer(jackson2JsonRedisSerializer); //hash的value序列化方式采用jackson template.setHashValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; } }
注意:1.如果出现重名的情况,就用修饰语 @Qualifier(“redisTemplate”)
2.如果出现在Idea中,可以执行出来,指定的字符串,但是在客户端redis-cli却不可以,那么解决这个问题,就要用到我们的redis-cli了
多使用cmd+p查看方法需要的parameter,给进去就好
RedisUtils开发
package com.wk.utils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; @Component public class RedisUtils { @Autowired //注入我们自己编写的redisTemplate private RedisTemplate<String, Object> redisTemplate; /** * 制定缓存失效时间 * @param key 键 * @param time 时间(秒) */ public boolean expire(String key, long time) { try { if (time > 0) { redisTemplate.expire(key, time, TimeUnit.SECONDS); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根据key 获取过期时间 * @param key 键 不能为null * @return 时间(秒) 返回0代表为永久有效 */ public long getExpire(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } /** * 判断key是否存在 * @param key 键 * @return true 存在 false不存在 */ public boolean hasKey(String key) { try { return redisTemplate.hasKey(key); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 删除缓存 * @param key 可以传一个值 或多个 */ @SuppressWarnings("unchecked") public void del(String... key) { if (key != null && key.length > 0) { if (key.length == 1) { redisTemplate.delete(key[0]); } else { redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key)); } } } // ============================String============================= /** * 普通缓存获取 * @param key 键 * @return 值 */ public Object get(String key) { return key == null ? null : redisTemplate.opsForValue().get(key); } /** * 普通缓存放入 * @param key 键 * @param value 值 * @return true成功 false失败 */ public boolean set(String key, Object value) { try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 普通缓存放入并设置时间 * @param key 键 * @param value 值 * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 * @return true成功 false 失败 */ public boolean set(String key, Object value, long time) { try { if (time > 0) { redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } else { set(key, value); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 递增 * @param key 键 * @param delta 要增加几(大于0) * @return */ public long incr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递增因子必须大于0"); } return redisTemplate.opsForValue().increment(key, delta); } /** * 递减 * @param key 键 * @param delta 要减少几(小于0) * @return */ public long decr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递减因子必须大于0"); } return redisTemplate.opsForValue().increment(key, -delta); } // ================================Map================================= /** * HashGet * @param key 键 不能为null * @param item 项 不能为null * @return 值 */ public Object hget(String key, String item) { return redisTemplate.opsForHash().get(key, item); } /** * 获取hashKey对应的所有键值 * @param key 键 * @return 对应的多个键值 */ public Map<Object, Object> hmget(String key) { return redisTemplate.opsForHash().entries(key); } /** * HashSet * @param key 键 * @param map 对应多个键值 * @return true 成功 false 失败 */ public boolean hmset(String key, Map<String, Object> map) { try { redisTemplate.opsForHash().putAll(key, map); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * HashSet 并设置时间 * @param key 键 * @param map 对应多个键值 * @param time 时间(秒) * @return true成功 false失败 */ public boolean hmset(String key, Map<String, Object> map, long time) { try { redisTemplate.opsForHash().putAll(key, map); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 向一张hash表中放入数据,如果不存在将创建 * @param key 键 * @param item 项 * @param value 值 * @return true 成功 false失败 */ public boolean hset(String key, String item, Object value) { try { redisTemplate.opsForHash().put(key, item, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 向一张hash表中放入数据,如果不存在将创建 * @param key 键 * @param item 项 * @param value 值 * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间 * @return true 成功 false失败 */ public boolean hset(String key, String item, Object value, long time) { try { redisTemplate.opsForHash().put(key, item, value); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 删除hash表中的值 * @param key 键 不能为null * @param item 项 可以使多个 不能为null */ public void hdel(String key, Object... item) { redisTemplate.opsForHash().delete(key, item); } /** * 判断hash表中是否有该项的值 * @param key 键 不能为null * @param item 项 不能为null * @return true 存在 false不存在 */ public boolean hHasKey(String key, String item) { return redisTemplate.opsForHash().hasKey(key, item); } /** * hash递增 如果不存在,就会创建一个 并把新增后的值返回 * @param key 键 * @param item 项 * @param by 要增加几(大于0) * @return */ public double hincr(String key, String item, double by) { return redisTemplate.opsForHash().increment(key, item, by); } /** * hash递减 * @param key 键 * @param item 项 * @param by 要减少记(小于0) * @return */ public double hdecr(String key, String item, double by) { return redisTemplate.opsForHash().increment(key, item, -by); } // ============================set============================= /** * 根据key获取Set中的所有值 * @param key 键 * @return */ public Set<Object> sGet(String key) { try { return redisTemplate.opsForSet().members(key); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 根据value从一个set中查询,是否存在 * @param key 键 * @param value 值 * @return true 存在 false不存在 */ public boolean sHasKey(String key, Object value) { try { return redisTemplate.opsForSet().isMember(key, value); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将数据放入set缓存 * @param key 键 * @param values 值 可以是多个 * @return 成功个数 */ public long sSet(String key, Object... values) { try { return redisTemplate.opsForSet().add(key, values); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 将set数据放入缓存 * @param key 键 * @param time 时间(秒) * @param values 值 可以是多个 * @return 成功个数 */ public long sSetAndTime(String key, long time, Object... values) { try { Long count = redisTemplate.opsForSet().add(key, values); if (time > 0) expire(key, time); return count; } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 获取set缓存的长度 * @param key 键 * @return */ public long sGetSetSize(String key) { try { return redisTemplate.opsForSet().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 移除值为value的 * @param key 键 * @param values 值 可以是多个 * @return 移除的个数 */ public long setRemove(String key, Object... values) { try { Long count = redisTemplate.opsForSet().remove(key, values); return count; } catch (Exception e) { e.printStackTrace(); return 0; } } // ===============================list================================= /** * 获取list缓存的内容 * @param key 键 * @param start 开始 * @param end 结束 0 到 -1代表所有值 * @return */ public List<Object> lGet(String key, long start, long end) { try { return redisTemplate.opsForList().range(key, start, end); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 获取list缓存的长度 * @param key 键 * @return */ public long lGetListSize(String key) { try { return redisTemplate.opsForList().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 通过索引 获取list中的值 * @param key 键 * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推 * @return */ public Object lGetIndex(String key, long index) { try { return redisTemplate.opsForList().index(key, index); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 将list放入缓存 * @param key 键 * @param value 值 * @param `time 时间(秒) * @return */ public boolean lSet(String key, Object value) { try { redisTemplate.opsForList().rightPush(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * @param key 键 * @param value 值 * @param time 时间(秒) * @return */ public boolean lSet(String key, Object value, long time) { try { redisTemplate.opsForList().rightPush(key, value); if (time > 0) expire(key, time); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * @param key 键 * @param value 值 * @param `time 时间(秒) * @return */ public boolean lSet(String key, List<Object> value) { try { redisTemplate.opsForList().rightPushAll(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @param time 时间(秒) * @return */ public boolean lSet(String key, List<Object> value, long time) { try { redisTemplate.opsForList().rightPushAll(key, value); if (time > 0) expire(key, time); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根据索引修改list中的某条数据 * @param key 键 * @param index 索引 * @param value 值 * @return */ public boolean lUpdateIndex(String key, long index, Object value) { try { redisTemplate.opsForList().set(key, index, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 移除N个值为value * @param key 键 * @param count 移除多少个 * @param value 值 * @return 移除的个数 */ public long lRemove(String key, long count, Object value) { try { Long remove = redisTemplate.opsForList().remove(key, count, value); return remove; } catch (Exception e) { e.printStackTrace(); return 0; } } }
启动的时候,就通过配置文件来启动
行家有没有,出手就知道
单位
包含
可以将多个配置文件都配置到一个
网络
bind 127.0.0.1 #绑定的ip
protected-mode yes #保护模式
port 6379
通用配置 GENERAL
daemonize yes #以守护进程的方式运行,默认是no,我们需要自己开启为yes
pidfile
可以使用 include 组合多个配置问题
网络配置
日志输出级别
日志输出文件
持久化规则
由于Redis是基于内存的数据库,需要将数据由内存持久化到文件中
持久化方式:
RDB文件相关
主从复制
Security模块中进行密码设置
客户端连接相关
maxclients 10000 最大客户端数量 maxmemory <bytes> 最大内存限制 maxmemory-policy noeviction # 内存达到限制值的处理策略 123
redis 中的默认的过期策略是 volatile-lru 。
设置方式
config set maxmemory-policy volatile-lru 1
**1、volatile-lru:**只对设置了过期时间的key进行LRU(默认值)
2、allkeys-lru : 删除lru算法的key
**3、volatile-random:**随机删除即将过期key
**4、allkeys-random:**随机删除
5、volatile-ttl : 删除即将过期的
6、noeviction : 永不过期,返回错误
————————————————
版权声明:本文为CSDN博主「每天进步一點點」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/DDDDeng_/article/details/108118544
AOF相关部分
redis是内存数据库,如果不讲内存中的数据库状态保存到磁盘中,那么一旦服务器进程退出,服务器中的状态也会消失,所以redis提供了持久化功能
在指定的时间间隔内,将内存中的数据用快照(snapshot)的方式写入到磁盘中,恢复时是将快照文件直接读到内存中。
Redis单独创建(fork)一个子进程进行持久化,会先将数据写入到一个临时文件中,带持久化过程都结束了,在用这个临时文件替换上次持久化好的文件。整个过程中,住进城市不进行任何IO操作的,这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加高效。RDB的却带那是最后一次持久化后的数据可能丢失。我们默认的就是RDB,一般情况下不需要修改这个配置。
触发机制
备份集自动生成一个dump.rdb
如何恢复rdb文件
只需要将rdb文件放在我们redis启动目录就可以,redis启动的时候会自动检查dump.rdb恢复期中的数据
查看需要存在的位置
config get dir
其自己默认的配置基本够用,但还不够
优点:
缺点:
将我们的所有命令都记录下来,history恢复的时候就把这个文件全部执行一遍
append
默认是不开启的 ,需要手动开启,appendonly 改为yes就开启了aof了
重启我们的redis,就可以生效了
但是如果aof文件有错误,这时候redis是不能启动的,因此我们需要修改这个aof文件
redis给我们通了一个工具redis-check-aof --fix
如果aof文件大于64m,太大,fork一个新的进程来将我们的文件进行重写
aof默认就是文件的无限追加,文件会越来越大
优缺点
优点:
缺点:
RDB | AOF | |
---|---|---|
启动优先级 | 低 | 高 |
体积 | 小 | 大 |
恢复速度 | 快 | 慢 |
数据安全性 | 丢数据 | 根据策略决定 |
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(Master/Leader),后者称为从节点(Slave/Follower), 数据的复制是单向的!只能由主节点复制到从节点(主节点以写为主、从节点以读为主)。
默认情况下,每台Redis服务器都是主节点,一个主节点可以有0个或者多个从节点,但每个从节点只能由一个主节点。
只配置从库,不用配置主库!
info replication # 查看当前库的信息 # Replication role:master #角色 master connnected_slaves:0 # 没有从机 master_replid:b63c90e6c501143759cb0e7f450bd1eb0c70882a master_replid2:00000000000000000000000000000 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个redis服务器,可以通过进程信息查看
SLAVEOF host 6379 #让6379当老大 info replication #在主机中查看
真实的主从配置应该是在配置文件里面的,我们在命令行里面进行的,只是暂时的
主机可以进行写,但从机不能写只能读
如果主机断掉了,从机依旧可以查询到,info replication;如果主机又重启了,从机依旧可以直接获取到主机写的信息!
如果是用命令行,来配置的主从,如果重启了,就会变回主机。然后把这个主机变成从机,立马就会从主机中获取值
复制原理
Slave启动成功连接到master后会发送一个sync同步命令
Master接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令, 在后台进程执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。
全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
增量复制:Master继续将新的所有收集到的修改命令一次传给slave,完成同步
但是只要是重新连接到master,一次完全同步(全量复制)将会被自动执行!我们的数据一定可以在从机中看到
主节点M,创造一个从节点S1,然后让另外一个从节点S2作为S1的从节点,然后,S1便即使M从节点,也是S2的主节点。这时候就实现了主从复制
如果老大再见了,那额我们可以改朝换代,只要再从集中输入:SLAVEOF no one; 便成为了主机,这是手动的然后再info replication ; 如果这个时候原来的老大回来了,那么只是一个光杆司令了
自动选择老大的模式
概述
主机从机切换技术的方法是:当主服务器当即后,需要手动吧一台从机服务器切换为主服务器,这就需要人工干预,还会造成一段时间内服务不可用的现象。当我们使用哨兵架构(Sentinel)来解决这一问题
自动谋朝篡位,能后台监控主机是否故障,如果故障了根据投票数自欧东能够将从库转换为主库
哨兵模式是一种特殊的模式,首先Redis提供了烧饼的命令,哨兵是一个独立的进程,作为进程,会独立运行。原理是:哨兵通过发送命令,等待redis服务器相应,从而监控运行的多个Redis实例
哨兵一般有至少三个,=
然而一个烧饼进程对Redis服务器进行监控,可能会出现问题,我们可以使用多个烧饼进行监控,各个烧饼之间还会进行监控,这样就形成了多哨兵模式
假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程((计算机)故障切换;失效备援(为系统备援能力的一种,当系统中其中一项设备失效而无法运作时,另一项设备即可自动接手原失效系统所执行的工作)),仅仅是哨兵1主管道认为主服务器不可用,这个现象称为主管下线。当后面的邵兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果有一个烧饼发起,进行failover故障转移操作,切换成功后,就会通过发布订阅模式,让各个哨兵把自己金控的从服务器是实现切换主机,这个过程称为客观下线
测试
我们目前的状态是:一主二从
配置哨兵配置文件sentinel.conf
在文件中,编辑文件,然后输入:
#sentinel monitor 被监控的名称 hostport 1 sentinel monitor myredis 127.0.0.1 6379 1
1代表主机管了,slave投票看让谁接替成为主机,票数最多的,就会成为主机
从哨兵日志中可以看到相关的修改信息,如果原来的主机现在回来了,那么会变成从机
哨兵模式
优点:
缺点:
哨兵模式的全部配置
如果哨兵集群
# 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
所有读的请求先到缓存中查询,如果缓存中没有,就回去数据库中查,如果还有找到,就会疯狂的去mysql 中查,可能造成恶意攻击
概念
缓存穿透是:用户要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询,发现也没有,于是本次查询失败,当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库,这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透
解决方案
布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层呢个先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力;
缓存空对象
一次请求若在缓存和数据库中都没找到,就在缓存中方一个空对象用于处理后续这个请求。
这样做有一个缺陷:存储空对象也需要空间,大量的空对象会耗费一定的空间,存储效率并不高。解决这个缺陷的方式就是设置较短过期时间
即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
概念
相较于缓存穿透,缓存击穿的目的性更强,一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。这就是缓存被击穿,只是针对其中某个key的缓存不可用而导致击穿,但是其他的key依然可以使用缓存响应。
比如热搜排行上,一个热点新闻被同时大量访问就可能导致缓存击穿。
比如热搜排行上,一个热点新闻被同时大量访问就可能导致缓存击穿。
解决方案
设置热点数据永不过期
这样就不会出现热点数据过期的情况,但是当Redis内存空间满的时候也会清理部分数据,而且此种方案会占用空间,一旦热点数据多了起来,就会占用部分空间。
加互斥锁(分布式锁)
在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。保证同时刻只有一个线程访问。这样对锁的要求就十分高。
概念
大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。
解决方案
redis高可用
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群
限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
————————————————
版权声明:本文参考CSDN博主「每天进步一點點」的原创文章,遵循CC 4.0 BY-SA版权协议
原文链接:https://blog.csdn.net/DDDDeng_/article/details/108118544
视频拦截:【狂神说Java】Redis最新超详细版教程通俗易懂_哔哩哔哩_bilibili
念
缓存穿透是:用户要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询,发现也没有,于是本次查询失败,当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库,这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透
解决方案
布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层呢个先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力;
缓存空对象
一次请求若在缓存和数据库中都没找到,就在缓存中方一个空对象用于处理后续这个请求。
这样做有一个缺陷:存储空对象也需要空间,大量的空对象会耗费一定的空间,存储效率并不高。解决这个缺陷的方式就是设置较短过期时间
即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
概念
相较于缓存穿透,缓存击穿的目的性更强,一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。这就是缓存被击穿,只是针对其中某个key的缓存不可用而导致击穿,但是其他的key依然可以使用缓存响应。
比如热搜排行上,一个热点新闻被同时大量访问就可能导致缓存击穿。
比如热搜排行上,一个热点新闻被同时大量访问就可能导致缓存击穿。
解决方案
设置热点数据永不过期
这样就不会出现热点数据过期的情况,但是当Redis内存空间满的时候也会清理部分数据,而且此种方案会占用空间,一旦热点数据多了起来,就会占用部分空间。
加互斥锁(分布式锁)
在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。保证同时刻只有一个线程访问。这样对锁的要求就十分高。
概念
大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。
解决方案
redis高可用
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群
限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
————————————————
版权声明:本文参考CSDN博主「每天进步一點點」的原创文章,遵循CC 4.0 BY-SA版权协议
原文链接:https://blog.csdn.net/DDDDeng_/article/details/108118544
视频拦截:【狂神说Java】Redis最新超详细版教程通俗易懂_哔哩哔哩_bilibili