分享下我的redis八股笔记
[1]Redis介绍
1.键值型与NoSql
Redis是一种键值型的NoSql数据库,这里有两个关键字:
- 键值型
- NoSql
其中键值型,是指Redis中存储的数据都是以key.value对的形式存储,而value的形式多种多样,可以是字符串.数值.甚至json:
而NoSql则是相对于传统关系型数据库而言,有很大差异的一种数据库。
对于存储的数据,没有类似Mysql那么严格的约束,比如唯一性,是否可以为null等等,所以我们把这种松散结构的数据库,称之为NoSQL数据库。
2. Redis数据类型
1 五种常用数据类型介绍
Redis存储的是key-value结构的数据,其中key是字符串类型,value有5种常用的数据类型:
- 字符串(string):普通字符串,Redis中最简单的数据类型,string的内部结构实现上类似Java的ArrayList
- 哈希(hash):也叫散列,类似于Java中的HashMap结构
- 列表(list):按照插入顺序排序,可以有重复元素,类似于Java中的LinkedList,底层是双向链表
- 集合(set):无序集合,没有重复元素,类似于Java中的HashSet
- 有序集合(sorted set/zset):集合中每个元素关联一个分数(score),根据分数升序排序,没有重复元素
1.zset的底层原理
Redis的有序集合(Zset)底层采用两种数据结构,分别是压缩列表(ziplist)和跳跃表(skiplist)。
- 当Zset的元素个数小于128个且每个元素的长度小于64字节时,采用ziplist编码。在ziplist中,所有的key和value都按顺序存储,构成一个有序列表。这种实现方式主要是为了节省内存,因为压缩列表是一块连续的内存区域,通过紧凑的存储可以有效地利用空间。虽然压缩列表可以有效减少内存占用,但在需要修改数据时,可能需要对整个列表进行重写,性能较低。
- 跳表是一种多层次的链表结构,通过多级索引提升查找效率。在不满足使用压缩列表的条件下,Redis会采用跳表作为Zset的底层数据结构。跳表能够提供平均O(logN)的时间复杂度进行元素查找,最坏情况下为O(N)。跳表中的每一层都是一个有序链表,并且层级越高,链表中的节点数就越少,从而允许在高层快速跳过一些元素,达到快速定位的目的。
综上所述,Redis的Zset通过灵活地使用压缩列表和跳跃表作为底层数据结构,在不同的场景下平衡了内存使用效率和数据操作性能。这两种数据结构各有优劣,压缩列表适用于数据量小、内存受限的场景,而跳跃表适合于数据量大、需要高效操作的环境。
什么是跳表?
跳表(Skip List)是一种基于有序链表的数据结构,通过多级索引的方式实现高效的查找、插入和删除操作。
跳表以空间换时间的方式优化了传统单链表的效率。在单链表中,即使数据是有序的,查找一个元素也需要从头到尾遍历整个链表,时间复杂度为O(n)。而在跳表中,通过建立多层索引来实现快速查找。顶层索引链表的节点数量远少于底层原始链表,并且层级越高,节点越少。
跳表中的每一层都是一个有序链表,并且每个节点都包含指向同层级下一个节点的指针以及指向下一层对应节点的down指针。例如,当查找一个元素时,首先在顶层索引进行查找,如果当前节点的值大于要查找的值,则继续在同一层级向右移动;如果小于要查找的值,则通过down指针下沉到下一层继续查找。每下降一层,搜索范围就缩小一半,最终在底层链表中找到目标元素或者确认元素不存在。
跳表的插入和删除操作同样高效,其时间复杂度也是O(logn)。向跳表中插入新元素时,首先要找到合适的插入位置,保持链表的有序性。然后通过随机函数决定新节点应该出现在哪些层级的索引中:随机结果高于某个固定概率p,就在该层级插入新节点。删除操作类似,先找到要删除的节点,然后在所有包含该节点的层级中移除它。
2.hash的底层原理
Redis的Hash数据结构底层原理主要基于两种数据结构:ziplist和hashtable。
具体来说,这两种数据结构的应用如下:
- ziplist:当满足特定条件时(键和值的字符串长度都小于64字节,且键值对数量少于512),Hash数据结构会采用ziplist作为其底层实现。在ziplist中,所有的key和value都按顺序存储,构成一个有序列表。这种实现方式主要是为了节省内存,因为压缩列表是一块连续的内存区域,通过紧凑的存储可以有效地利用空间。
- hashtable:当不满足ziplist的条件时,Hash数据结构会使用hashtable作为底层实现。在hashtable中,每个键值对都以字典的形式保存,其中字典的键为字符串对象,保存了原键值对的键;字典的值为另一个字符串对象,保存了原键值对的值。这样的结构允许快速的查找、插入和删除操作。
此外,在Hash数据结构中,如果ziplist编码所需的两个条件中的任意一个不再满足时,会发生编码转换,即原本保存在ziplist中的所有键值对会被转移到字典中,对象的编码也会从ziplist变为hashtable。这通常发生在键的长度过大、值的长度过大或者键值对的数量过多的情况下。
综上所述,Redis的Hash数据结构根据数据的规模和访问模式灵活地在ziplist和hashtable之间切换,以达到既节省内存又保证访问效率的目的。
2.3种redis特殊数据类型
Bitmap (位图)
Bitmap 存储的是连续的二进制数字(0 和 1),本来int数字占4字节32位,但通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态(比如:01表示1,001表示2) 。,所以 Bitmap 本身会极大的节省储存空间。
# 将名为myBitmap的bitmap的第5位设置为1 SETBIT myBitmap 5 1 //SETBIT key offset value
你可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做 offset(偏移量)。
HyperLogLog(基数统计)
HyperLogLog 是一种有名的基数计数概率算法 ,基于 LogLog Counting(LLC)优化改进得来,并不是 Redis 特有的,Redis 只是实现了这个算法并提供了一些开箱即用的 API。
Redis 提供的 HyperLogLog 占用空间非常非常小,只需要 12k 的空间就能存储接近2^64
个不同元素。这是真的厉害,这就是数学的魅力么!并且,Redis 对 HyperLogLog 的存储结构做了优化,采用两种方式计数:
- 稀疏矩阵:计数较少的时候,占用空间很小。
- 稠密矩阵:计数达到某个阈值的时候,占用 12k 的空间
Geospatial (地理位置)
Geospatial index(地理空间索引,简称 GEO) 主要用于存储地理位置信息,基于 Sorted Set 实现。
通过 GEO 我们可以轻松实现两个位置距离的计算、获取指定位置附近的元素等功能。
数值范围0-40亿的数如何排序(bitmap)
使用Bitmap进行排序是一种特殊的方法,适用于处理大量数据的排序问题,尤其是在内存有限的情况下。以下是使用Bitmap排序的步骤:
- 初始化Bitmap:根据数值范围创建一个足够大的Bitmap。由于数值范围是0-40亿,Bitmap的大小需要能够覆盖这个范围,即至少需要40亿位。
- 标记数值:遍历待排序的数值列表,将每个数值在Bitmap中对应的位置标记为1。例如,如果数值是5,则在Bitmap的第6位(从0开始计数)标记为1。
- 按位输出:按照Bitmap的顺序,输出所有标记为1的位置对应的数值,即可得到排序后的结果。
4.String 还是 Hash 存储对象数据更好呢?
- String 存储的是序列化后的对象数据,存放的是整个对象。Hash 是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 就非常适合。
- String 存储相对来说更加节省内存,缓存相同数量的对象数据,String 消耗的内存约是 Hash 的一半。并且,存储具有多层嵌套的对象时也方便很多。如果系统对性能和资源消耗非常敏感的话,String 就非常适合。
在绝大部分情况,我们建议使用 String 来存储对象数据即可!
5.购物车信息用 String 还是 Hash 存储更好呢?
由于购物车中的商品频繁修改和变动,购物车信息建议使用 Hash 存储:
- 用户 id 为 key
- 商品 id 为 field,商品数量为 value
那用户购物车信息的维护具体应该怎么操作呢?
- 用户添加商品就是往 Hash 里面增加新的 field 与 value;
- 查询购物车信息就是遍历对应的 Hash;
- 更改商品数量直接修改对应的 value 值(直接 set 或者做运算皆可);
- 删除商品就是删除 Hash 中对应的 field;
- 清空购物车直接删除对应的 key 即可。
6.使用 Redis 实现一个排行榜怎么做?
Redis 中有一个叫做 Sorted Set
的数据类型经常被用在各种排行榜的场景,比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。
相关的一些 Redis 命令: ZRANGE
(从小到大排序)、 ZREVRANGE
(从大到小排序)、ZREVRANK
(指定元素排名)。
7.Set 的应用场景是什么?
Redis 中 Set
是一种无序集合,集合中的元素没有先后顺序但都唯一,有点类似于 Java 中的 HashSet
。
Set
的常见应用场景如下:
- 存放的数据不能重复的场景:网站 UV 统计(数据量巨大的场景还是
HyperLogLog
更适合一些)、文章点赞、动态点赞等等。 - 需要获取多个数据源交集、并集和差集的场景:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集) 等等。
- 需要随机获取数据源中的元素的场景:抽奖系统、随机点名等等。
8.使用 Set 实现抽奖系统怎么做?
如果想要使用 Set
实现一个简单的抽奖系统的话,直接使用下面这几个命令就可以了:
SADD key member1 member2 ...
:向指定集合添加一个或多个元素。SPOP key count
:随机移除并获取指定集合中一个或多个元素,适合不允许重复中奖的场景。SRANDMEMBER key count
: 随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。
9.使用 Bitmap 统计活跃用户怎么做?
Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。
你可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做 offset(偏移量)。
如果想要使用 Bitmap 统计活跃用户的话,可以使用日期(精确到天)作为 key,然后用户 ID 为 offset,如果当日活跃过就设置为 1。
10.什么是HyperLogLog ?使用 HyperLogLog 统计页面 UV 怎么做?
工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站PV(PageView页面访问量),可以使用Redis的incr、incrby轻松实现。
但像UV(Unique Visitor,独立访客)、独立IP数、搜索记录数等需要去重和计数的问题如何解决?这种求集合中不重复元素个数的问题称为基数问题。
解决基数问题有很多种方案:
(1)数据存储在MySQL表中,使用distinct count计算不重复个数
(2)使用Redis提供的hash、set、bitmaps等数据结构来处理
以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。
能否能够降低一定的精度来平衡存储空间?Redis推出了HyperLogLog
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
什么是基数?
比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。
使用 HyperLogLog 统计页面 UV 主要需要用到下面这两个命令:
PFADD key element1 element2 ...
:添加一个或多个元素到 HyperLogLog 中。PFCOUNT key1 key2
:获取一个或者多个 HyperLogLog 的唯一计数。
1、将访问指定页面的每个用户 ID 添加到 HyperLogLog
中。
PFADD PAGE_1:UV USER1 USER2 ...... USERn
2、统计指定页面的 UV。
PFCOUNT PAGE_1:UV
11.redis怎么实现消息队列?
使用list类型保存数据信息,rpush生产消息,lpop消费消息,当lpop没有消息时,可以sleep一段时间,然后再检查有没有信息,如果不想sleep的话,可以使用blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。
12.Redis 怎么实现延时队列
使用sortedset,拿时间戳作为score,消息内容作为key,调用zadd来生产消息,消费者用zrangebyscore
指令获取N秒之前的数据轮询进行处理。
3. Redis常用命令
1 字符串操作命令
Redis 中字符串类型常用命令:
- SET key value 设置指定key的值
- GET key 获取指定key的值
- SETEX key seconds value 设置指定key的值,并将 key 的过期时间设为 seconds 秒
- SETNX key value 只有在 key 不存在时设置 key 的值
setnx命令的原理
SETNX
(SET if Not eXists)是Redis提供的一个非常有用的命令,它允许用户在保证原子性的前提下为一个key设置值,但前提是这个key在Redis中尚不存在。以下是SETNX
命令的工作原理:
- 原子性:Redis通过单线程模型执行命令,确保了
SETNX
操作的原子性。这意味着在并发环境下,同一时刻只有一个客户端能够成功地为一个key设定值。 - 条件设置:
SETNX
命令只在给定的key不存在时设置其值。如果key已经存在,命令将不执行任何操作,并返回0。如果key不存在,命令将设置其值,并返回1。
setex命令的原理
Redis的SETEX命令用于将值设置为给定的key,并指定该键值对的过期时间。
Redis是一个高性能的键值存储系统,它支持多种数据类型和原子操作。其中,SETEX
命令是用于设置一个带有过期时间的键值对。以下是SETEX命令的工作原理:
- 键值存储:在Redis中,每个key都与一个RedisObject结构相关联,该结构包含类型信息和指向底层简单动态字符串(SDS)的指针。当执行
SETEX
命令时,Redis会创建一个新的RedisObject来存储key,同时也会为value创建一个新的RedisObject。 - 过期机制:
SETEX
命令允许用户为键设置一个过期时间,这是通过在Redis的数据库结构中的expires字典中记录每个键的过期时间来实现的。这个过期时间是以毫秒精度的UNIX时间戳来表示的。 - 原子性操作:Redis通过其单线程模型确保了命令的原子性执行。
2 通用命令
Redis的通用命令是不分数据类型的,都可以使用的命令:
- KEYS pattern 查找所有符合给定模式( pattern)的 key
- EXISTS key 检查给定 key 是否存在
- TYPE key 返回 key 所储存的值的类型
- DEL key 该命令用于在 key 存在是删除 key
4.在Java中操作Redis
Spring Data Redis使用方式
Spring Data Redis中提供了一个高度封装的类:RedisTemplate,对相关api进行了归类封装,将同一类型操作封装为operation接口,具体分类如下:
- ValueOperations:string数据操作
- SetOperations:set类型数据操作
- ZSetOperations:zset类型数据操作
- HashOperations:hash类型的数据操作
- ListOperations:list类型的数据操作
StringRedisTemplate
尽管JSON的序列化方式可以满足我们的需求,但依然存在一些问题,如图:
为了在反序列化时知道对象的类型,JSON序列化器会将类的class类型写入json结果中,存入Redis,会带来额外的内存开销。
为了减少内存的消耗,我们可以采用手动序列化的方式,换句话说,就是不借助默认的序列化器,而是我们自己来控制序列化的动作,同时,我们只采用String的序列化器,这样,在存储value时,我们就不需要在内存中就不用多存储数据,从而节约我们的内存空间
这种用法比较普遍,因此SpringDataRedis就提供了RedisTemplate的子类:StringRedisTemplate,它的key和value的序列化方式默认就是String方式。
省去了我们自定义RedisTemplate的序列化方式的步骤,而是直接使用:
@Autowired private StringRedisTemplate stringRedisTemplate; @Test void testString() { // 写入一条String数据 stringRedisTemplate.opsForValue().set("verify:phone:11111, "124143"); // 获取string数据 Object name = stringRedisTemplate.opsForValue().get("name"); System.out.println("name = " + name); } private static final ObjectMapper mapper = new ObjectMapper(); @Test void testSaveUser() throws JsonProcessingException { // 创建对象 User user = new User("name", 21); // 手动序列化.将一个对象序列化为JSON字符串并存储到Redis中 String json = mapper.writeValueAsString(user); // 写入数据 stringRedisTemplate.opsForValue().set("user:200", json); // 获取数据 String jsonUser = stringRedisTemplate.opsForValue().get("user:200"); // 手动反序列化 User user1 = mapper.readValue(jsonUser, User.class); System.out.println("user1 = " + user1); }
最后小总结:
RedisTemplate的两种序列化实践方案:
- 方案一:自定义RedisTemplate修改RedisTemplate的序列化器为GenericJackson2JsonRedisSerializer
- 方案二:使用StringRedisTemplate写入Redis时,手动把对象序列化为JSON读取Redis时,手动把读取到的JSON反序列化为对象
[2].redis基础知识
0.redis怎么应对高并发?
在应对高并发的场景中,Redis作为一种高效的内存数据存储系统,被广泛应用于缓存、消息队列等场景,以提升系统的读取速度和降低数据库的负载。下面将逐一列出针对高并发问题时,如何利用Redis来提高系统性能和稳定性:
- 采用Redis集群模式数据分片:在多个节点上分布数据分片,即使某个节点不可用,也不会影响其他节点的正常运行,从而避免缓存雪崩现象。跨机房部署:通过在不同地理位置的机房部署Redis集群,可以进一步提升容灾能力,保证在某个机房出现故障时依然能提供服务。
- 使用持久化和预热缓存数据持久化:在重启Redis之前,通过执行SAVE指令将数据持久化到磁盘,确保数据不会因为重启而全部失效。人工触发预热:重启后手动或自动预热缓存,保证缓存在服务恢复后能够立即起到加速访问的作用。
- 随机设置缓存过期时间分散失效时间:为缓存设置不同的过期时间,避免大量缓存在同一时刻集中失效,从而防止缓存雪崩的发生
0.怎么保证Redis的高并发高可用
首先可以搭建主从集群,再加上使用redis中的哨兵模式,哨兵模式可以实现主从集群的自动故障恢复,里面就包含了对主从服务的监控、自动故障恢复、通知;如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主;同时Sentinel也充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端,所以一般项目都会采用哨兵的模式来保证redis的高并发高可用
1.redis为什么这么快?
嗯,这个有几个原因吧~~~
1、完全基于内存的,C语言编写,没有磁盘IO上的开销。数据存在内存中,读写速度快。
2、采用单线程,避免不必要的上下文切换以及锁等同步机制的开销
3、使用多路I/O复用模型,基于select/epoll等I/O多路复用技术实现高吞吐量网络I/O
- IO多路复用是一种操作系统中的一种技术,允许一个线程或进程同时处理多个输入输出(IO)操作。Redis通过使用IO多路复用技术,以单线程的方式高效地处理了多个客户端的请求,避免了为每个连接创建新线程的开销,并保持高性能。
能解释一下I/O多路复用模型?
IO多路复用模型的思路就是:系统提供了select、poll、epoll函数可以同时监控多个fd(文件描述符)的操作,有了这个函数后,应用线程通过调用select函数就可以同时监控多个fd,一旦某个描述符就绪(一般是读就绪或者写就绪),select函数就会返回可读/可写状态,这时询问线程再去通知想请求IO操作的线程,对应线程此时再发起IO请求去读/写数据。
文件描述符是一个非负整数,用于标识被进程打开的文件,是操作系统为了高效管理这些文件所创建的索引。
为什么redis是单线程的?为什么后面又引入多线程?
2.既然Redis那么快,为什么不用它做主数据库,只用它做缓存?
虽然Redis非常快,但它也有一些局限性,不能完全替代主数据库。有以下原因:
**事务处理:**Redis只支持简单的事务处理,对于复杂的事务无能为力,比如跨多个键的事务处理。
**数据持久化:**Redis是内存数据库,数据存储在内存中,如果服务器崩溃或断电,数据可能丢失。虽然Redis提供了数据持久化机制,但有一些限制。
**数据处理:**Redis只支持一些简单的数据结构,比如字符串、列表、哈希表等。如果需要处理复杂的数据结构,比如关系型数据库中的表,那么Redis可能不是一个好的选择。
**数据安全:**Redis没有提供像主数据库那样的安全机制,比如用户认证、访问控制等等。
因此,虽然Redis非常快,但它还有一些限制,不能完全替代主数据库。所以,使用Redis作为缓存是一种很好的方式,可以提高应用程序的性能,并减少数据库的负载。
3.说一下 Redis 和 Memcached 的区别和共同点
现在公司一般都是用 Redis 来实现缓存,而且 Redis 自身也越来越强大了!不过,了解 Redis 和 Memcached 的区别和共同点,有助于我们在做相应的技术选型的时候,能够做到有理有据!
共同点:
- 都是基于内存的数据库,一般都用来当做缓存使用。
- 都有过期策略。
- 两者的性能都非常高。
区别:
- MemCached 数据结构单一,仅用来缓存数据,而 Redis 支持多种数据类型。
- MemCached 不支持数据持久化,重启后数据会消失。Redis 支持数据持久化。
- Redis 提供主从同步机制和 cluster 集群部署能力,能够提供高可用服务。Memcached 没有提供原生的集群模式,需要依靠客户端实现往集群中分片写入数据。
- Redis 的速度比 Memcached 快很多。
- Redis 使用单线程的多路 IO 复用模型,Memcached使用多线程的非阻塞 IO 模型。(Redis6.0引入了多线程IO,用来处理网络数据的读写和协议解析,但是命令的执行仍然是单线程)
- value 值大小不同:Redis 最大可以达到 512M;memcache 只有 1mb。
4.Redis遇到哈希冲突怎么办?
当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时, 我们称这些键发生了冲突(collision)。
Redis 的哈希表使用链地址法(separate chaining)来解决键冲突: 每个哈希表节点都有一个 next
指针, 多个哈希表节点可以用 next
指针构成一个单向链表, 被分配到同一个索引上的多个节点可以用这个单向链表连接起来, 这就解决了键冲突的问题。
原理跟 Java 的 HashMap 类似,都是数组+链表的结构。当发生 hash 碰撞时将会把元素追加到链表上。
5.redis线程模型
1.redis单线程模型
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。
- 文件事件处理器使用I/O多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
- 当被监听的套接字准备好执行连接accept、read、write、close等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。
那 Redis6.0 之前为什么不使用多线程? 我觉得主要原因有 3 点:
- 单线程编程容易并且更容易维护;
- Redis 的性能瓶颈不在 CPU ,主要在内存和网络;
- 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。
2.Redis6.0 之后为何引入了多线程?
Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。
虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。
[3]redis作缓存读写一致性问题
1.3种常用的缓存读写策略详解
1. Cache Aside Pattern(旁路缓存模式)
Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。
Cache Aside Pattern 中服务端需要同时维系 db 和 cache,并且是以 db 的结果为准。
写:
- 先更新 db
- 然后直接删除 cache 。
读 :
- 从 cache 中读取数据,读取到就直接返回
- cache 中读取不到的话,就从 db 中读取数据返回
- 再把数据放到 cache 中。
1.在写数据的过程中,可以先删除 cache ,后更新 db 么?
不行,因为这样可能会造成 数据库(db)和缓存(Cache)数据不一致的问题。举例:请求 1 先写数据 A,请求 2 随后读数据 A 的话,就很有可能产生数据不一致性的问题。
过程如下:请求 1 先把 cache 中的 A 数据删除 -> 请求 2 从 db 中读取数据->请求 1 再把 db 中的 A 数据更新。
2.在写数据的过程中,先更新 db,后删除 cache 就没有问题了么?
理论上来说还是可能会出现数据不一致性的问题,不过概率非常小,因为缓存的写入速度是比数据库的写入速度快很多。举例:请求 1 先读数据 A,请求 2 随后写数据 A,并且数据 A 在请求 1 请求之前不在缓存中的话,也有可能产生数据不一致性的问题。
大概过程是:请求 1 从 db 读数据 A-> 请求 2 更新 db 中的数据 A(此时缓存中无数据 A ,故不用执行删除缓存操作 ) -> 请求 1 将数据 A 写入 cache
3.Cache Aside Pattern 的缺陷和解决办法
缺陷 1:首次请求数据一定不在 cache 的问题
解决办法:可以将热点数据可以提前放入 cache 中。
缺陷 2:写操作比较频繁的话导致 cache 中的数据会被频繁被删除,这样会影响缓存命中率 。
解决办法:
- 数据库和缓存数据强一致场景:更新 db 的时候同样更新 cache,采用redisson实现的读写锁。在读的时候添加共享锁,可以保证读读不互斥,读写互斥(其他线程可以一起读,但是不能写)。当我们更新数据的时候,添加排他锁,它是读写,读读都互斥(其他线程不能读也不能写),这样就能保证在写数据的同时是不会让其他线程读数据的,避免了脏数据。那这个排他锁是如何保证读写、读读互斥的呢?其实排他锁底层使用也是setnx,保证了同时只能有一个线程操作锁住的方法
- 可以短暂地允许数据库和缓存数据不一致的场景:更新 db 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小。
2.Read/Write Through Pattern(读写穿透)
只更新缓存,由缓存自己同步更新数据库
写(Write Through):
- 先查 cache,cache 中不存在,直接更新 db。
- cache 中存在,则先更新 cache,然后 cache 服务自己更新 db(同步更新 cache 和 db)。
读(Read Through):
- 从 cache 中读取数据,读取到就直接返回 。
- 读取不到的话,先从 db 加载,写入到 cache 后返回响应。
和 Cache Aside Pattern 一样, Read-Through Pattern 也有首次请求数据一定不再 cache 的问题,对于热点数据可以提前放入缓存中。
3.Write Behind Pattern(异步缓存写入)
只更新缓存,由缓存自己异步更新数据库
2.redis做为缓存,mysql的数据
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
我的笔记专栏,内有自己整理的八股知识笔记和算法刷题笔记,我会不断通过他人和自己的面经来更新和完善自己的八股笔记。专栏每增加一篇文章费用就会上涨一点,如果你喜欢的话建议你尽早订阅。内有超详细苍穹外卖话术!后续还会更新其他项目和我的实习经历的话术!敬请期待!