Redis

一、redis的下载与安装(Linux操作系统)

1、 redis下载

可以在redis官网的下载界面下载自己需要的版本,将下载后的文件copy到自己需要保存的目录下,这里我们放在/usr/local/reids文件夹中。也可以先将当前目录切换到/user/local/redis中,然后执行wget https://download.redis.io/redis-stable.tar.gz命令。(注意,下载文件需要root权限,使用su root命令进行切换)

2、 redis安装

使用tar -xzvf redis-stable.tar.gz命令解压,然后使用cd redis-stable切换到解压后的文件夹下,最后使用make命令进行编译(需要安装gcc编译器,使用yum install gcc安装即可)。

二、redis的基本命令

先切换到redis-stable目录下

./src/redis-server redis.conf 使用redis.conf配置文件来开启reids服务

上面的方法在关闭了终端后,redis服务也会关闭。可以使用守护进程的方法来开启redis服务,在关闭终端后redis服务仍开启。修改配置文件,将daemonize no改为daemonize yes。重写开发服务,就会以守护进程方式开启。
使用redis客户端

./src/redis-cli -p 6380 客户端连接到指定端口的服务

关闭redis 服务
在开启redis客户端后输入:

shutdown

或者直接输入

./src/redis-cli -p 6380 shutdown

先使用redis客户端

select 0 使用0号数据库
dbsize 查看数据库的大小
flushdb 清空当前数据库
flushall 清空所有数据库
keys * 查看当前数据库有多少键
exists key 判断对应的键存不存在
type key 参考key是什么类型
del key 删除对应的key
exprie key time 设置对应的key在time秒后过期
ttl key 查看key在多少秒后过期

Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)
不过需要注意的是,Redis中的数据都是以键值对(key-value)的形式存放的,上面的五种类型只是value的类型。
String类型的基本命令

set age 24 创建一个键为age值为24的键值对
get age 获取age对应的值
incr age 设置age对应的值加1
decr age 设置age对应的值减1
incrby age 3 设置age对应的值加3
decrby age 3 设置age对应的值减3
mset name zhangsan sex male 设置name为张三,sex为male
mget name sex 获取name,sex对应的值

hash类型的基本命令

hset student name zhangsan 设置student对应的hash表中的字段name的值为zhangsan
hget student name 获取student对应的hash表中国的字段name的值
hmset student name zhangsan sex male age 23 同时设置student对应的hash表中的多个字段
hgetall student student 获取student的所有字段及对应值

List类型的基本命令

lpush students zhangsan 向students对应的列表左插入zhangsan
rpush students lishi 向students对应的类别右插入lishi
lpop studetns 弹出最左侧的元素
rpop studetns 弹出最右侧的元素
lrange students 0 -1 查询所有元素

set类型的基本命令

sadd class class1 class2 class3 将class1,class2,class3添加到class集合中
smembers class 查询class集合中的元素
sunion tclass sclass 取并集
sinter tclass scalss 取交集
sdiff tclass sclass 取差

zset类型的基本命令,zset中每个元素都关联一个分数,集合中的元素安装分数从低到高排列

zadd student 100 zhangsan 80 lishi 90 wangwu 创建student集合并且指定每个值得分数
zrange student 0 -1 withscores 返回说哟元素并且附带分数
zrangebyscore student 80 90 返回80到90得分之间的元素

setnx(SET if Not EXists):与set命名类似,但如果这个键已经存储则设置失败。可以使用setnx配置expire命名实现分布式锁,下面以lettuce为列进行说明。使用setnx作为锁,为了防止线程出现异常后锁得不到释放,需要为这个键设置一个过期时间。但有可能出现误删锁的现象,一个线程加的锁过期了但这个线程仍在执行,另一个线程又加了锁,当第一个线程尝试删除锁时会误删第二个线程加的锁。为每个线程的setnx的值生成一个随机值(版本号),需要删除锁后比较版本号是否改变,如果改变说明其他线程已经加锁。

