关系型数据库和非关系型数据库
关系型数据库
含义:采用关系模型来组织数据的数据库。简单说关系模型就是二维表格模型,而关系型数据库就是由二维表及其之间的联系组成的数据结构
优点:
- 容易理解:二维表结构是非常贴近逻辑世界的一个概念,关系模型相对网状、层次等其他模型来说更容易理解
- 使用方便:通用的SQL语言使得操作关系型数据库非常方便
- 易于维护:丰富的完整性(实体完整性、参照完整性和用户定义的完整性)大大减低了数据冗余和数据不一致的概率
缺点:
- 高并发读写需求。 网站的用户并发性非常高,往往达到每秒上万次读写请求,对于传统关系型数据库来说,硬盘I/O是一个很大的瓶颈
- 海量数据的高效率读写。 网站每天产生的数据量是巨大的,对于关系型数据库来说,在一张包含海量数据的表中查询和修改,效率是非常低的
- 高扩展性和可用性。在基于web的结构当中,数据库是最难进行横向扩展的,当一个应用系统的用户量和访问量与日俱增的时候,数据库却没有办法像web server和app server那样简单的通过添加更多的硬件和服务节点来扩展性能和负载能力。对于很多需要提供24小时不间断服务的网站来说,对数据库系统进行升级和扩展是非常痛苦的事情,往往需要停机维护和数据迁移。
一些不需要关系型数据库的情况:
- 关系型数据库在对事物一致性的维护中有很大的开销,而现在很多web2.0系统对事物的读写一致性都不高
- 对关系数据库来说,插入一条数据之后立刻查询,是肯定可以读出这条数据的,但是对于很多web应用来说,并不要求这么高的实时性,比如发一条消息之后,过几秒乃至十几秒之后才看到这条动态是完全可以接受的
- 任何大数据量的web系统,都非常忌讳多个大表的关联查询,以及复杂的数据分析类型的复杂SQL报表查询,特别是SNS类型的网站(SNS,专指社交网络服务,包括了社交软件和社交网站。)
非关系型数据库
含义:NoSQL一词首先是Carlo Strozzi在1998年提出来的,指的是他开发的一个没有SQL功能,轻量级的,开源的关系型数据库。但是NoSQL的发展慢慢偏离了初衷,我们要的不是“no sql”,而是“no relational(not noly)”,也就是我们现在常说的非关系型数据库了。
优点:
- 格式灵活:存储数据的格式可以是key,value形式、文档形式、图片形式等等,文档形式、图片形式等等,使用灵活,应用场景广泛,而关系型数据库则只支持基础类型。
- 速度快:nosql可以使用硬盘或者随机存储器作为载体,而关系型数据库只能使用硬盘;
- 高扩展性。
- 成本低:nosql数据库部署简单,基本都是开源软件。
缺点:
- 不提供sql支持,学习和使用成本较高;
- 无事务处理;
- 数据结构相对复杂,复杂查询方面稍欠。
总结
关系型数据库的最大特点就是事务的一致性:传统的关系型数据库读写操作都是事务的,具有ACID的特点,这个特性使得关系型数据库可以用于几乎所有对一致性有要求的系统中,如典型的银行系统。
但是,在网页应用中,尤其是SNS应用中,一致性却不是显得那么重要,用户A看到的内容和用户B看到同一用户C内容更新不一致是可以容忍的,或者说,两个人看到同一好友的数据更新的时间差那么几秒是可以容忍的,因此,关系型数据库的最大特点在这里已经无用武之地,起码不是那么重要了。
相反地,关系型数据库为了维护一致性所付出的巨大代价就是其读写性能比较差,而像微博、facebook这类SNS的应用,对并发读写能力要求极高,关系型数据库已经无法应付因此,必须用新的一种数据结构存储来代替关系数据库。
关系数据库的另一个特点就是其具有固定的表结构,因此,其扩展性极差,而在SNS中,系统的升级,功能的增加,往往意味着数据结构巨大变动,这一点关系型数据库也难以应付,需要新的结构化数据存储。
于是,非关系型数据库应运而生,由于不可能用一种数据结构化存储应付所有的新的需求,因此,非关系型数据库严格上不是一种数据库,应该是一种数据结构化存储方法的集合。
必须强调的是,数据的持久存储,尤其是海量数据的持久存储,还是需要一种关系数据库这员老将。
什么时候用redis什么时候用mysql?
Redis和MySQL不是相互替代的关系,而是相辅相成的,越来越多的项目组已经采用了redis+MySQL的架构来开发平台工具。
首先mysql是持久化数据库,是关系型数据库,是直接保存在硬盘上的。redis是非关系型数据库,是内存运行的数据存储获取工具。
但是数据量多少并不是redis和mysql选择的标准,因为都可以集群括展。
他们的使用场景是不同的:
- 关系型数据库最重要的有两个点,第一是持久化存储的功能,即数据都存储在硬盘中。第二就是关系型数据库可以提供复杂的查询和统计功能。
- 关系型数据库偏向于快速存取数据,用于实时响应要求高的场景,响应时间在毫秒级,通常作为热点数据的缓存使用。
数据多而且调用频繁的话,用mysql存储的话数据库连接被一直占用,其它的数据请求就进来了,导致连接超时,数据量大的话,数据库直接死机了。只能重启才能解决问题。这个时候如果把数据请求量大的数据放在redis中的话就可以分担一下mysql的压力,从而提高系统的性能,解决请求并发问题。
Redis持久化
RDB模式和AOF模式
在默认情况下,Redis将数据库快照保存在名为dump.rdb的二进制文件中
两个模式的选择:
- 如果主要充当缓存功能,或者可以承受数分钟数据的丢失, 通常生产环境一般只需启用RDB可,此也是默认值
- 如果数据需要持久保存,一点不能丢失,可以选择同时开启RDB和AOF,一般不建议只开启AOF
RDB模式
具体原理有两种SAVE,BGSAVE
SAVE
SAVE是阻塞服务,在创建新文件dump.rdb替代旧文件时候无法响应客户端请求,生产环境中很少这样,一般都是停机维护时候才考虑
BGSAVE
与之对应的,BGSAVE就是非阻塞的。当创建RDB文件时候,会fork一个子进程来做这件事,同时父进程会正常接收处理来自客户端的请求。子进程执行RDB操作,处理完后会向父进程发送一个信号,通知父进程处理完毕,父进程用新的dump.rdb文件替代旧文件。可以说BGSAVE是一个异步命令。fork是指redis通过创建子进程来进行RDB操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。
RDB优点:
- RDB保存了某个时间点的数据,可以保留多个备份。当出现问题的时候方便恢复到不同时间节点(多版本恢复),同事文件格式支持不少第三方工具分析
- RDB可以最大化Redis性能,父进程在保存RDB文件时候,唯一要做的就是fork一个子进程,接着子进程会接替保存工作,父进程无需执行任何磁盘io操作
- RDB在大量数据的时候,恢复比AOF快
- 文件单一紧凑,方便网络传输,适合灾难恢复
RDB缺点:
- RDB不能实时保存数据,即这次保存数据和上次保存数据之间这段时间如果有新数据,可能会丢失这部分新数据。虽然Redis允许设置不同的保存点来控制保存RDB文件的频率,但是由于数据集的属性,这不是一个轻松地操作,因此会丢失好几分钟内的数据
- 当数据量非常大的时候,从父进程fork子进程来保存RDB文件时候需要一点时间。当数据集很庞大的时候,fork会非常耗时,造成服务器在一定时间内停止处理客户端,会有毫秒或秒级响应。
AOF模式
AOF即Append Only File,需要手动开启,采用追加的方式保存,默认文件是appendonly.aof,记录所有写的命令。
AOF 方式不能保证绝对不丢失数据,目前常见的操作系统中,执行系统调用 write 函数,将一些内容写入到某个文件里面时,为了提高效率,系统通常不会直接将内容写入硬盘里 面,而是先将内容放入一个内存缓冲区(buffer)里面,等到缓冲区被填满,或者用户执行 fsync 调用和 fdatasync 调用时才将储存在缓冲区里的内容真正的写入到硬盘里,未写入磁盘之前,数据可能会丢失 。过程:
- 命令追加:写到aof_buf中;
- 写入文件:执行write操作;
- 同步文件:同步到磁盘中。
优点:
- 数据安全性相对较高,根据所使用的fsync策略(fsync是同步内存中redis所有已经修改的文件到存储设备),默认是appendfsync everysec,即每秒执行一次 fsync,在这种配置下,Redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据( fsync会在后台线程执行,所以主线程可以继续努力地处理命令请求)
- 由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中不需要seek, 即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,可以通过 redis-check-aof 工具来解决数据一致性的问题
- Redis可以在 AOF文件体积变得过大时,自动地在后台对AOF进行重写,重写后的新AOF文件包含了恢复当前数据集所需的最小命令集合。整个重写操作是绝对安全的,因为Redis在创建新 AOF文件的过程中,append模式不断的将修改数据追加到现有的 AOF文件里面,即使重写过程中发停机,现有的 AOF文件也不会丢失。而一旦新AOF文件创建完毕,Redis就会从旧AOF文件切换到新AOF文件,并开始对新AOF文件进行追加操作。
- AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,也可以通过该文件完成数据的重建AOF文件有序地保存了对数据库执行的所有写入操作,这些写入操作以Redis协议的格式保存,因此 AOF文件的内容非常容易被人读懂,对文件进行分析(parse)也很轻松。导出(export)AOF文件也非常简单:
- 举个例子,如果你不小心执行了FLUSHALL.命令,但只要AOF文件未被重写,那么只要停止服务器,移除 AOF文件末尾的FLUSHAL命令,并重启Redis ,就可以将数据集恢复到
FLUSHALL执行之前的状态。
缺点:
- 即使有些操作是重复的也会全部记录,AOF 的文件大小要大于 RDB 格式的文件
- AOF 在恢复大数据集时的速度比 RDB 的恢复速度要慢
- 根据fsync策略不同,AOF速度可能会慢于RDB
- bug 出现的可能性更多
Redis的数据结构讲一讲 + 使用场景
五种基本的数据类型:String、Hash、List、Set、SortedSet
更高级的有:HyperLogLog、Geo、BloomFilter
键值对数据库是怎么实现的?
Redis 的键值对中的 key 就是字符串对象,而 value 可以是字符串对象,也可以是集合数据类型的对象,比如 List 对象、Hash 对象、Set 对象和 Zset 对象。
Redis 是使用了一个「哈希表」保存所有键值对,哈希表的最大好处就是让我们可以用 O(1) 的时间复杂度来快速查找到键值对。哈希表其实就是一个数组,数组中的元素叫做哈希桶。哈希桶存放的是指向键值对数据的指针,这样通过指针就能找到键值对数据,然后因为键值对的值可以保存字符串对象和集合数据类型的对象,所以键值对的数据结构中并不是直接保存值本身,而是保存了 void key 和 void value 指针,分别指向了实际的键对象和值对象,这样一来,即使值是集合数据,也可以通过 void * value 指针找到。如图:
特别说明:void key 和 void value 指针指向的是 Redis 对象,Redis 中的每个对象都由 redisObject 结构表示
string
String 是 Redis 最简单最常用的数据结构
Redis中的字符串,不是 C 语言中的字符串(即以空字符’\0’结尾的字符数组),是自己构建了一种名为 简单动态字符串(simple dynamic string,SDS)的抽象类型,并将 SDS 作为 Redis的默认字符串表示。其数据结构如下所示:
上图中,uint8_t表示8位无符号整数
为什么使用SDS?
- 由于 len 属性的存在,我们获取 SDS 字符串的长度只需要读取 len 属性,时间复杂度为 O(1)。而对于 C 语言,获取字符串的长度通常是经过遍历计数来实现的,时间复杂度为 O(n)。
- C 语言中使用
strcat
函数来进行两个字符串的拼接,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。而对于 SDS 数据类型,在进行字符修改的时候,会首先根据记录的 len 属性检查内存空间是否满足需求,如果不满足,会进行相应的空间扩展,然后在进行修改操作,所以不会出现缓冲区溢出。 - C语言由于不记录字符串的长度,所以如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露。而对于SDS,由于
len
属性和alloc
属性的存在,对于修改字符串SDS实现了空间预分配和惰性空间释放两种策略 - 因为C字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等),内容可能包括空字符串,因此C字符串无法正确存取;而所有 SDS 的API 都是以处理二进制的方式来处理
buf
里面的元素,并且 SDS 不是以空字符串来判断是否结束,而是以 len 属性表示的长度来判断字符串是否结束。
ZipList(压缩列表)
压缩列表的最大特点,就是它被设计成一种内存紧凑型的数据结构,占用一块连续的内存空间,不仅可以利用 CPU 缓存,而且会针对不同长度的数据,进行相应编码,这种方法可以有效地节省内存开销。
压缩列表的构成如下:
- zlbytes,记录整个压缩列表占用对内存字节数;
- zltail,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;
- zllen,记录压缩列表包含的节点数量;
- zlend,标记压缩列表的结束点,固定值 0xFF(十进制255)。
- prevlen,记录了「前一个节点」的长度;
- encoding,记录了当前节点实际数据的类型以及长度;
- data,记录了当前节点的实际数据;
当往压缩列表中插入数据时,压缩列表就会根据数据是字符串还是整数,以及数据的大小,会使用不同空间大小的 prevlen 和 encoding 这两个元素里保存的信息,这种根据数据大小和类型进行不同的空间大小分配的设计思想,正是 Redis 为了节省内存而采用的。
缺点:空间扩展操作也就是重新分配内存,因此连锁更新一旦发生,就会导致压缩列表占用的内存空间要多次重新分配,这就会直接影响到压缩列表的访问性能。所以说,虽然压缩列表紧凑型的内存布局能节省内存开销,但是如果保存的元素数量增加了,或是元素变大了,会导致内存重新分配,最糟糕的是会有「连锁更新」的问题。
hash表
Redis 散列可以存储多个键值对之间的映射。和字符串一样,散列存储的值既可以是字符串又可以是数值,并且用户同样可以对散列存储的数字值执行自增或自减操作。
整数集合
整数集合是 Set 对象的底层实现之一。当一个 Set 对象只包含整数值元素,并且元素数量不时,就会使用整数集这个数据结构作为底层实现。整数集合本质上是一块连续内存空间,它的结构定义如下:
typedef struct intset {
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
} intset;
保存元素的容器是一个 contents 数组,虽然 contents 被声明为 int8_t 类型的数组,但是实际上 contents 数组并不保存任何 int8_t 类型的元素,contents 数组的真正类型取决于 intset 结构体里的 encoding 属性的值。
整数集合的升级操作
整数集合会有一个升级规则,就是当我们将一个新元素加入到整数集合里面,如果新元素的类型(int32_t)比整数集合现有所有元素的类型(int16_t)都要长时,整数集合需要先进行升级,也就是按新元素的类型(int32_t)扩展 contents 数组的空间大小,然后才能将新元素加入到整数集合里,当然升级的过程中,也要维持整数集合的有序性。
整数集合升级的过程不会重新分配一个新类型的数组,而是在原本的数组上扩展空间,然后在将每个元素按间隔类型大小分割。整数集合升级的好处是节省内存资源。
跳表
Redis 只有在 Zset 对象的底层实现用到了跳表,跳表的优势是能支持平均 O(logN) 复杂度的节点查找。Zset 对象是唯一一个同时使用了两个数据结构来实现的 Redis 对象,这两个数据结构一个是跳表,一个是哈希表。这样的好处是既能进行高效的范围查询,也能进行高效单点查询。
链表在查找元素的时候,因为需要逐一查找,所以查询效率非常低,时间复杂度是O(N),于是就出现了跳表。跳表是在链表基础上改进过来的,实现了一种「多层」的有序链表,这样的好处是能快读定位数据。如图:
如果我们要在链表中查找节点 4 这个元素,只能从头开始遍历链表,需要查找 4 次,而使用了跳表后,只需要查找 2 次就能定位到节点 4,因为可以在头节点直接从 L2 层级跳到节点 3,然后再往前遍历找到节点 4。
quicklist(快表)
在 Redis 3.0 之前,List 对象的底层数据结构是双向链表或者压缩列表。然后在 Redis 3.2 的时候,List 对象的底层改由 quicklist 数据结构实现。
其实 quicklist 就是「双向链表 + 压缩列表」组合,因为一个 quicklist 就是一个链表,而链表中的每个元素又是一个压缩列表。
在前面讲压缩列表的时候,我也提到了压缩列表的不足,虽然压缩列表是通过紧凑型的内存布局节省了内存开销,但是因为它的结构设计,如果保存的元素数量增加,或者元素变大了,压缩列表会有「连锁更新」的风险,一旦发生,会造成性能下降。
quicklist 解决办法,通过控制每个链表节点中的压缩列表的大小或者元素个数,来规避连锁更新的问题。因为压缩列表元素越少或越小,连锁更新带来的影响就越小,从而提供了更好的访问性能。
使用场景
避免缓存穿透的利器之BloomFilter
本质就是用单向散列函数把数据映射到二进制向量中
布隆过滤器优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。即假阳性,就是说如果每一位为0表示一定没有,为1表示可能会出现没有的情况。
Bloom Filter跟单哈希函数Bit-Map不同之处在于:Bloom Filter使用了k个哈希函数,每个字符串跟k个bit对应。从而降低了冲突的概率。
因为布隆过滤器可以明确知道某个查询数据库不存在,所以可以过滤掉无效的查询到数据库,减少数据库的压力。
如果有大量的key需要设置同一时间过期,一般需要注意什么?
如果大量的key过期时间设置的过于集中,到过期的那个时间点,Redis可能会出现短暂的卡顿现象。严重的话会出现缓存雪崩,我们一般需要在时间上加一个随机值,使得过期时间分散一些。
缓存穿透,缓存击穿,缓存血崩
缓存穿透
定义:缓存穿透是指缓存和数据库都没有的数据,被大量请求,比如订单号不可能为
-1
,但是用户请求了大量订单号为-1
的数据,由于数据不存在,缓存就也不会存在该数据,所有的请求都会直接穿透到数据库。
如果被恶意用户利用,疯狂请求不存在的数据,就会导致数据库压力过大,甚至垮掉。解决:
缓存击穿
定义:缓存击穿是指数据库原本有得数据,但是缓存中没有,一般是缓存突然失效了,这时候如果有大量用户请求该数据,缓存没有则会去数据库请求,会引发数据库压力增大,可能会瞬间打垮。
解决:
缓存血崩
定义:缓存雪崩是指缓存中有大量的数据,在同一个时间点,或者较短的时间段内,全部过期了,这个时候请求过来,缓存没有数据,都会请求数据库,则数据库的压力就会突增,扛不住就会宕机。
解决:
redis高并发和快的原因
- redis是基于内存的,内存的读写速度非常快;没有磁盘IO的开销
- redis是单线程的,省去了很多上下文切换线程的时间;
- redis使用多路复用技术,可以处理并发的连接。非阻塞IO 内部实现采用epoll,采用了epoll+自己实现的简单的事件框架。
Redis的单线程
Redis由很多个模块组成,如网络请求模块、索引模块、存储模块、高可用集群支撑模块、数据操作模块等。
很多人说Redis是单线程的,就认为Redis中所有模块的操作都是单线程的,其实这是不对的。我们所说的Redis单线程,指的是"其网络IO和键值对读写是由一个线程完成的",也就是说,Redis中只有网络请求模块和数据操作模块是单线程的。而其他的如持久化存储模块、集群支撑模块等是多线程的。
原因:
- 锁带来的性能消耗。多线程可能会产生竞态条件,如果要对数据进行细粒度操作需要加锁,会加大开销增大延时。
- CPU上下文切换带来的性能消耗。在多核CPU架构下,Redis如果在不同的核上运行,就需要频繁地进行上下文切换,这个过程会增加Redis的执行时间,客户端也会观察到较高的尾延迟了
- redis是IO密集型程序,对于CPU是利用率没那么高,CPU并不是性能瓶颈
- 在单线程中使用多路复用 I/O技术也能提升Redis的I/O利用率
总结:上面的原因说的也是多线程实现redis的劣势。我们可以从整体来看,一个计算机程序在执行的过程中,主要需要进行两种操作分别是读写操作和计算操作。其中读写操作主要是涉及到的就是I/O操作,其中包括网络I/O和磁盘I/O,计算操作主要涉及到CPU。而多线程的目的,就是通过并发的方式来提升I/O的利用率和CPU的利用率。那么,Redis需不需要通过多线程的方式来提升提升I/O的利用率和CPU的利用率呢?首先redis数据的存取对CPU的要求很小,所以说CPU不是redis性能的瓶颈。那么再看IO,提高IO效率多线程是一种方案,但不是唯一的一种,还有IO多路复用这个技术。所以在redis采用的是IO复用的技术来提高IO并发。
Redis的IO多路复用
redis是非阻塞IO+IO多路复用的技术来实现的
Linux的IO多路复用机制是指一个线程处理多个IO流,也就是select/epoll机制。
Redis怎么统计在线用户
方案 | 特点 |
---|---|
有序集合 | 能够同时储存在线用户的名单以及用户的上线时间,能够执行非常多的聚合计算操作,但是耗费的内存也非常多。 |
集合 | 能够储存在线用户的名单,也能够执行聚合计算,消耗的内存比有序集合少,但是跟有序集合一样,这个方案消耗的内存也会随着用户数量的增多而增多。 |
HyperLogLog | 无论需要统计的用户有多少,只需要耗费 12 KB 内存,但由于概率算法的特性,只能给出在线人数的估算值,并且也无法获取准确的在线用户名单。 |
位图 | 在尽可能节约内存的情况下,记录在线用户的名单,并且能够对这些名单执行聚合操作。 |
redis,讲讲缓存一致性问题
果你的业务处于起步阶段,流量非常小,那无论是读请求还是写请求,直接操作数据库即可。但随着业务量的增长,你的项目请求量越来越大,这时如果每次都从数据库中读数据,那肯定会有性能问题。这个阶段通常的做法是,引入「缓存」来提高读性能,架构模型如上图。当下优秀的缓存中间件,当属 Redis 莫属,它不仅性能非常高,还提供了很多友好的数据类型,可以很好地满足我们的业务需求。但引入缓存之后,你就会面临一个问题:之前数据只存在数据库中,现在要放到缓存中读取,具体要怎么存呢?
最简单的方案:
- 数据库的数据,全量刷入缓存(不设置失效时间)
- 写请求只更新数据库,不更新缓存
- 启动一个定时任务,定时把数据库的数据,更新到缓存中
缺点:
- 缓存利用率低:不经常访问的数据,还一直留在缓存中
- 数据不一致:因为是「定时」刷新缓存,缓存和数据库存在不一致(取决于定时任务的执行频率)
所以,这种方案一般更适合业务「体量小」,且对数据一致性要求不高的业务场景。
那么现在就有两个问题,缓存利用率和一致性问题
缓存利用率
想要缓存利用率「最大化」,只需要缓存中只保留最近访问的「热数据,可以这样做:
- 写请求依旧只写数据库
- 读请求先读缓存,如果缓存不存在,则从数据库读取,并重建缓存
- 同时,写入缓存中的数据,都设置失效时间
这样一来,缓存中不经常访问的数据,随着时间的推移,都会逐渐「过期」淘汰掉,最终缓存中保留的,都是经常被访问的「热数据」,缓存利用率得以最大化。
一致性问题
大部分观点认为,做缓存不应该是去更新缓存,而是应该删除缓存,然后由下个请求去去缓存,发现不存在后再读取数据库,写入缓存。原因有如下两个:
- 线程安全问题。有请求A和请求B进行更新操作,假如有以下情况:(1)线程A更新了数据库(2)线程B更新了数据库(3)线程B更新了缓存(4)线程A更新了缓存,于是这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据
- 业务场景角度。如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用更新操作而不是删除,就会导致数据压根还没读到,缓存就被频繁的更新,浪费性能。其次,如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。显然,删除缓存更为适合。
当数据发生更新时,我们不仅要操作数据库,还要一并操作缓存。具体操作就是,修改一条数据时,不仅要更新数据库,也要连带缓存一起更新。但数据库和缓存都更新,又存在先后问题,那对应的方案就有 2 个:
先更新缓存,后更新数据库
如果缓存更新成功了,但数据库更新失败,那么此时缓存中是最新值,但数据库中是「旧值」。虽然此时读请求可以命中缓存,拿到正确的值,但是,一旦缓存「失效」,就会从数据库中读取到「旧值」,重建缓存也是这个旧值。这时用户会发现自己之前修改的数据又「变回去」了,对业务造成影响。
先更新数据库,后更新缓存
如果数据库更新成功了,但缓存更新失败,那么此时数据库中是最新值,缓存中是「旧值」。之后的读请求读到的都是旧数据,只有当缓存「失效」后,才能从数据库中得到正确的值。这时用户会发现,自己刚刚修改了数据,但却看不到变更,一段时间过后,数据才变更过来,对业务也会有影响。
上述这两个方案都不行,以下给出解决方案:
如果先更新缓存,后更新数据库的话,使用延时双删策略
延时双删的方案的思路是,为了避免更新数据库的时候,其他线程从缓存中读取不到数据,就在更新完数据库之后,再sleep一段时间,然后再次删除缓存。sleep的时间要对业务读写缓存的时间做出评估,sleep时间大于读写缓存的时间即可。
如果先更新数据库,后更新缓存的话,设置缓存过期时间,消息队列
设置过期时间:每次放入缓存的时候,设置一个过期时间,比如5分钟,以后的操作只修改数据库,不操作缓存,等待缓存超时后从数据库重新读取。如果对于一致性要求不是很高的情况,可以采用这种方案。
消息队列:先更新数据库,成功后往消息队列发消息,消费到消息后再删除缓存,借助消息队列的重试机制来实现,达到最终一致性的效果。
基于以上原因,redis官方选择了更简单、更快的方法,不支持错误回滚。这样的话,如果在我们的业务场景中需要保证原子性,那么就要求了开发者通过其他手段保证命令全部执行成功或失败,例如在执行命令前进行参数类型的校验,或在事务执行出现错误时及时做事务补偿。
Redis的事务满足原子性吗?
redis中的事务是不满足原子性的,在运行错误的情况下,并没有提供类似数据库中的回滚功能。那么为什么redis不支持回滚呢,官方文档给出了说明,大意如下:
- redis命令失败只会发生在语法错误或数据类型错误的情况,这一结果都是由编程过程中的错误导致,这种情况应该在开发环境中检测出来,而不是生产环境
- 不使用回滚,能使redis内部设计更简单,速度更快
- 回滚不能避免编程逻辑中的错误,如果想要将一个键的值增加2却只增加了1,这种情况即使提供回滚也无法提供帮助
redis有那些命令是原子指令
为何Redis使用跳表而非红黑树实现SortedSet?
首先要知道红黑树和跳表的插入删除,删除,查找时间复杂度是一样的。
redis作者说了三个原因:
- 范围查找。跳表在区间查询的时候效率是高于红黑树的,跳表进行查找O(logn)的时间复杂度定位到区间的起点,然后在原始链表往后遍历就可以了 ,其他插入和单个条件查询,更新两者的复杂度都是相同的O(logn)。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。
- 易于实现
- 从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
redis如何实现消息队列
消息队列是指利用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。
回顾下我们使用的消息队列有如下特点:
- 三个角色:生产者、消费者、消息处理中心
- 异步处理模式:生产者将消息发送到一条虚拟的通道(消息队列)上,而无须等待响应。消费者则订阅或是监听该通道,取出消息。两者互不干扰,甚至都不需要同时在线,也就是我们说的松耦合
- 可靠性:消息要可以保证不丢失、不重复消费、有时可能还需要顺序性的保证