redis是什么?
Redis 是当前互联网世界最为流行的 NoSQL(Not Only SQL)数据库。NoSQL 在互联网系统中的作用很大,因为它可以在很大程度上提高互联网系统的性能。
Redis 具备一定持久层的功能,也可以作为一种缓存工具。对于 NoSQL 数据库而言,作为持久层,它存储的数据是半结构化的,这就意味着计算机在读入内存中有更少的规则,读入速度更快。
对于那些结构化、多范式规则的数据库系统而言,它更具性能优势。作为缓存,它可以支持大数据存入内存中,只要命中率高,它就能快速响应,因为在内存中的数据读/写比数据库读/写磁盘的速度快几十到上百倍,其作用如图所示。
当前 Redis 已经成为了主要的 NoSQL 工具,其原因如下。
1)响应快速
Redis 响应非常快,每秒可以执行大约 110 000 个写入操作,或者 81 000 个读操作,其速度远超数据库。如果存入一些常用的数据,就能有效提高系统的性能。
2)支持 6 种数据类型
它们是字符串、哈希结构、列表、集合、可排序集合和基数。比如对于字符串可以存入一些 Java 基础数据类型,哈希可以存储对象,列表可以存储 List 对象等。这使得在应用中很容易根据自己的需要选择存储的数据类型,方便开发。
对于 Redis 而言,虽然只有 6 种数据类型,但是有两大好处:一方面可以满足存储各种数据结构体的需要;另外一方面数据类型少,使得规则就少,需要的判断和逻辑就少,这样读/写的速度就更快。
3)操作都是原子的
所有 Redis 的操作都是原子的,从而确保当两个客户同时访问 Redis 服务器时,得到的是更新后的值(最新值)。在需要高并发的场合可以考虑使用 Redis 的事务,处理一些需要锁的业务。
4)MultiUtility 工具
Redis 可以在如缓存、消息传递队列中使用(Redis 支持“发布+订阅”的消息模式),在应用程序如 Web 应用程序会话、网站页面点击数等任何短暂的数据中使用。
正是因为 Redis 具备这些优点,使得它成为了目前主流的 NoSQL 技术,在 Java 互联网中得到了广泛使用。
一方面,使用 NoSQL 从数据库中读取数据进行缓存,就可以从内存中读取数据了,而不像数据库一样读磁盘。现实是读操作远比写操作要多得多,所以缓存很多常用的数据,提高其命中率有助于整体性能的提高,并且能减缓数据库的压力,对互联网系统架构是十分有利的。
另一方面,它也能满足互联网高并发需要高速处理数据的场合,比如抢红包、商品秒杀等场景,这些场合需要高速处理,并保证并发数据安全和一致性。
redis的安装
https://www.runoob.com/redis/redis-install.html
以redis-6.2.1为例
1、安装C 语言的编译环境
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ppUdpxlW-1622861176786)(C:\Users\li146\AppData\Roaming\Typora\typora-user-images\image-20210430120307742.png)]
2、下载redis-6.2.1.tar.gz放/opt目录
wget http://download.redis.io/releases/redis-6.2.1.tar.gz
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5tqYZm7a-1622861176788)(C:\Users\li146\AppData\Roaming\Typora\typora-user-images\image-20210430120401160.png)]
3、解压压缩包,解压命令:tar -zxvf redis-6.2.1.tar.gz
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e6wnvOBv-1622861176790)(C:\Users\li146\AppData\Roaming\Typora\typora-user-images\image-20210430120642931.png)]
4、解压完成后进入目录:cd redis-6.2.1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5AigBPXr-1622861176791)(C:\Users\li146\AppData\Roaming\Typora\typora-user-images\image-20210430120759720.png)]
5、在redis-6.2.1目录下再次执行make命令
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-om6EFx8q-1622861176794)(C:\Users\li146\AppData\Roaming\Typora\typora-user-images\image-20210430120902525.png)]
6、执行make install命令
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dyggf8g0-1622861176796)(C:\Users\li146\AppData\Roaming\Typora\typora-user-images\image-20210430121047938.png)]
7、进入redis默认安装目录 /usr/local/bin
[root@localhost redis-6.2.1]# cd /usr/local/bin
8、启动
-
备份redis.conf, 进入opt/redis-6.2.1中拷贝一份redis.conf到etc目录
[root@localhost redis-6.2.1]# cp redis.conf /etc/redis.conf
-
修改redis.conf 把 daemonize no改成yes
root@localhost etc]# vi redis.conf #允许后台启动
-
启动
进入**/usr/local/bin目录下运行redis-server /etc/redis.conf**
[root@localhost etc]# cd /usr/local/bin [root@localhost bin]# ls dump.rdb redis-check-aof redis-cli redis-server redis-benchmark redis-check-rdb redis-sentinel [root@localhost bin]# redis-server /etc/redis.conf
-
进入客户端
[root@localhost bin]# redis-cli 127.0.0.1:6379>
9、关闭redis
-
1、直接shutdown
[root@localhost bin]# redis-cli 127.0.0.1:6379> shutdown
-
2、退出客户端找到进程杀死进程
[root@localhost bin]# redis-cli 127.0.0.1:6379> exit [root@localhost bin]# ps -ef | grep redis root 7927 1 0 11:54 ? 00:00:05 redis-server 127.0.0.1:6379 root 8368 3113 0 12:29 pts/0 00:00:00 grep --color=auto redis [root@localhost bin]# kill -9 7927
String数据类型
#####################################################
127.0.0.1:6379> set k1 java #添加键值对
OK
127.0.0.1:6379> get k1 #查看对应key的值
"java"
#####################################################
127.0.0.1:6379> append k1 linux #向k1的value值后面追加字符串
(integer) 9
127.0.0.1:6379> get k1
"javalinux"
127.0.0.1:6379> strlen k1 #查看k1的长度
(integer) 9
127.0.0.1:6379> setnx k2 mysql #只有在 key 不存在时,设置 key 的值
(integer) 1
127.0.0.1:6379> keys * #查看所有的key
1) "k1"
2) "k2"
#####################################################
27.0.0.1:6379> set k3 0
OK
127.0.0.1:6379> incr k3 #自增1 只能对数字值操作,如果为空,新增值为1
(integer) 1
127.0.0.1:6379> get k3
"1"
127.0.0.1:6379> incr k3
(integer) 2
127.0.0.1:6379> decr k3 #自减1 只能对数字值操作,如果为空,新增值为1
(integer) 1
127.0.0.1:6379> incrby k3 10 #设置步长 k3的值增加10
(integer) 11
127.0.0.1:6379> decrby k3 5 #k3的值减5
(integer) 6
#####################################################
127.0.0.1:6379> mset k4 v4 k5 v5 k6 v6 #同时设置一个或多个 key-value对
OK
127.0.0.1:6379> mget k4 k5 k6 #同时获取一个或多个 value
1) "v4"
2) "v5"
3) "v6"
#同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。
127.0.0.1:6379> msetnx k10 v10 k11 v11 k12 v12
(integer) 1
127.0.0.1:6379> mget k1 k2 k3 k4 k5 k6 k10 k11 k12
#####################################################
127.0.0.1:6379> getrange k1 0 -1 #获取全部的value
"javalinux"
127.0.0.1:6379> getrange k1 0 2 #获取全下标从0开始到2的值
"jav"
127.0.0.1:6379> setrange k1 4 sql # 覆写下标从4开始的值
(integer) 9
127.0.0.1:6379> setex k55 15 hahahaha #设置过期时间为15秒
OK
127.0.0.1:6379> getset k3 redis #得到k3的value 6 ,再把6改成redis
"6"
127.0.0.1:6379> get k3
"redis"
#####################################################
flushall 可以清除所有数据库的值
flushdb 清除当前数据库的值
#####################################################
List数据类型
#####################################################
127.0.0.1:6379> lpush k v1 v2 v3 #从左边插入一个或多个值
(integer) 3
127.0.0.1:6379> rpush k v4 v5 v6 #从右边插入一个或多个值
(integer) 6
127.0.0.1:6379> lpop k 2 #从左边吐出来2个值 值在键在,值光键亡。
1) "v3"
2) "v2"
127.0.0.1:6379> rpop k 1 #从右边吐出来1个值
1) "v6"
127.0.0.1:6379> lrange k 0 -1
1) "v1"
2) "v4"
3) "v5"
#####################################################
127.0.0.1:6379> lindex k 0 #安装索引下标获得元素
"v1"
127.0.0.1:6379> llen k #查看列表的长度
(integer) 3
127.0.0.1:6379> lrange k 0 -1
1) "v1"
2) "v4"
3) "v5"
127.0.0.1:6379> linsert k before v5 v7 #在v5的前面插入v7插入值
(integer) 4
127.0.0.1:6379> lrange k 0 -1
1) "v1"
2) "v4"
3) "v7"
4) "v5"
127.0.0.1:6379> linsert k after v5 v7 ##在v5的后面插入v7插入值
(integer) 5
127.0.0.1:6379> lrange k 0 -1
1) "v1"
2) "v4"
3) "v7"
4) "v5"
5) "v7"
127.0.0.1:6379> lrem k 2 v7 #从左边删除两个v7
(integer) 2
127.0.0.1:6379> lrange k 0 -1
1) "v1"
2) "v4"
3) "v5"
127.0.0.1:6379> lset k 2 v7 #将下标为2的值换成v7
OK
127.0.0.1:6379> lrange k 0 -1
1) "v1"
2) "v4"
3) "v7"
#####################################################
Set数据类型
#####################################################
127.0.0.1:6379> sadd user name age sex #将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略
(integer) 3
127.0.0.1:6379> smembers user #取出该集合的所有值。
1) "sex"
2) "age"
3) "name"
127.0.0.1:6379> sismember user sex #判断集合<key>是否为含有该<value>值,有1,没有0
(integer) 1
127.0.0.1:6379> scard user #返回该集合的元素个数
(integer) 3
127.0.0.1:6379> srem user sex #删除集合中的指定元素
(integer) 1
127.0.0.1:6379> smembers user
1) "age"
2) "name"
127.0.0.1:6379> spop user #随机从该集合中吐出一个值
"age"
#####################################################
127.0.0.1:6379> sadd user name age sex id #将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略
(integer) 4
127.0.0.1:6379> srandmember user 3 #随机从该集合中取出n个值。不会从集合中删除
1) "sex"
2) "name"
3) "id"
127.0.0.1:6379> sadd system name root
(integer) 2
127.0.0.1:6379> smove user system id #把user集合中的id移动到system集合
(integer) 1
127.0.0.1:6379> smembers system
1) "id"
2) "root"
3) "name"
127.0.0.1:6379>
#####################################################
127.0.0.1:6379> sinter user system #返回两个集合的交集元素
1) "name"
127.0.0.1:6379> sunion user system #返回两个集合的并集元素
1) "age"
2) "id"
3) "name"
4) "root"
5) "sex"
127.0.0.1:6379> sdiff user system #返回两个集合的差集元素(key1中的,不包含key2中的)
1) "sex"
2) "age"
#####################################################
hash数据结构
#####################################################
127.0.0.1:6379> hset user1 name zhangsan age 18 sex 1 #设置key为user1,field为name age sex的集合
(integer) 3
127.0.0.1:6379> hget user1 age #查看key为user1,field为age的值
"18"
127.0.0.1:6379> hmset user2 name lisi age 20 sex 1 #批量设置hash的值
OK
127.0.0.1:6379> hexists user2 name #查看哈希表 key 中,给定域 field 是否存在
(integer) 1
127.0.0.1:6379> hkeys user1 #列出该hash集合的所有field
1) "name"
2) "age"
3) "sex"
127.0.0.1:6379> hvals user1 #列出该hash集合的所有value
1) "zhangsan"
2) "18"
3) "1"
127.0.0.1:6379> hincrby user1 age 3 #哈希表 key 中的域 field 的值加上增量加3
(integer) 21
#####################################################
Zset数据结构
#####################################################
127.0.0.1:6379> zadd user 100 java 200 mysql 300 linux 400 redis
(integer) 4
#将一个或多个 member 元素及其 score 值加入到有序集 key 当中。
127.0.0.1:6379> zrange user 0 -1 #返回有序集 key 中,下标在0到-1之间的元素
1) "java"
2) "mysql"
3) "linux"
4) "redis"
127.0.0.1:6379> zadd user 250 php
(integer) 1
127.0.0.1:6379> zrange user 0 -1 WITHSCORES #带评分的返回
1) "java"
2) "100"
3) "mysql"
4) "200"
5) "php"
6) "250"
7) "linux"
8) "300"
9) "redis"
10) "400"
127.0.0.1:6379> zrangebyscore user 200 300 #集合中200-300的值
1) "mysql"
2) "php"
3) "linux"
127.0.0.1:6379> zincrby user 50 linux #为元素的score加上增量
"350"
127.0.0.1:6379> zrem user php #删除该集合下,指定值的元素
(integer) 1
127.0.0.1:6379> zcount user 250 500 #统计该集合,分数区间内的元素个数
(integer) 2
127.0.0.1:6379> zrank user mysql #返回该值在集合中的排名,从0开始。
(integer) 1
#####################################################
BitMaps 位操作的字符串
#####################################################
#每个独立用户是否访问过网站存放在Bitmaps中, 将访问的用户记做1, 没有访问的用户记做0, 用偏移量作为用户的id。设置键的第offset个位的值(从0算起) , 假设现在有20个用户,userid=1, 6, 11, 15, 19的用户对网站进行了访问
#####################################################
127.0.0.1:6379> setbit users 1 1 #设置Bitmaps中某个偏移量的值(0或1)
(integer) 0
127.0.0.1:6379> setbit users 6 1
(integer) 0
127.0.0.1:6379> setbit users 11 1
(integer) 0
127.0.0.1:6379> setbit users 15 1
(integer) 0
127.0.0.1:6379> setbit users 19 1
(integer) 0
127.0.0.1:6379> getbit users 15 #获取Bitmaps中某个偏移量的值
(integer) 1
127.0.0.1:6379> getbit users 8 #不存在
(integer) 0
127.0.0.1:6379> bitcount users #统计字符串从start字节到end字节比特值为1的数量
(integer) 5
127.0.0.1:6379> bitcount users 1 3
(integer) 3
#####################################################
#2020-11-04 日访问网站的userid=1,2,5,9。
127.0.0.1:6379> setbit unique:users:20201104 1 1
(integer) 0
127.0.0.1:6379> setbit unique:users:20201104 2 1
(integer) 0
127.0.0.1:6379> setbit unique:users:20201104 5 1
(integer) 0
127.0.0.1:6379> setbit unique:users:20201104 9 1
(integer) 0
#2020-11-03 日访问网站的userid=0,1,4,9。
127.0.0.1:6379> setbit unique:users:20201103 0 1
(integer) 0
127.0.0.1:6379> setbit unique:users:20201103 1 1
(integer) 0
127.0.0.1:6379> setbit unique:users:20201103 4 1
(integer) 0
127.0.0.1:6379> setbit unique:users:20201103 9 1
(integer) 0
#计算出两天都访问过网站的用户数量
127.0.0.1:6379> bitop and unique:users:and:20201104_03 unique:users:20201103 unique:users:20201104
(integer) 2
#计算出任意一天都访问过网站的用户数量
127.0.0.1:6379> bitop or unique:users:and:20201104_03 unique:users:20201103 unique:users:20201104
(integer) 2
127.0.0.1:6379>
#####################################################
HyperLogLog 基数运算
#####################################################
127.0.0.1:6379> pfadd p java #添加指定元素到 HyperLogLog 中
(integer) 1
127.0.0.1:6379> pfadd p php
(integer) 1
127.0.0.1:6379> pfadd p java #执行命令后p估计的近似基数发生变化,则返回1,否则返回0。
(integer) 0
127.0.0.1:6379> pfadd p c++ mysql
(integer) 1
127.0.0.1:6379> pfadd p java
(integer) 1
127.0.0.1:6379> pfcount p # 计算p的近似基数
(integer) 1
127.0.0.1:6379> pfadd p c++
(integer) 1
127.0.0.1:6379> pfcount p
(integer) 2
127.0.0.1:6379> pfadd p java
(integer) 0
127.0.0.1:6379> pfcount p
(integer) 2
127.0.0.1:6379> pfadd k1 a
(integer) 1
127.0.0.1:6379> pfadd k1 b
(integer) 1
127.0.0.1:6379> pfmerge k100 k1 p # 将一个或多个key合并后的结果存储在另一个key中
OK
127.0.0.1:6379> pfcount k100 #计算k100的近似基数
(integer) 4
#####################################################
Geospatial 地理信息
#####################################################
#添加地理位置(经度,纬度,名称)
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 shenzhen 116.38 39.90 beijing
(integer) 3
#获得指定地区的坐标值 列如获取上海的坐标
127.0.0.1:6379> geopos china:city shanghai
1) "121.47000163793563843"
2) "31.22999903975783553"
#获取两个位置之间的直线距离,如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位
127.0.0.1:6379> geodist china:city beijing shanghai km
"1068.1535"
#以给定的经纬度为中心,找出某一半径内的元素
#例如查找东经110度,北纬30度方圆1000公里的元素
127.0.0.1:6379> georadius china:city 110 30 1000 km
1) "chongqing"
2) "shenzhen"
#####################################################
springboot整合redis
1、 在pom.xml文件中引入redis相关依赖
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- spring2.X集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>
<!--添加fastjson包-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
2、添加配置类(序列化操作)
package com.atguigu.thxy.redis_springboot.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
3、application.properties 配置redis配置
#Redis服务器地址
spring.redis.host=192.168.140.136
#Redis服务器连接端口
spring.redis.port=6379
#Redis数据库索引(默认为0)
spring.redis.database= 0
#连接超时时间(毫秒)
spring.redis.timeout=1800000
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0
4、测试
package com.atguigu.thxy.redis_springboot.controller;
@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
@Autowired
@Qualifier("redisTemplate")
private RedisTemplate redisTemplate;
@GetMapping
public String testRedis() {
//设置值到redis
redisTemplate.opsForValue().set("name","lucy");
//从redis获取值
String name = (String)redisTemplate.opsForValue().get("name");
return name;
}
}
redis的事务操作
**定义:**Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。Redis事务的主要作用就是串联多个命令防止别的命令插队。
命令:multi(开启组队)、exec(执行)、discard(放弃组队)
从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。组队的过程中可以通过discard来放弃组队。
案列一:组队成功
127.0.0.1:6379> multi #开启组队
OK
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)> exec #执行
1) OK
2) OK
案例二:组队失败,提交失败
#####################################################
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set m1 v1
QUEUED
127.0.0.1:6379(TX)> set m2 #语法错误
(error) ERR wrong number of arguments for 'set' command
#组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
#####################################################
案例三:组队成功,提交有失败情况
#####################################################
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set m1 v1
QUEUED
127.0.0.1:6379(TX)> incr m1 #非数字不能自增
QUEUED
127.0.0.1:6379(TX)> set m1 v2
QUEUED
#如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
#####################################################
1、乐观锁
定义:乐观锁(Optimistic Lock)顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
案例一:
#在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
127.0.0.1:6379> watch balance
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby balance 10
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 110
127.0.0.1:6379>
#####################################################
#####################################################
127.0.0.1:6379> watch balance
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby balance 10
QUEUED
127.0.0.1:6379(TX)> exec
(nil) #原因:在第一次的事务中,balance的版本号已经改变了
2、事务的三大特性
- 单独的隔离操作
- 事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 没有隔离级别的概念
- 队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
- 不保证原子性
- 事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
redis的持久化
Redis 提供了2个不同形式的持久化方式。
1、RDB(Redis Database)
-
定义:在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里
-
备份是如何执行的
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
-
Fork
-
Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
-
在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了**“写时复制技术”**
-
一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
-
-
RDB持久化流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2eLKau4K-1622861176799)(C:\Users\li146\AppData\Roaming\Typora\typora-user-images\image-20210503124622754.png)]
-
dump.rdb文件
- 在redis.conf中配置文件名称,默认为dump.rdb
- rdb文件的保存路径,也可以修改。默认为Redis启动时命令行所在的目录下
-
如何触发RDB(持久化策略)
-
自动触发
- 自动触发最常见的情况是在配置文件中通过save m n,指定当m秒内发生n次变化时,会触发bgsave。
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9htu8hDS-1622861176801)(C:\Users\li146\AppData\Roaming\Typora\typora-user-images\image-20210503125732621.png)]
-
手动触发
-
sava
save命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在Redis服务器阻塞期间,服务器不能处理任何命令请求,不建议
-
bgsava
而bgsave命令会创建一个子进程,由子进程来负责创建RDB文件,父进程(即Redis主进程)则继续处理请求。bgsave命令执行过程中,只有fork子进程时会阻塞服务器,而对于save命令,整个过程都会阻塞服务器,因此save已基本被废弃,线上环境要杜绝save的使用;
-
-
-
stop-writes-on-bgsave-error
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EIwV1rVU-1622861176803)(file:///C:\Users\li146\AppData\Local\Temp\ksohtml9372\wps1.jpg)]
当Redis无法写入磁盘的话,直接关掉Redis的写操作。推荐yes.
-
压缩文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i9wINaRL-1622861176805)(file:///C:\Users\li146\AppData\Local\Temp\ksohtml9372\wps2.jpg)]
对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。
如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能。推荐yes.
-
检查完整性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9H7rfpkF-1622861176806)(file:///C:\Users\li146\AppData\Local\Temp\ksohtml9372\wps3.jpg)]
在存储快照后,还可以让redis使用CRC64算法来进行数据校验,
但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能
推荐yes.
-
优势
- 适合大规模的数据恢复
- 对数据完整性和一致性要求不高更适合使用
- 节省磁盘空间
- 恢复速度快
-
劣势
- Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
- 虽然Redis在fork时使用了****写时拷贝技术****,但是如果数据庞大时还是比较消耗性能。
- 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。
-
总结
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ibaUMeSI-1622861176807)(C:\Users\li146\AppData\Roaming\Typora\typora-user-images\image-20210503131004597.png)]
2、AOF(Append Only File)
2.1、定义:
官网介绍:以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
2.2、AOF持久化流程
-
客户端的请求写命令会被append追加到AOF缓冲区内;
-
AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
-
AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
-
Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-03JLdLH7-1622861176807)(C:\Users\li146\AppData\Roaming\Typora\typora-user-images\image-20210503140711075.png)]
2.2、AOF默认不开启
-
可以在redis.conf中配置文件名称,默认为 appendonly.aof
-
AOF文件的保存路径,同RDB的路径一致。启动目录下
2.4、AOF和同时开启
- AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)
2.5、AOF的启动/修复/恢复
-
AOF的备份机制和性能虽然和RDB不同, 但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis工作目录下,启动系统即加载。
-
正常恢复
- 修改默认的appendonly no,改为yes
- 将有数据的aof文件复制一份保存到对应目录(查看目录:config get dir)
- 恢复:重启redis然后重新加载
-
异常恢复
- 修改默认的appendonly no,改为yes
- 如遇到AOF文件损坏,通过/usr/local/bin/redis-check-aof–fix appendonly.aof进行恢复
- 备份被写坏的AOF文件
- 恢复:重启redis,然后重新加载
2.6、AOF同步频率设置
- appendfsync always
始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好
- appendfsync everysec
每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
- appendfsync no
redis不主动进行同步,把同步时机交给操作系统。
2.7、Rewrite压缩
1是什么:
AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制, 当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof
2重写原理,如何实现重写
AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),redis4.0版本后的重写,是指上就是把rdb 的快照,以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作。
no-appendfsync-on-rewrite:
如果 no-appendfsync-on-rewrite=yes ,不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)
如果 no-appendfsync-on-rewrite=no, 还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)
触发机制,何时重写
Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发
重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。
auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)
auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写。
例如:文件达到70MB开始重写,降到50MB,下次什么时候开始重写?100MB
系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,
如果Redis的AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。
3、重写流程
(1)bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。
(2)主进程fork出子进程执行重写操作,保证主进程不会阻塞。
(3)子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。
(4)1).子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。2).主进程把aof_rewrite_buf中的数据写入到新的AOF文件。
(5)使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QUaUqDfm-1622861176808)(file:///C:\Users\li146\AppData\Local\Temp\ksohtml9372\wps4.jpg)]
2.8、优势
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oCr4JLNP-1622861176809)(C:\Users\li146\AppData\Roaming\Typora\typora-user-images\image-20210503141818076.png)]
- 备份机制更稳健,丢失数据概率更低。
- 可读的日志文本,通过操作AOF稳健,可以处理误操作。
2.9、劣势
- 比起RDB占用更多的磁盘空间。
- 恢复备份速度要慢。
- 每次读写都同步的话,有一定的性能压力。
- 存在个别Bug,造成恢复不能。
redis之主从复制
-
定义:主机数据更新后根据配置和策略, 自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主
-
作用
-
读写分离,性能扩展
-
容灾快速恢复
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kq3ZlLQr-1622861176810)(C:\Users\li146\AppData\Roaming\Typora\typora-user-images\image-20210503151208771.png)]
-
-
主从复制之一主两从例子
在myredis文件夹中搭建一主两从
1、新建文件夹,复制redis.conf到文件夹中
[root@localhost ~]# mkdir /myredis [root@localhost ~]# cd /myredis [root@localhost myredis]# ls [root@localhost myredis]# cp /etc/redis.conf /myredis/redis.conf [root@localhost myredis]# ls redis.conf
2、新建3个配置文件,分别是redis6379.conf、redis6380.conf、redis6381.conf
[root@localhost myredis]# vi redis6379.conf [root@localhost myredis]# vi redis6380.conf [root@localhost myredis]# vi redis6381.conf
3、分别往3个配置文件中加入如下内容
############################################################### include /myredis/redis.conf pidfile /var/run/redis_6379.pid port 6379 dbfilename dump6379.rdb ############################################################### include /myredis/redis.conf pidfile /var/run/redis_6380.pid port 6380 dbfilename dump6380.rdb ############################################################### include /myredis/redis.conf pidfile /var/run/redis_6381.pid port 6381 dbfilename dump6381.rdb
4、启动3台服务器
[root@localhost myredis]# redis-server redis6379.conf [root@localhost myredis]# redis-cli -p 6379 [root@localhost myredis]# redis-server redis6380.conf [root@localhost myredis]# redis-cli -p 6380 [root@localhost myredis]# redis-server redis6381.conf root@localhost myredis]# redis-cli -p 6381
5、查看进程是否启动
[root@localhost myredis]# ps -ef | grep redis root 2894 1 0 15:02 ? 00:00:00 redis-server *:6379 root 2906 1 0 15:02 ? 00:00:00 redis-server *:6380 root 2940 1 0 15:03 ? 00:00:00 redis-server *:6381 root 2960 2594 0 15:04 pts/0 00:00:00 grep --color=auto redis
6、配从不配主
slaveof ip prot
成为某个实例的从服务器
1、在6380和6381上执行: slaveof 127.0.0.1 6379
127.0.0.1:6380> slaveof 127.0.0.1 6379 OK
2、在主机上写,在从机上可以读取数据
在从机上写数据报错
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7S3GdCKd-1622861176811)(file:///C:\Users\li146\AppData\Local\Temp\ksohtml13488\wps2.png)]
3、主机挂掉,重启就行,一切如初
4、从机重启需重设:slaveof 127.0.0.1 6379
可以将配置增加到文件中。永久生效。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YNTyPDZT-1622861176812)(file:///C:\Users\li146\AppData\Local\Temp\ksohtml13488\wps3.png)]
主从复制原理
- Slave启动成功连接到master后会发送一个sync命令
- Master接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令, 在后台进程执行完毕之后,master将传送整个数据文件到slave,以完成一次完全同步
- 主服务器接受到同步消息,把主服务器数据进行持久化,rdb文件,把rdb文件发送到从服务器,从服务器读取rdb文件
- 全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
- 增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步
- 但是只要是重新连接master,一次完全同步(全量复制)将被自动执行
薪火相传
上一个Slave可以是下一个slave的Master,Slave同样可以接收其他 slaves的连接和同步请求,那么该slave作为了链条中下一个的master, 可以有效减轻master的写压力,去中心化降低风险。
用 slaveof
中途变更转向:会清除之前的数据,重新建立拷贝最新的
风险是一旦某个slave宕机,后面的slave都没法备份
主机挂了,从机还是从机,无法写数据了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-50Oc0IEK-1622861176813)(file:///C:\Users\li146\AppData\Local\Temp\ksohtml13488\wps4.png)]
反客为主(哨兵模式的手动版)
当一个master宕机后,后面的slave可以立刻升为master,其后面的slave不用做任何修改。
用 slaveof no one 将从机变为主机。
哨兵模式
反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SPq3SKeE-1622861176813)(C:\Users\li146\AppData\Roaming\Typora\typora-user-images\image-20210503163113786.png)]
现在例子为为一主二仆模式,6379带着6380、6381,增加哨兵模式
1、自定义的/myredis目录下新建sentinel.conf文件
#vi sentinel.conf
2、配置哨兵,填写内容
sentinel monitor mymaster 127.0.0.1 6379 1
其中mymaster为监控对象起的服务器名称, 1 为至少有多少个哨兵同意迁移的数量。
3、启动哨兵
/usr/local/bin
redis做压测可以用自带的redis-benchmark工具
执行redis-sentinel /myredis/sentinel.conf
[root@localhost myredis]# redis-sentinel /myredis/sentinel.conf
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3SGWBbVp-1622861176814)(C:\Users\li146\AppData\Roaming\Typora\typora-user-images\image-20210503163741271.png)]
4、关闭6379主机测试哨兵
大概10秒左右可以看到哨兵窗口日志,切换了新的主机
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TEFDsOy6-1622861176815)(C:\Users\li146\AppData\Roaming\Typora\typora-user-images\image-20210503163839166.png)]
5、故障恢复
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1Ud67tvb-1622861176816)(C:\Users\li146\AppData\Roaming\Typora\typora-user-images\image-20210503164325720.png)]
6、用java实现主从复制
private static JedisSentinelPool jedisSentinelPool=null;
public static Jedis getJedisFromSentinel(){
if(jedisSentinelPool==null){
Set<String> sentinelSet=new HashSet<>();
sentinelSet.add("192.168.11.103:26379");
JedisPoolConfig jedisPoolConfig =new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(10); //最大可用连接数
jedisPoolConfig.setMaxIdle(5); //最大闲置连接数
jedisPoolConfig.setMinIdle(5); //最小闲置连接数
jedisPoolConfig.setBlockWhenExhausted(true); //连接耗尽是否等待
jedisPoolConfig.setMaxWaitMillis(2000); //等待时间
jedisPoolConfig.setTestOnBorrow(true); //取连接的时候进行一下测试 ping pong
jedisSentinelPool=new JedisSentinelPool("mymaster",sentinelSet,jedisPoolConfig);
return jedisSentinelPool.getResource();
}else{
return jedisSentinelPool.getResource();
}
}
redis集群
-
问题
容量不够,redis如何进行扩容?
并发写操作, redis如何分摊?
另外,主从模式,薪火相传模式,主机宕机,导致ip地址发生变化,应用程序中配置需要修改对应的主机地址、端口等信息。
之前通过代理主机来解决,但是redis3.0中提供了解决方案。就是无中心化集群配置
-
什么是集群
Redis 集群实现了对Redis的水平扩容,即启动N个redis节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N。
Redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求。
-
搭建集群
制作6个实例,6379,6380,6381,6389,6390,6391模拟6个服务器
3.1、创建实例
[root@localhost myredis]# vi redis6380.conf [root@localhost myredis]# vi redis6381.conf [root@localhost myredis]# vi redis6389.conf [root@localhost myredis]# vi redis6389.conf [root@localhost myredis]# vi redis6390.conf [root@localhost myredis]# vi redis6391.conf [root@localhost myredis]# ll 总用量 120 -rw-r--r-- 1 root root 177 5月 3 20:07 redis6379.conf -rw-r--r-- 1 root root 181 5月 3 20:07 redis6380.conf -rw-r--r-- 1 root root 181 5月 3 20:08 redis6381.conf -rw-r--r-- 1 root root 182 5月 3 20:08 redis6389.conf -rw-r--r-- 1 root root 182 5月 3 20:08 redis6390.conf -rw-r--r-- 1 root root 182 5月 3 20:08 redis6391.conf -rw-r--r-- 1 root root 92223 5月 3 14:54 redis.conf -rw-r--r-- 1 root root 409 5月 3 16:29 sentinel.conf
3.2、修改redis.conf文件,写入以下数据*
include /home/bigdata/redis.conf port 6379 pidfile "/var/run/redis_6379.pid" dbfilename "dump6379.rdb" cluster-enabled yes cluster-config-file nodes-6379.conf cluster-node-timeout 15000
3.3、启动六台服务器
[root@localhost myredis]# redis-server redis6379.conf [root@localhost myredis]# redis-server redis6380.conf [root@localhost myredis]# redis-server redis6381.conf [root@localhost myredis]# redis-server redis6389.conf [root@localhost myredis]# redis-server redis6390.conf [root@localhost myredis]# redis-server redis6391.conf
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9SUnlD2A-1622861176817)(C:\Users\li146\AppData\Roaming\Typora\typora-user-images\image-20210503203054473.png)]
3.4、将六个节点合成一个集群
进入redis安装目录的src目录下输入命令:
redis-cli --cluster create --cluster-replicas 1 192.168.1.101:6379 192.168.1.101:6380 192.168.1.101:6381 192.168.1.101:6389 192.168.1.101:6390 192.168.1.101:6391 ######################################################## cd /opt/redis-6.2.1/src
3.5、采用集群策略连接,设置数据会自动切换到相应的写主机
[root@localhost src]# redis-cli -c -p 6379
3.6、通过 cluster nodes 命令查看集群信息
127.0.0.1:6379> cluster nodes
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rT0WG49o-1622861176818)(C:\Users\li146\AppData\Roaming\Typora\typora-user-images\image-20210503203602207.png)]
-
什么是slots
一个 Redis 集群包含 16384 个插槽(hash slot), 数据库中的每个键都属于这 16384 个插槽的其中一个,
集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。
集群中的每个节点负责处理一部分插槽。 举个例子, 如果一个集群可以有主节点, 其中:
节点 A 负责处理 0 号至 5460 号插槽。
节点 B 负责处理 5461 号至 10922 号插槽。
节点 C 负责处理 10923 号至 16383 号插槽。
集群命令
不在一个slot下的键值,是不能使用mget,mset等多键操作。
-
查询key的slot
192.168.1.101:6380> cluster keyslot user (integer) 5474
-
计算slot下key的数量
192.168.1.101:6380> cluster countkeysinslot 5474 (integer) 2
-
查下slot下的key值
192.168.1.101:6380> cluster getkeysinslot 5474 10 1) "age{user}" 2) "name{user}"
redis应用问题解决(重点)
在生产环境中,会因为很多的原因造成访问请求绕过了缓存,都需要访问数据库持久层,虽然对Redsi缓存服务器不会造成影响,但是数据库的负载就会增大,使缓存的作用降低
缓存穿透
问题描述
key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
缓存穿透是指查询一个根本不存在的数据,缓存层和持久层都不会命中。在日常工作中出于容错的考虑,如果从持久层查不到数据则不写入缓存层,缓存穿透将导致不存在的数据每次请求都要到持久层去查询,失去了缓存保护后端持久的意义
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kv9u8V6z-1622861176818)(C:\Users\li146\AppData\Roaming\Typora\typora-user-images\image-20210504130112685.png)]
解决方案
(1)缓存空对象
缓存空对象:是指在持久层没有命中的情况下,对key进行set (key,null)
- 第一,value为null 不代表不占用内存空间,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间,比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除
- 第二,缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象
(2)布隆过滤器拦截
-
在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,当收到一个对key请求时先用布隆过滤器验证是key否存在,如果存在在进入缓存层、存储层。可以使用bitmap做布隆过滤器。这种方法适用于数据命中不高、数据相对固定、实时性低的应用场景,代码维护较为复杂,但是缓存空间占用少。
布隆过滤器实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
方案对比
缓存击穿
问题描述
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
系统中存在以下两个问题时需要引起注意:
- 当前key是一个热点key(例如一个秒杀活动),并发量非常大。
- 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的SQL、多次IO、多个依赖等。
在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SP66ufZp-1622861176819)(C:\Users\li146\AppData\Roaming\Typora\typora-user-images\image-20210504132757527.png)]
解决问题
(1)预先设置热门数据
- 在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长
(2)实时调整
- 现场监控哪些数据热门,实时调整key的过期时长
(3)分布式锁
-
只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。set(key,value,timeout)
缓存雪崩
问题描述
由于缓存层承载着大量请求,有效地保护了存储层,但是如果缓存层由于某些原因不可用(宕机)或者大量缓存由于超时时间相同在同一时间段失效(大批key失效/热点数据失效),大量请求直接到达存储层,存储层压力过大导致系统雪崩。
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
缓存雪崩与缓存击穿的区别在于这里针对很多key缓存,前者则是某一个key
正常访问
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4YL4WrRM-1622861176823)(file:///C:\Users\li146\AppData\Local\Temp\ksohtml1072\wps1.jpg)]
缓存失效瞬间
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VekioC8O-1622861176824)(file:///C:\Users\li146\AppData\Local\Temp\ksohtml1072\wps2.jpg)]
解决方案
(1)构建多级缓存
- 本地进程作为一级缓存,redis作为二级缓存,不同级别的缓存设置的超时时间不同,即使某级缓存过期了,也有其他级别缓存兜底
- 本地进程作为一级缓存,redis作为二级缓存,不同级别的缓存设置的超时时间不同,即使某级缓存过期了,也有其他级别缓存兜底
(2)使用锁或队列
- 用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况
(3)设置过期标志更新缓存
- 记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。
- 缓存的过期时间用随机值,尽量让不同的key的过期时间不同(例如:定时任务新建大批量key,设置的过期时间相同)