Redis

是一个基于内存的、可持久化的键值对(key-value型)存储系统,数据读写是内存中的。

是单线程吗?为什么还这么快?

是也不是,Redis单线程指的是 接受客户端请求->解析请求->进行数据读写操作->发送数据给客户端 这个过程是由一个线程来完成,这是我们常说Redis是单线程的原因,关闭文件、AOF刷盘、释放内存又有各自的线程,这是说Redis多线程的原因。

单线程Redis吞吐量可以达到10W/每秒,Redis 采用单线程(网络 I/O 和执行命令)那么快,有如下几个原因:

  • Redis的数据读写是在内存中完成,所有的数据都存储在内存中。内存的读写速度远快于磁盘,这极大地提升了数据的访问速度。
  • Redis内部使用了多种高效的数据结构,如String、list、hash、set、zset,支持快速的查找、插入和删除操作。
  • 采用单线程可以避免多线程之间的竞争,省去了多线程切换带来的时间和性能上的、锁竞争的开销,不会导致死锁问题。
  • Redis采用I/O多路复用机制处理大量客户端的Socket请求,一个线程处理多个I/O流。Redis只运行单线程的情况下,内核会一直监听这些socket上的连接请求和数据请求,一旦请求到达,就会交给Redis线程处理,就实现了一个Redis线程处理多个IO流的效果。

数据结构

Redis提供了丰富的数据类型,常见的有五种

键值对中的Key就是字符串对象,而value可以是字符串对象,也可以是集合数据类型的对象

String(字符串)

key-value结构,key是唯一标识,value是具体的值

基本命令

set key value 设置值;get key获取值;incr key递增1;

适用场景

1.全局ID/分布式ID:在一个系统中,无论数据分布在哪个节点或数据库,生成的ID都是全局唯一的;功能与全局ID相同,但是是在分布式系统中。例如全局计数器。

例如分表,一张数据表中存储的数据量太大,会导致B+树的高度增加导致查询效率变低,可以采用水平分表的方式将一个表分成两个,两个新表数据都来自同一个表但是会有两份新的自增ID,导致不能通过ID来唯一标识,这时可以将表数据存储到Redis中,借助String类型的incr让ID key每次自增1,形成唯一标识的自增ID列表。

  1. 当需要插入新数据时,先向Redis发送INCR命令,获取一个新的全局唯一ID。
  2. 使用这个全局唯一ID作为新数据的主键或唯一标识。
  3. 将新数据插入到相应的分表中。

2.缓存:将数据库中的热点数据存储到Redis的String类型中,极大的提高系统的访问速度,减少数据库的访问压力;用户的会话信息(登录状态、购物车内容等)可以存储在Redis的中,以便请求共享。

可能存在缓存击穿的问题:

缓存击穿

如果缓存中的某个热点数据过期(被频繁访问的的数据,例如秒杀活动)了,此时大量的请求访问该热点数据就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿问题。

解决方案:

置不给热点数据设过期时间,由后台异步更新缓存,或者在热点数据准备要过期前 提前通知后台线程更新缓存以及重新设置过期时间。

分布式锁,保证同一时间只有一个业务线程请求缓存,未能获取分布式锁的,等待锁释放后读取缓存。

分布式锁(多个进程(服务)之间的互斥访问):在分布式系统中,当多个进程或服务需要访问共享资源时,保证同一时间只有一个进程或服务可以访问该资源,以避免并发问题。

多个用户访问不同的服务时,访问同一个数据库,但是用户在不同的进程里,数据库在内存中无法共享,可以把令牌放到线程所共享的Redis中,每个进程执行时都去Redis中检查令牌是否存在,如果有则难道令牌去执行代码,如果没有则等待其他进程将其删除。令牌在无锁,不在有锁。

一个用户线程去Redis缓存查看代码数据,而Redis缓存中的数据已过期,就会去数据库中查找,在查询数据库之前,使用分布式锁来确保只有一个线程或进程去数据库查询,查到后将数据放到Redis中,由于数据已经被更新,所以能够直接从Redis中获取数据,无需再访问数据库。当并发数太多,每台用户都要完成以上步骤很繁杂,可以加入分布式锁,第一个用户将数据放到Redis中,所有的线程共享此Redis,以后的用户线程只需要再次查看Redis就可以了,无需再次访问数据库。(存在两次查看Reids,double check,第一次是第一个用户查看Redis缓存中是否有数据,若没有则存数据库中加载数据放到Redis,第二次查看是加入分布式锁后,以后的用户去Redis中拿令牌查看数据,无需访问数据库)确保缓存的一致性和减少数据库的压力。

可能导致缓存穿透的问题:

缓存穿透

内部实现

底层的数据结构实现是SDS简单动态字符串和int

len,记录了字符串的长度(buf中已占用空间的长度),获取字符型串长度的时候只需返回这个成员变量的值就行。

free,buff中剩余可用空间的长度。通过剩余空间的长度可以知道空间占用情况,当超过负载值动态扩容。也可通过修改len与free实现缩容。

  • 扩容:当字符串长度小于1M时,扩容都是加倍修改后的空间,如果超过1M,扩容时一次只会多扩1M。
  • 缩容:不会释放,修改len与free,供下次使用。

buf[],数据空间。

好处:返回字符串长度的时间复杂度低O(1);二进制安全,用len属性记录字符串的长度可以包含'\0',数据写入是什么样,它被读取时就是什么样;自动扩容不会发生缓冲区溢出

List(列表)

ZipLIist
结构

是Redis为节约内存而开发的,是由连续内存块组成的顺序型数据结构,类似数组。

表头:

  • zlbytes,记录整个压缩列表占用内存字节数;
  • zltail,记录压缩列表尾部节点距离起始地址有多少字节
  • zlen,记录压缩列表包含的节点数量

entry压缩列表节点:

prevlen,记录 前一个节点 的长度,目的是实现从后向前遍历;

prevlen是如何根据数据的大小和类型进行不同空间大小的分配?

如果前一个节点的长度小于254字节,那么prevlen属性需要用1字节的空间来保存这个长度值

如果前一个节点的长度大于254字节,那么prevlen属性需要用5字节的空间来保存这个长度

encoding,记录当前节点的实际数据的类型和长度 ,类型主要有两种:字符串和整数;

encoding是如何根据数据的大小和类型进行不同空间大小的分配?

如果当前节点是整数,则encoding会使用1字节的空间进行编码,也就是encoding长度为1字节。

如果当前节点是字符串,根据字符串的大小,encoding会使用1字节/2字节/5字节的空间进行编码。

data,记录当前节点的实际数据;

zlend标记压缩列表的结束点

优缺点
  • 优点:一种内存紧凑型的数据结构,占用一块连续的内存空间,可以利用cpu缓存,也可以针对不同长度的数据进行相应的编码,有效的节省内存开销。顺序存储,查询效率高。
  • 缺点:不能保存过多的元素,否则查询效率会降低;新增或修改某个元素,压缩列表占用的内存空间需要重新分配,甚至可能引发连锁更新问题。
存在的问题

连锁更新

压缩列表新增某个元素或修改某个元素时,如果空间不够,压缩列表占用的内存空间就需要重新分配。而当新插入的元素较大时,可能会导致后续元素的(entry1)prevlen占用空间都发生变化,从而引起 连锁更新 问题(entry2的长度、entry3的长度...变大),导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降。

quickList

Redis3.0之前,List对象的底层数据结构是ZipList。在Redis3.2,List对象的底层由quicklist数据结构实现。

是双向链表+压缩链表的组合,因为quicklist是一个链表,而链表中的每个元素又是一个压缩列表。

解决连锁更新问题

通过控制每个链表节点(quickListNode)中的压缩列表的大小或者元素个数,来规避连锁更新的问题。(因为压缩列表元素越少或越小,连锁更新来的的影响就越小,从而提供了更好的访问性能)

能缓解连锁更新问题,链表中某个节点变长不会影响后面的节点,但是遍历的效率低。

Set(集合)

ZSet(有序集合)

Hash(哈希)

互斥锁(一个进程中多个线程之间的互斥访问)

持久化

默认是RDB快照,存储的是二进制数据,是异步且定时每隔一段时间将内存数据放入磁盘,当系统若宕机后内存中没有数据,重启后就能从磁盘中加载到内存,在内存做数据处理。

相关推荐
无色海2 小时前
mysql连接生命周期-连接阶段
数据库
无色海3 小时前
MySQL协议中的TLS实现
数据库
麓殇⊙4 小时前
redisson锁的可重入、可重试、超时续约原理详解
redis·分布式锁
jstart千语4 小时前
【Redisson】锁可重入原理
redis·分布式·redisson
weixin_418007604 小时前
SpringJPA统计数据库表行数及更新频率
数据库
2301_767233224 小时前
怎么优化MySQL中的索引
数据库·mysql
无色海4 小时前
MySQL 压缩数据包详解
数据库
海尔辛4 小时前
防御性安全:数字取证
数据库·安全·数字取证
繢鴻5 小时前
数据库优化实战分享
数据库
Cachel wood6 小时前
后端开发:计算机网络、数据库常识
android·大数据·数据库·数据仓库·sql·计算机网络·mysql