ValueOperations vop = redisTemplate.opsForValue();
//为当前线程生成一个随机值
String ticket = UUID.randomUUID().toString();
//第一参数是键,第二个参数是一个随机值,第三个参数是过期时间,第四个参数是第三个参数的单位
Boolean lock = vop.setIfAbsent("k1", ticket, 2, TimeUnit.SECONDS);
if(lock){
	System.out.println("加锁成功");
	vop.set("name", "xxx");
	String name = (String) vop.get("name");
    try {
		Thread.sleep(3000);
	} catch (InterruptedException e) {
		e.printStackTrace();
   	} finally {
   		//解锁时有可能当前锁已经过期,并被其他线程解锁,需要防止误删其他线程加的锁通过比较值是否相等就可知道是否被其他线程加锁
		if(!ticket.equals(vop.get("k1")) || !redisTemplate.delete("k1")) 
			System.out.println("线程1解锁失败");
		}
}else {
	System.out.println("加锁失败");
}
``


# 三、jedis
jedis是一款java操作redis数据库的工具,使用jedis只需要添加jedis依赖即可。在使用jedis连接redis服务之前,我们需要对redis进行配置。

 - 修改配置文件
将bind 127.0.0.1 改为bind 0.0.0.0,将protected-mode yes改为protected-mode no
 - 关闭防火墙
