[数据库]-Redis数据库
Redis
参考视频链接
基础知识
1. 默认有16个数据库,默认使用第0个数据库。使用select 数字
命令来指定跳转到第几个数据库,使用dbsize
命令查看当前数据库数据多少,使用set 名称 值
来指定某个值,使用get 名称
来获取指定名称(key)的值,使用keys *
命令查看所有的key,flushdb
清除当前数据库,flushall清除所有数据库
2. redis6开始有多线程版本,此前只有单线程。因为redis是基于内存操作,CPU不是redis性能瓶颈,瓶颈是机器内存和网络带宽,既然可以使用单线程就使用单线程了(这里说的单线程是说工作线程是单线程,除了工作线程外还有其它的维护线程等)
3. 使用exists key
名称可以判断是否存在该key,返回1存在,0不存在
move key名称 数据库序号
可以讲指定key从当前数据库移到指定序号数据库
del key名称
移除该key
expire key名称 秒值
可以指定key的过期时间
ttl key名称
可以查看key还有多久过期
type key名称
查看key是什么数据类型
数据类型
字符串类型(string)
append key名称 其它内容
往指定key的内容后追加其他内容,如果key不存在则新建该keystrlen key名称(字符串)
返回字符串的长度incr key名称
使key加一decr key名称
使key减一incrby key名称 数值
使key增长指定数值decrby key名称 数值
使key减少指定数值getrange key start end
截取字符串,范围为[start,with];如果是0和-1会得到整个字符串setrange key offset value
将key从offset位置开始把后面长度为value长度的部分替换为value;如 key为abcde,set key 1 xx会把key改为axxdesetex key second value
设置key,同时设置其过期时间setnx key value
设置key(如果该key不存在),设置成功返回1;如果该key已存在,那么设置失败,返回值0(不会覆盖原值)mset k1 v1 k2 v2 ......
批量设置keymget k1 k2 k3 ...
批量获取key的valuemsetnx k1 v1 k2 v2 ...
批量设置key,如果这些key都不存在,这是一个原子性操作,也就是说,一旦其中有一个key是存在的,那么该操作完全不生效(一起成功或一起失败)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
,这里的key格式为:user:{id}:{field}
getset key
先获取key的值然后设置key的值,如果一开始该key不存在则会返回null
list
- 所有的list命令都以l开头
lpush key value
往一个list类型的key的头部加入值,返回当前list的长度lrange key start end
获得key中的值(是栈的方式存储的rpush key value
往key的尾部插入值lpop key
移除key头部的值rpop key
移除key尾部的值lindex key
index 获取key中指定下标的元素的值llen key
返回key的长度lrem key count value
移除key中count个valueltrim key start end
截取key中[start,end]的部分rpoplpush source destination
移除source尾部的值并将其插到destination的头部lset key index value
将key中指定下标的元素的值设为value,如果该列表不存在或key中不存在对应的下标,那么报错exists key
判断该列表是否存在linsert key before/after pivot value
往列表中的pivot值的前或后插入value- 实际上是一个链表。如果移除了所有值,空链表也表示不存在
set(集合)
- set中的值是无序的,不重复的。命令以s开头
sadd key value
往key中添加值smembers key
查看key中所有的值sismember key member
判断member是否是key中的值scard key
获取key中元素的个数srem key member1 member2 ...
移除key中指定的成员srandmember key [count]
随机获取集合中指定个数的值(count可选,没有写则获取一个)spop key
随即删除元素smove source destination member
将source中指定的元素移动到destination中sdiff key1 key2
以key1为参照,获取两个key中不同的元素sinter key1 key2
获取两个key中相同的元素(交集)sunion key1 key2
获取两个key的交集
hash(哈希)
- map集合,key中存储键值对
hset key field value
在key中设置一个键值对hget key field
获取key中某一个键对应的值hmset key field1 value1 field2 value2 ...
设置多个键值对(该命令在4.0版本后已被弃用,建议使用hset)hmget key field1 field2 ...
获取多个键值对hgetall key
获取所有键值对hdel key field1 field2 ...
删除指定键值对hlen key
获取key中键值对的个数hexists key field
判断key中是否存在键fieldhkeys key
获取所有的fieldhvals key
获取所有的valuehincrbt key field increment
让key中field字段的值自增一hsetnx key field value
如果key中不存在field则创建之,否则报错hset user:1 field value
对象的设置,类似的做法在字符串中也出现过,但用哈希来做比字符串更加适用
zset(有序集合)
- 在set的基础上,增加了一个值
zadd key score value
score表示排序zrangebyscore key -inf +inf [withscores]
根据score对value进行排序,值的取值为负无穷到正无穷(即对所有的score从小到大排序),加上withscores在输出排序时会把scores也打印出来(使用zrevrange可以降序排序)zrem key value
移除key中指定的valuezcard key
查看key中共有多少个valuezcount ley min max
统计key中score在[min,max]中的value有几个
geospatial(地理位置)
geoadd key longitude latitude member
往key中添加member,并指定member的经度纬度。两极无法直接添加。我们一般会下载城市数据,直接通过java程序一次性导入geopos key member1 member2 ...
从key中获取指定元素的位置信息geodist key member1 member2 [unit]
返回key中指定两个元素之间的距离,unit为所用的单位,m为米,km为千米,mi为英里,ft为英尺georedius key longitude latitude radius m/km/ft/mi
在key中,以给定的经纬度为中心,找出某一半径内的元素,加上with dist可以把直线距离也获取出来,加上with coord可以把经纬度获取出来,加上count 数字可以只获取指定个数的元素georadiusbymember key member radius m/km/ft/mi
在可以中,以指定成员为中心,找出某一半径内的其他成员geohash
返回一个或多个未知元素的geohash表示
geohash key member1 member2 ...
将二维的经纬度转换为一维的字符串,如果两个字符串越接近,那么表示距离越近- geo底层实现的原理其实就是zset,我们可以用zset命令来操作geo
hyperloglog(基数统计)
pfadd key element1 element2 ...
添加元素pfcount key
统计元素个数pfmerge destkey sourcekey1 sourcekey2 ...
将所有sourcekey以并集的方式合并到新的destkey- 会有一定出错率,因此如果不允许容错就使用set或者自己的数据类型
bitmaps
- 登陆/未登录,某天打卡/未打卡等两个状态的信息存储
setbit key offset value
设置key中某一个offset的值为0或1getbit key offset
查看key中指定offset的值bitcount key [start end]
返回key中所有offset有多少个对应的值为1,加上[start,end]可以设置offset的范围
事务
基本操作
- redis单条命令是保证原子性的,但事务是不保证原子性的。redis事务没有隔离级别的概念,所有的命令在事务中并没有被直接执行,只有发起执行命令的时候才会执行
- 开启事务(
multi
) -> 命令入队(即普通命令) -> 执行事务(exec
) discard
放弃事务- 编译型异常(代码有问题,命令有错),事务中所有的命令都不会被执行,如:
multiset k1 v1set k2 v2getset k3 #命令使用出错,此时将命令入列后会出错set k4 v4exec #此时执行事务也会报错get k5 #此时获取k5是空的,所有的命令
运行期异常(1/0),如果事务队列中存在违法性,那么执行命令的时候,其他命令是可以正常执行的,错误命令抛出异常,如:
set k1 "v1"multiincr k1set k2 v2set k3 v3get k3exec #此时执行完除了执行时报错的第一条命令外(自增只能用在数字)其它命令都执行成功
乐观锁
- 正常执行
set money 100set out 20watch money #监视money对象,事务执行成功后监控会自动取消掉multidecrby money 20incrby out 20exec
如果在一个事务未执行前,另外一个线程修改了watch对象,那么先watch的那个线程中的事务就会执行失败,返回空
- 如果执行失败,就要unwatch取消监视,然后重新watch监视最新的状态
Jedis
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.7.0</version></dependency>
new 一个Jedis对象即可,api方法对应上面的指令
- 事务
Jedis jedis = new Jedis("127.0.0.1",6379); Transaction multi = jedis.multi(); try { ... multi.exec(); }catch (Exception e){ multi.discard(); e.printStackTrace(); }finally { jedis.close(); }
springboot整合
1.springboot所有的配置类都有一个自动配置类,自动配置类都会绑定一个properties配置文件。redis的自动配置类为RedisAutoConfiguration,绑定的配置文件为RedisProperties(可以在maven依赖中的autoconfigure包中查看源码)
2.
@ConditionalOnMissingBean(name = {"redisTemplate"})
注解的意思是如果不存在redisTemplate这个bean那么这个类就会生效,因此我们可以自己编写这样一个类来替换掉这个类,因为这个默认的类没有过多的设置,比如序列化等
3.在springboot 2.x之后原来使用的jedis被替换为了lettuce。RedisConnectionFactory中有两个实现类分别对应jedis和lettuce,其中对应jedis的实现类中有很多内容不生效,因此不能使用。所以我们在application.properties中配置的时候都要选择lettuce的进行配置
- 依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.3.12.RELEASE</version></dependency>
- 在application.properties中设置主机以及端口号
spring.redis.host=127.0.0.1spring.redis.port=6379
@SpringBootTestclass BootsecondApplicationTests { @Autowired private RedisTemplate redisTemplate; @Test void contextLoads() { // 操作不同的数据类型,具体的操作的api与命令相同 //opsForValue() 操作字符串 //opsForHash() 操作哈希 。。。。。 redisTemplate.opsForHash(); //获取连接对象 RedisConnection connection = redisTemplate.getConnectionFactory().getConnection(); connection.flushDb(); connection.flushAll(); }}
- 自定义RedisTemplate
1.直接设置字符串的key传入一个没有序列化的对象会报一个序列化的错,因此要对对象序列化。要么传对象的json字符串,要么让对象(类)序列化,继承Serializable接口。一般都会让类去继承Serializable接口
2.自定义redis的配置类
@Configurationpublic class RedisConfig {//自定义RedisTemplate对象 @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate<String, Object> template = new RedisTemplate(); template.setConnectionFactory(redisConnectionFactory); //json序列化配置 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); //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; }}
3.将api操作封装为工具类,在工具类中使用redisTemplate完成数据库操作
redis.conf配置文件
1.单位的设置
2.配置文件对unit单位不敏感
3.可以包含其它配置文件
4.network 网络的配置
bind 127.0.0.1 # 绑定的ipprotected-mode yes #保护模式port 6379 #端口
5.general 通用配置
daemonize yes #是否以守护进程(即可以后台运行)的方式运行,默认是no,我们需要自己开启为yespidfile /var/run/redis_6379.pid #如英国以后台方式运行,我们就需要指定一个pid文件loglevel notice 日志级别logfile "" #日志的文件位置名,如果是空的那就是标准输出database 16 #默认数据库个数always-show-logo yes #是否总是展示logo
6.snapshotting 快照
在规定的时间内,执行了多少次操作则会持久化到文件(.rdb .aof)。因为redis是内存数据库,如果没有持久化,数据就会丢失
# 如果900s内至少有1个key进行了修改,就进行持久化save 900 1# 如果300s内至少有10个key进行了修改,就进行持久化save 300 10# 如果60s内至少有10000个key进行了修改,就进行持久化save 60 10000stop-writes-on-bgsave-error yes #持久化如果出错了是否继续工作rdbcompression yes #是否压缩rdb文件,是的话需要消耗一些cpu资源rdbchecksum yes #保存rdb文件的时候,发现错误时是否进行校验检查dir ./ #rdb文件保存的目录
7.replication主从复制相关配置
replicationof <masterip> <masterport> #主机的ip地址及服务所占端口号masterauth <master-password> #主机的密码(如果有)
8.security安全
requirepass 123456 #设置登陆密码,默认是没有密码
不过一般是在命令行中进行操作:
config set requirepass "123456"# 之后用ping命令不能直接登陆了,必须使用auth命令输入密码auth 123456
9.client客户端
maxclients 10000 #设置能连接上redis的最大客户端数量maxmemory <bytes> #redis配置最大的内存容量maxmemory-policy noeviction #内存到达上限之后的处理策略# volatile-lru:只对设置了过期时间的key进行LRU(默认值) # allkeys-lru : 删除lru算法的key# volatile-random:随机删除即将过期key# allkeys-random:随机删除 # volatile-ttl : 删除即将过期的 # noeviction : 永不过期,返回错误
10.append only mode(aof配置)
appendonly no #默认是不开启aof模式的,即使用rdb方式持久化,大部分情况下rdb够用了appendfilename "appendonly.aof" #持久化的文件的名字appendfsync everysec #每秒执行一次sync同步,可能会丢失这一秒的数据 #always 表示每次修改都会sync,消耗性能 #no 不执行sync,这个时候操作系统自己同步数据,速度最快
Redis的持久化
Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以Redis提供了持久化功能
RDB(Redis DataBase)
在指定的时间间隔内将内存中的数据集快照(snapshot)写入磁盘,恢复时将快照文件直接读到内存里
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能,如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加高效
RDB保存的文件是dump.rdb
RDB的缺点是最后一次持久化后的数据可能丢失,因为它需要一定的时间间隔进行操作,如果在操作过程中意外宕机了,那这最后一次修改的数据就没有了;除此之外,创建(fork)进程也会占用一定的内存空间
触发机制
save命令会直接调用RDBsave,会阻塞Redis服务器进程,知道RDB文件创建为止,在此期间服务器不能处理客户端的任何请求
执行flushall命令,退出redis(shutdown),也会触发RDB
如何恢复rdb文件
使用config get dir命令可以看到redis启动目录“/usr/local/bin”,只要在这个目录下存在dump.rdb文件,启动时就会自动恢复文件中的数据
AOF(Append Only File)
以日志的形式来记录每个写操作,将redis执行过的所有指令记录下来,读操作不记录,只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,即redis重启根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复
aof采用的是写后日志的方式,即先执行命令把数据写入内存再记录命令。写后日志好处在于,由于redis在写入日志前不会对命令进行语法检查,所以只记录执行成功的命令,避免出现记录错误命令的情况,而且在命令执行后再记录日志不会阻塞当前的写操作;缺点在于,数据可能会丢失,如果redis刚执行完命令此时发生宕机,就无法记录这些命令到文件中,导致这些命令存在丢失的风险,而且当redis把日志文件写入磁盘时还是会阻塞后续的操作无法执行的
aof修复速度以及运行效率都比rdb要慢
AOF保存的文件是appendonly.aof。关于AOF的相关配置可在配置文件中进行修改
由于aof是追加的机制,所以当文件大到一定的限制,会进行重写rewrite
aof文件的修正
如果当前使用的是aof,而aof文件有错位时,redis是启动不起来的,需要先修复这个aof文件。redis提供了一个工具完成修复操作,只需要输入执行redis-check-aof --fix aof的文件名
即可。文件正常的情况下,重启redis数据就可以直接恢复了
总结
- rdb能够在指定的时间间隔内对数据进行快照存储
- aof记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,aof命令以redis协议追加保存每次写的操作到文件末尾,redis还能对aof文件进行后台重写,使得aof文件不至于太大
- 只用来做缓存的话,如果只需要数据在服务器运行时存在,也可以不使用任何的持久化
- 同时开启两种持久化方式时,redis重启的时候会优先载入aof文件来恢复原始数据,因为通常情况下aof文件保存的数据集比rdb保存的数据集要完整;rdb的数据不实时,同时使用两者时服务器重启也只会找aof文件。但建议不要只使用aof,因为rdb更适合用于备份数据库(aof在不断变化不好备份),快速重启,而且不会有aof可能潜在的bug,留着作为以防万一的手段
关于性能 ,因为rdb文件只用作后背用途,建议只在Slave上持久化rdb文件,而且只要15分钟备份一次就够了,只保留save 900 1这条规则
如果启用了aof,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的aof文件就可以了,代价是一带来了持续的IO,二aof rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少aof rewrite的频率。aof重写的基础大小默认值64M太小了,可以设到5G以上,默认超过原大小100%大小重写可以改到适当的数值
如果不启用aof,仅靠Master-Slave Replication实现高可用性也可以,能省掉一大笔IO,也减少了rewrite时带来的系统波动。代价是如果Master/Slave同时挂掉,会丢失十几分钟的数据,启动脚本也要比较两台机器中的rdb文件,载入较新的那个
发布订阅
redis也可以实现类似于消息队列那样的消息通信,基于发布/订阅的模式
PSUBSCRIBE pattern [pattern...]
订阅一个或多个符合给定模式的频道
PUBSUB subcommand [argument [argument...]]
查看订阅于发布系统状态
PUBLISH channel message
将信息发送到指定的频道
PUNSUBSCRIBE [pattern [pattern...]]
退订所有给定模式的频道
SUBSCRIBE channel [channel...]
订阅给定的一个或多个频道的消息
UNSUBSCRIBE [channel [channel...]]
退订给定的频道
通过SUBSCRIBE命令订阅某频道后,redis-server里维护了一个字典,字典的键就是一个个频道,字典的值则是一个链表,链表中保存了所有订阅这个channel的客户端。SUBSCRIBE命令的关键就是将客户端添加到给定的channel的订阅链表中
通过PUBSLISH命令向订阅者发送消息,redis-server会使用给定的频道作为键,在它所维护的channel字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者
可以使用于实时消息系统,聊天室,订阅关注系统等应用场景,太复杂的场景就需要用到消息队列了
主从复制
主从复制,是指将一台Redis服务器的数据,复制到其它的Redis服务器,前者称为主节点(master/leader),后者称为从节点(slave/follower);数据的复制是单向的,只能从主节点到从节点。Master以写为主,Slave以读为主
默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点或没有从节点,但一个从节点只能有一个主节点
主从复制的作用:
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式
- 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余
- 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务,即写redis的时候应用连接主节点,读redis的时候应用连接从节点,分担服务器负载,尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高redis服务器的并发量
- 高可用基石:除了上述作用外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础
一般来说,至少需要三台服务器搭建集群,一主二从,因为在从机中需要进行选举。只使用一台Redis是万万不能的,原因如下:
- 从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大
- 从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G,也不能将所有内存用作Redis存储内存,一般来说单台Redis最大使用内存不应该超过20G
主从机器的设置
info replication
命令可以查看当前redis服务器的主从机器相关信息
默认情况下所有服务器都是主机,配置主从机器时只需在从机上设置其主机即可,使用SLAVEOF host port
命令进行设定,host为主机的服务器ip地址,port为主机redis在其机器上占用的端口号。使用命令进行配置是暂时性的,应该在配置文件中进行配置才能做到永久性
SLAVEOF no one
可以使自己成为主机
主从机器的部分行为
1. 主机可以写,从机只能读不能写,在从机中发出写的命令时会直接报错。主机中的数据会自动被从机保存
2. 主机断开连接,从机依旧是连接到主机的,当主机恢复正常,从机依旧可以获取到主机中的数据
3. 如果是使用命令行配置从机,从机重启后又会变回主机;只要一被设置为从机,立马就会从主机中获取主机的数据
复制原理
Slave启动成功连接到master后会发送一个sync同步命令,master接收到命令后,启动后台的存盘进程,同时收集所有接收到的用于修改数据集的命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步
全量复制:slave在接收到数据库文件数据后,将其存盘并加载到内存中
增量复制:master继续将新的所有收集到的修改命令依次传给slave,完成同步
只要是重新连接master,一次完全同步(全量复制)将被自动执行
哨兵模式
主从切换的方法是,当主服务器宕机后,手动把一台从服务器切换为主服务器,这时需要人工干预,不仅费时费力还会造成一段时间服务不可用。为了解决这个问题,提出了哨兵模式(Sentinel)
Redis提供了哨兵的命令,哨兵是一个独立运行的进程,通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例;当哨兵监测到master宕机,会自动将Slave切换成master,然后通过发布订阅模式通知其它的从服务器,修改配置文件,让它们切换主机
不过实现哨兵模式的配置项非常多,非常麻烦
多哨兵模式
然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此可以使用多个哨兵进行监控,同时各个哨兵之间还会进行监控,这样就形成了多哨兵模式
在多哨兵模式中,假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观地认为主服务器不可用,这个现象称为主观下线,当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover[故障转移]操作,切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线
如果主机断开连接,且新的主机已经被选取出来,此时主机再恢复连接,只会称为新的主机的从机
Redis缓存穿透和雪崩
Redis缓存的使用,即除了使用MySQL等持久层数据库外,使用Redis作为数据的缓存,极大地提升了应用程序的性能和效率,特别是数据查询方面,但同时它也带来了一些问题,其中最严重的就是数据的一致性问题,从严格意义上讲,这个问题没有解决方案,如果对数据的一致性要求很高,那么就不能使用缓存。除此之外,还有其他的典型问题如缓存穿透,缓存雪崩,和缓存击穿
缓存穿透
用户想要查询一个数据,发现Redis内存数据库中没有,即缓存没有命中,于是向持久层数据库查询,发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库,这会给持久层数据库造成很大压力,这时就称为出现了缓存穿透
解决方案:
1.布隆过滤器
2.缓存空对象:当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据会从缓存中获取,保护了数据源。这种方法会存在两个问题:如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,而这当中可能会有很多空值的键;即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响
缓存击穿
缓存中有一个key非常热点,在不停地扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障(缓存)上凿开了一个洞
解决方案:
1.设置热点数据永不过期
2.加互斥锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其它线程没有获得锁的权限,需要等待。这种方式将高并发的压力转移到了分布式锁,对分布式锁的考验很大
缓存雪崩
缓存雪崩是指在某一个时间段内,缓存集中过期失效。但更为致命的缓存雪崩是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间端集中创建缓存,这个时候,数据库也是可以顶住压力的,无非就是对数据库产生周期性的压力而已,而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮
解决方案:
1.redis高可用:既然redis有可能挂掉,那就多设置几台redis,搭建集群,这样一台挂掉之后其它的还可以继续工作
2.限流降级:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量,比如对一个key只允许一个线程查询数据和写缓存,其它线程等待
3.数据预热:在正式部署之前,先把可能的数据预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中,在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