关闭对指定端口的防火墙firewall-cmd --zone=public --add-port=6380/tcp --permanent,重新启动防火墙firewall-cmd --reload
```java
Jedis jedis = null; 
        try {
        	jedis = new Jedis("192.168.170.128", 6380);	//连接redis服务
            jedis.rpush("letter", new String[]{"aaa", "bbb", "ccc"});	//jedis的命令与redis相同
            List<String> list = jedis.lrange("letter", 0, -1);
            System.out.println(list);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
       		if(jedis != null)
            	jedis.close();	//关闭连接
        }

四、Redis持久化

持久化:将数据存储在永久性存储介质上,在特定时间进行数据恢复的机制。
持久化的意义:防止数据丢失,确保数据安全。
持久化的两种方式:RDB和AOF

  • RDB(Redis Database):将数据快照(某一时刻数据库中的数据)保存到数据库中
    (1)执行save命令:手动保存数据。由于redis时单线程的,当save命令的执行时间过长时会导致redist的阻塞。
    (2)执行bgsave命令:手动保存数据。不同于save,bgsave会开启一个子进程,由子进程进行数据的读写。
    (3)配置文件:在配置文件中添加save second changes ,当second秒内,写操作执行的次数达到changes会将数据快照写入永久性存储介质,使用bgsave写入。
  • RDB的优点与缺点
    (1)优点:
    数据恢复速度快(只需要载入数据,不需要重新执行命令)
    适用于数据恢复,全量复制
    当数据量少时, 数据体积小(进行了二进制文件的压缩),存储效率高
    (2)缺点:无法做到实时持久化,可能会产生数据丢失。
  • AOF(Append Only):将每次的写命令记录到单独的日志文件中,当进行数据恢复时执行日志中的命令
    (1)开启AOF:在配置文件中添加appendonly yesappendfsync always|everysec|no
    (2)AOF的三种持久化策:always每进行一次写操作就将写操作命令保存到AOF文件中,everysec每秒保存一次写命令,no由操作系统进行控制。
    (3)AOF重写:当aof文件过大时,只保存能够恢复数据的最小指令集(通过删除无效命令,合并命令等方式实现)。redis4.0以后会将二进制数据保存到aof文件头部。
    (4)AOF重写流程:
    <1>当文件达到阈值时,判断是否由bgsave或bgrewriteaof在执行,如果有则等待。
    <2>创建一个子进程,子进程完成文件的重写。
    (5)文件重写命令
    <1>手动重写:执行bgrewriteaof命令
    <2>自动重写:
    auto-aof-rewrite-min-size size:当aof_current_size大于size时开始重写
    auto-aof-rewrite-percentage percent:当 s i z e − b a s e s i z e b a s e s i z e ≥ p e r c e n t \frac{size-base_size}{base_size}\geq percent basesizesizebasesizepercent时开始重写。
    (6)AOF工作流程
    <1>客户端的写命令会被追加到AOF的缓冲区中
    <2>AOF缓冲区根据AOF的持久化策略将缓冲区中的内容同步到磁盘中
    <3>AOF文件大小超过重写策略或者手动重写时会将AOF文件进行压缩
    (7)AOF的优缺点
    <1>优点:实时性高,单词执行效率高,数据安全
    <2>缺点:AOF文件可能会非常大,数据恢复时效率相对较低

五、事务

redis的事务就是命令的一个执行队列,这命令的执行队列必须一次性执行不可被打断。
(1)事务的基本操作
<1>开启、执行与取消事务

multi	//开启事务
discard //取消事务
exec 	//执行事务

需要注意的是,如果在执行事务之前的命令有语法错误会对事务进行回滚,但如果运行时错误不会进行回滚并且发生错误位置的后续代码也会被执行。
<2>乐观锁与分布式锁
watch配合事务可以实现乐观锁

watch name
multi
set name lishi
exec

如果在执行事务之前name发生了改变,那事务就不会执行。取消对变量的监视可以使用unwatch取消对所有变量的监视。
redis中的setnx可以实现分布式锁,setnx name zhangsan进行键值对的设置时,如果name这个键在之前就已经存在则设置失败,否则设置成功。

六、删除算法

redis可以使用expire等命令来指定某个数据的有效期,redis会在内存中开辟一块存储区域(类似与二维表),左边存放着数据的存储地址,右边存放着过期时间。
在这里插入图片描述

  • 过期删除策略
    (1)定时删除:每隔一段时间删除过期数据,CPU压力大,内存压力小
    (2)惰性删除:当访问一个数据,发现它过期时才进行删除,CPU压力小,内存压力大
    (3)定期删除算法:上面两种算法的综合。
    将expires分为一个一个的块在这里插入图片描述
    每秒中执行server.hz(默认值为10)次检查,每次检查250ms/server.hz个块(循环检查,从上次未检查的位置开始),检查每个块时随机选W个key检测,如果删除的key的个数 > W*25%则下一个块仍然为当前块,否则选择下一个块。
  • 逐出算法
    当内存不够时,会对过期数据进行回收,但回收过后内存可能仍然不够用,这是需要使用逐出策略对某一些数据进行逐出。
    逐出策略:
    (1)检测易失数据(expires中的数据)
    ① volatile-lru:挑选最近最少使用的数据淘汰
    ② volatile-lfu:挑选最近使用次数最少的数据淘汰
    ③ volatile-ttl:挑选将要过期的数据淘汰
    ④ volatile-random:任意选择数据淘汰
    (2)检测全库数据(所有数据集server.db[i].dict )
    ⑤ allkeys-lru:挑选最近最少使用的数据淘汰
    ⑥ allkeys-lfu:挑选最近使用次数最少的数据淘汰
    ⑦ allkeys-random:任意选择数据淘汰
    (3)放弃数据驱逐
    ⑧ no-enviction(驱逐):禁止驱逐数据(redis4.0中默认策略),会引发错误OOM(Out Of Memory)

七、主从复制

  • 基本概念
    创建一个主节点多个从节点,主节点负责写操作,从节点负责读操作。其中最关键的是实现数据的同步,也就是主节点的数据与从节点的数据保持一致。
  • 作用
    (1)实现读写分离,提高负载能力
    (2)实现负载均衡,提高并发量和吞吐量
    (3)实现数据冗余,提高故障恢复的能力
    (4)高可用的基础
  • 三个阶段
    (1)建立连接
    在这里插入图片描述
    主要就是保存彼此的端口号。slave使用slaveof no one命令断开连接。
    (2)数据同步阶段
    在这里插入图片描述
    主要分为全量复制和部分复制(增量复制)两个阶段,全量复制阶段slave得到master的所有数据,然而在进行复制的这个过程中master有可能还在执行写命令,这时就需要将缓冲区中的写操作发送给salve。
    (3)数据传播阶段:
    <1>在完成数据同步阶段后,当master执行写操作后需要将这些写操作发送给slave。
    在这里插入图片描述
    master复制偏移量(offset):记录发送给所有slave的指令字节对应的位置(多个)
    slave复制偏移量(offset):记录slave接收master发送过来的指令字节对应的位置(一个)
    在进行命令传播阶段(上图中的部分复制应该为命令传播),master判断slave发送来的runid,如果这个runid不在master存放的列表中说明这个slave它没见过,需要进行全量复制。如果offset已经超出了缓冲区存储的范围(这个命令已经不在缓冲区中),也需要进行全量复制。如果master的offset与slave的offset不同并且slave的offset在缓冲区的范围内,说明数据不同步,只需要发送给slave它需要的命令。
    <2>心跳机制:进入命令传播阶段,master与slave使用心跳机制实现双方保持在线
    master心跳:每隔一段时间执行ping指令判断slave是否在线
    slave:每隔一段时间执行REPLCONF ACK {offset},汇报自己的offset获取数据,判断master是否在线。
    (4)主从复制常见问题
    <1>当slave发送的offset不在缓冲区的范围中时会进行全量复制,如果完成全量复制后offset又不在缓冲区范围内(全量复制时间过长,并且缓冲区过小可能会导致这个现象)会导致重写进行全量复制,有可能造成死循环。

八、哨兵(sentinel)模式

  • 哨兵:一个分布式系统,用来对主从结构中的每台服务器进行监控,当master宕机时选择一个slave作为master,并让所有的slave连接上这个master。
    哨兵也是redis提供的一个服务,这个服务位于src目录下,可以使用server-sentinel来开启这个服务。在sentinel的配置文件中,通过sentinel monitor mymaster 127.0.0.1 6379 2来指定监视的master的端口,并且指定投票达到2时就认为master宕机了。
    哨兵工作的三个阶段:监控、通知和故障转移
  • 监控:哨兵与master,slave建立连接,哨兵彼此之间建立连接。
    第一个哨兵与master建立连接时,哨兵可以获得slave的信息,master也将会保存哨兵的信息。
    第二个哨兵与mater建立连接时,第二个哨兵可以获得第一个哨兵,由此哨兵彼此之间也可以建立连接。
  • 通知
    哨兵获取master的信息,并将信息在哨兵彼此之间进行传播
  • 故障转移
    在一个sentinel认为master下线时,master就处于主观下线的状态。sentinel发起一个投票,如果投票数达到或者超过了设置的阈值就会认为master是真的下线时,这是master就处于客观下线的状态。sentinel之间会进行投票,选出一个进行故障处理。被选出的sentinel会从slave中选出一个节点,这个节点作为master。被选出的节点执行slaveof no one,断开与master的连接。然后其他的节点执行slaveof 新masterIP端口建立与它的连接。选master通过以下准则:
    <1>选在线的
    <2>选择响应快的
    <3>选择与master 最近建立连接的
    <4>优先原则:优先级高的,offset小的,runid小的

九、集群

将多个计算机联通起来,提供统一的管理方式,使其对外提供单机的作用。机器由多个节点构成,每个节点有一个master和若干个slave。
集群可以分散单台服务器的负载压力,实现负载均衡。
集群可以减轻单台服务器的存储压力,实现可扩展性。
降低单台服务器宕机造成的灾难。

  • 集群的工作原理
    <1>将所有的存储空间分为16384份(一份叫做一个槽位),每个计算机上保存几个槽位。每个计算机上都保存着一份槽位表,记录哪些槽位分布在哪些计算机上。
    <2>对于每个key,通过CRC(key)%1683获取它所在的槽位。
    <3>进行读写操作时,访问任意一台计算机,如果这台计算机上有对应的槽位就在这台计算机上进行读写,否则查找槽位表,找到对应的计算机。

  • 集群扩容
    <1>启动一个redis节点M
    <2>让M节点准备导入槽数据
    <3>源节点准备迁出数据
    <4>源节点迁出数据
    <5>通知所有主节点,更新槽位表

  • 集群收缩
    <1>将下线节点的槽迁出到其他节点
    <2>通知其他节点忘记下线节点

  • 集群故障转移
    与哨兵故障转移机制类似,这里每个集群节点都是一个哨兵

  • 集群实现方式

十、企业级解决方案

  • 缓存雪崩:大量的key集中过期,数据库请求压力过大崩溃,大量的请求积压在redis,进一步导致redis崩溃。
    解决方案:
    <1>设置随机过期,避免同时过期
    <2>当并发量不高时,使用加锁排队
  • 缓存穿透:redis中大面积未命中(访问的数据数据库中也没有),出现了许多无效访问(有可能是黑客攻击),导致数据库崩溃,redis崩溃。
    解决方案:
    <1>增加对用户的校验
    <2>添加一个key-value,key为用户访问的key,value为null。
    <3>采用布隆过滤器或者布谷鸟过滤器,过滤掉无效访问
  • 缓存击穿:某个热点key过期,大量请求都查询这个key,大量进行数据库的访问,导致数据库崩溃。
    解决方案:
    <1>设置热点数据不过期
    <2>加互斥锁
  • 缓存预热:在系统启动时就将数据库中的数据加载到redis中,避免用户去查询数据。

十一、底层实现原理

一、基本数据结构

  • 动态字符串SDS:
    <1>基本结构:
    在这里插入图片描述
    <2>动态扩容:如新字符串小于为1M,长度为原来长度2倍加1;如果新字符串大于1M,扩容长度为原来长度+1M+1
    <3>优点:支持动态扩容;获取长度的时间复杂度为O(1);减少内存分配次数;二进制安全(不会下表越界,可以存放特殊字符)

  • IntSet:
    <1>结构:
    在这里插入图片描述
    <2>特点:数据是唯一且有序的;具备升级机制(也就是更改flags,设置元素的大小分,分别由2,4,8B,当有元素过大时,修改大小以容纳所有元素);采用二分查找查询。

  • Dict
    <1>结构:
    在这里插入图片描述

在这里插入图片描述 在这里插入图片描述
<2>与java中的HashMap表基本相同,主要不同点在于扩容与收缩(java没有收缩)。
当负载因子>1并且没有bagsave等后台进程执行时扩容,当负载因子大于5时扩容。采用渐进式扩容,有两个表,一个为空。在扩容时,每执行一次查询或修改操作时,将元素表中的一个Etry链表扩容到令一个空的表中。
当负载因子<0.1时,进行收缩操作,每执行一次查询或修改操作时,将元素表中的一个Etry链表扩容到令一个空的表中。

  • ZipList
    <1>结构:
    在这里插入图片描述

根据ZLtail可以获取尾节点的值,每个节点previous_entry_length可以获取前一个节点,previous_entry_length在前一个节点小于254字节时占一个字节,否则占5字节。
<2>特点:
是一种连续内存的双向链表。
不使用指针而通过记录前一个节点长度来遍历(占用内存小,指针占八个字节而保存长度最多占五个字节。
增删大数据时可能会发生连续更新问题。

  • QuickList
    <1>结构:将多个ZipList连接起来。QuickList可以对节点进行压缩,通过list-compress-depth配置项完成,0表示不压缩,1表示首位各有1个不压缩中间压缩,2表示首尾各有两个不压缩中间压缩。
    在这里插入图片描述
    <2>特点:
    解决了ZipList的连续内存占用问题
    控制了ZipList的大小,提高了内存申请效率(小块内存申请效率高)
    可以压缩中间简单,减少内存占用
  • SkipList
    <1>结构:是一种特殊的双端链表,每个节点由score(分数)和ele(存储的数据)组成,根据score从下到大排列。具有不同层指针,层数越高间隔越大。
    在这里插入图片描述

在这里插入图片描述
<2>特点:查找效率高,实现简单(这也是为什么不使用红黑树的原因)。

  • RedisObject:Redis中所有的数据都会封装为RedisObject。
    <1>结构:
    在这里插入图片描述
    type对应着五大基本类型,encoding对应着编码(也就是用那种底层数据结构实现,对应关系如下图)。

在这里插入图片描述
二、五大基本数据类型和底层实现
五大基本数据类型都属于RedisObject,只是编码方式不同

  • String:有三种编码方式,分别是RAW,EMBSTR和INT
    <1>RAW:指针执行一个SDS字符串,当存储数据大于44字节使用
    在这里插入图片描述
    <2>EMBSTR:将SDS拼接在ReidsObject后面占用连续内存,小于44字节时使用,总共占用64字节正好是一个内存分片
    在这里插入图片描述
    <3>INT:当字符串是整数并且小于Long.MAX时,直接存储在指针上。在3.2之前,如果数量小于512并且元素大小小于64B采用ZipList,否则采用LinkedList。在3.2之后采用QuickList。

在这里插入图片描述

  • List:在3.2之前如果元素个数小于512大小小于64B采用ZipList,否则采用LinkedList。3.2之后采用QuickList。
  • Set:元素为整数且个数不超过set-max-intset-entries时采用IntSet,否则采用HT编码(值为null,类似于HashTable)。
  • Zset:当元素个数小于128大小小于64B时,采用ZipList(Zset自己实现排序)
    在这里插入图片描述
    否则,采用HT+SkipList实现,HT实现按元素查找分数,SkipList实现有序
    采用SkipList而不采用红黑树和B+树的原因:
    1、对内存占用没有想象的严重
    2、顺序遍历比较方便
    3、实现简单,容易修改
    在这里插入图片描述
  • Hash:当元素个数小于512并且大小小于64B时采用ZipList
    在这里插入图片描述

否则采用HT编码
在这里插入图片描述

十二、Redis网络模型

一、五种IO

  • 阻塞IO:在数据尚未九尾时,用户线程等待数据、内核态也需要等待数据
    在这里插入图片描述

  • 非阻塞IO:在数据尚未就位时,内核态需要等待数据,用户一直尝试获取数据
    在这里插入图片描述

  • IO多路复用:利用单个线程来监控多个FD(文件描述符),当某个文件描述符就位时拷贝数据。
    在这里插入图片描述
    <1>select:利用一个二进制数据来标志需要监听的FD(需要监听的标志为1),内核态遍历这些FD,如果有FD就绪就这个二进制数据对应位上标志为1并返回这个二进制位,用户态再遍历这个二进制位,判断哪些FD就绪了。
    在这里插入图片描述
    在这里插入图片描述
    <2>过程与select基本相同,poll采用了链表存储要监控的FD,扩大了监控范围。
    <3>首先创建一个epoll(内部也就是eventpoll)然后将需要监听的数据添加到epoll的红黑树上,当有FD就绪时就放入rdList(list-Head)下,等待就绪,如果rdList不为空就将对应的FD拷贝到用户空间。
    在这里插入图片描述

  • 信号驱动IO
    在这里插入图片描述

  • 异步IO:通知内核态拷贝数据后,用户态做其他事情。
    在这里插入图片描述

二、Redis网络模型:使用IO多路复用 + 事件派发进行网络IO,整个过过程中最耗时的就是请求命令写入querBuf、将queryBuf解析为redis命令,还有将执行结果写回。因此这几部分采用了多线程来处理。
在这里插入图片描述

Redis与数据库的一致性

缓存与数据库的一致性

  • 缓存有数据,缓存和数据库的数据相同
  • 缓存没有数据,数据库中的数据是最新值

缓存与数据库的不一致性

不属于上面情况的都属于缓存不一致

缓存的读写模式

  • 读写缓存:先写缓存,然后再写DB
    <1>同步直写:写缓存的同时写DB。
    <2>异步写回:写缓存时先不写DB,等数据从缓存淘汰时再写回。
  • 只读缓存
    <1>新增数据:直接写DB。由于缓存中没有对应的数据,一定会去数据库中取数据并且只有取数据成功才会获得数据,因此不会出现缓存一致性问题。
    <2>删改数据:
    1、先删缓存,再更新DB。再删除缓存后更新DB失败,如果应用读取数据到缓存中就会导致缓存和DB的不一致性。
    在这里插入图片描述

2、先跟新DB,再删除缓存。更新DB之后,如果删除缓存失败就会导致缓存和DB的不一致性。
在这里插入图片描述

  • 将只读缓存当作读写缓存使用:在更新数据库之后,不再是删除缓存而是直接更新缓存
    <1>无并发
    1、先更新数据库再更新缓存:若更新DB成功,但Cache更新失败,此时DB最新值,但缓存旧值,后续读请求会直接命中缓存,得到旧值。
    2、先更新缓存再更新数据库:先更新数据中缓存,再更新数据库。如果更新失败,再缓存过期后数据库中的数据将得不到更新。可以使用MQ不断进行消息重试。
    <2>并发读写
    1、读写并发
    (1)先更新据库再跟新缓存:再缓存没有更新的这段时间读到的是旧值。
    (2)先更新缓存再更新数据库:数据库会和redis短暂的不一致
    2、写写并发
    (1)先更新据库再跟新缓存:线程A和B同时更新数据库,更新数据库的顺序是A B更新缓存的顺序是B A这样会导致缓存不一致
    (2)先更新缓存再更新数据库:更新缓存的顺序是A B,更新数据库的顺序是B A,这样也会导致缓存不一致性。
    对于写写并发,可以使用分布式锁,使得同一时刻只有一个线程能够修改缓存和数据库。写写并发可能会出现写失败的情况,可以通过MQ来重试。

解决只读缓存删改数据缓存不一致性问题

  • 无并发情况下
    将要删改的Cache值或者DB的值存在MQ中,然后删改cache或者DB,删除成功则从MQ中删掉对应的消息,如果失败则不断重试。
  • 高并发情况下
    <1>先删缓存再更新DB:再高并发情况下上面的解决方案仍然会出现缓存不一致,分为两种情况,删除缓存成功和删除缓存失败
    1、删除缓存成功:在t1时刻线程T1删除缓存,t2时刻线程T2读取数据库中未修改值到缓存中,t3时刻T1删改数据库。这样缓存中存放的是旧值。
    解决方案:延迟双删 + 设置缓存过期时间。
    延迟双删:先删除一次缓存,在修改数库后过一段时间再删除一次缓存。
    为什么要过一段时间再删:从数据库中读取数据到缓存需要一段时间,如果更新数据库后立即删除缓存,之前从数据库中读取的缓存还有可能读到缓存中。
    时间如何确定:时间应该大于读数据和写缓存的时间。
    设置缓存的过期时间:当过了一段时间后缓存过期,这样就会去缓存中取最新数据,最多就是缓存过期时间内数据不一致。
    2、删除缓存失败:如果删除缓存失败上面的方案也会出现缓存不一致的问题。
    (1)在方案1的基础上使用MQ重试删除
    更新DB - > 删除cache失败 - > 将消息发送给MQ - > 自己消费消息,重新尝试删除 - > 重试删除直到成功
    (2)方案1对业务代码的侵入性太强。可以采用binlog + 订阅程序解决
    更新DB数据 - > DB会将操作信息写入binlog日志 - > 订阅程序提取出所需要的数据及K - > 另起一段非业务代码,获得该信息
    尝试删除Cache操作,发现删除失败将这些信息发送至MQ - > 重新从MQ获得该数据,重试删除操作
    <2>先更新DB再删除缓存:由于先更新DB,通过尝试删除缓存直到删除缓存成功,这样缓存中存放的一定是最新的数据。

总结

在这里插入图片描述
https://zhuanlan.zhihu.com/p/450576104

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值