Redis问题总结

一、基础

redis是一个高性能的key-value数据库,它是完全开源免费的,而且redis是一个NOSQL类型数据库,是为了解决高并发、高扩展,大数据存储等一系列的问题而产生的数据库解决方案,是一个非关系型的数据库。

1. 支持的数据类型

Redis支持诸如字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、带范围查询的排序集合(sorted sets)、位图(bitmaps)、hyperloglogs、带半径查询和流的地理空间索引等数据结构(geospatial indexes)。

String

常用命令 :set/get/decr/incr/mget等;

应用场景

  1. String通常用于保存单个字符串或JSON字符串数据
  2. 因String是二进制安全的,所以你完全可以把一个图片文件的内容作为字符串来存储
  3. 计数器(常规 key-value 缓存应用。常规计数:微博数,粉丝数)

实现方式:String在redis内部存储默认就是一个字符串,被redisObject所引用,当遇到incr、decr等操作时会转成数值型进行计算,此时redisObject的encoding字段为int。

Hash

常用命令 :hget/hset/hgetall等

应用场景 :我们要存储一个用户信息对象数据,其中包括用户ID、用户姓名、年龄和生日,通过用户ID我们希望获取该用户的姓名或者年龄或者生日;

实现方式:Redis的Hash实际是内部存储的Value为一个HashMap,并提供了直接存取这个Map成员的接口。Key是用户ID, value是一个Map。这个Map的key是成员的属性名,value是属性值。这样对数据的修改和存取都可以直接通过其内部Map的Key(Redis里称内部Map的key为field),也就是通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据。当前HashMap的实现有两种方式:当HashMap的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,这时对应的value的redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时redisObject的encoding字段为int。

为什么不 用 string 存储一个对象

hash 值最接近关系数据库结构的数据类型,可以将数据库一条记录或程序中一个对象转换成 hashmap 存放在 redis 中。

用户 ID 为查找的 key,存储的 value 用户对象包含姓名,年龄,生日等信息,如果用普通的 key/value 结构来存储,主要有以下 2 种方式:

第一种方式将用户 ID 作为查找 key,把其他信息封装成为一个对象以序列化的方式存储,这种方式增加了序列化/反序列化的开销,并且在需要修改其中一项信息时,需要把整个对象取回,并且修改操作需要对并发进行保护,引入 CAS 等复杂问题。

第二种方法是这个用户信息对象有多少属性就存成多少个 key-value 对,用用户 ID+ 对应属性的名称作为唯一标识来取的对应属性的值,虽然省去了序列化开销和并发问题,但是用户ID 重复存储,如果存在大量这样的数据,内存浪费还是非常大的。

List

常用命令 :lpush/rpush/lpop/rpop/lrange等;

应用场景

1、对数据量大的集合数据删减

列表数据显示,关注列表,粉丝列表,留言评论等…分页,热点新闻等利用 LRANG 还可以很方便的实现分页的功能,在博客系统中,每篇博文的评论也可以存入一个单独的 list 中。

2、任务队列

(list 通常用来实现一个消息队列,而且可以确认表先后顺序,不必像 MySQL 那样还需要通过 ORDER BY 来进行排序)

实现方式:Redis list的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。

Set

常用命令 :sadd/spop/smembers/sunion等;应用场景 :Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的;

实现方式:set 的内部实现是一个 value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。

Sorted Set

常用命令 :zadd/zrange/zrem/zcard等;应用场景 :Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择sorted set数据结构,比如twitter 的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排好序的。

实现方式:Redis sorted set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。

4.redis相比memcached有哪些优势?

4.1 memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型

4.2 redis的速度比memcached快很多

4.3 redis可以持久化其数据

5.Memcache与Redis的区别都有哪些?

5.1 存储方式 Memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。Redis有部份存在硬盘上,这样能保证数据的持久性。

5.2 数据支持类型 Memcache对数据类型支持相对简单。Redis有复杂的数据类型。

00.为什么redis需要把所有数据放到内存中?

Redis为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。所以redis具有快速和数据持久化的特征。如果不将数据放在内存中,磁盘I/O速度为严重影响redis的性能。在内存越来越便宜的今天,redis将会越来越受欢迎。

如果设置了最大使用的内存,则数据已有记录数达到内存限值后不能继续插入新值。

00.WATCH命令和基于CAS的乐观锁?

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。

在Redis的事务中,WATCH命令可用于提供CAS(check-and-set)功能。假设我们通过WATCH命令在事务执行之前监控了多个Keys,倘若在WATCH之后有任何Key的值发生了变化,EXEC命令执行的事务都将被放弃,同时返回Null multi-bulk应答以通知调用者事务执行失败。

00.假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?

使用keys指令可以扫出指定模式的key列表。

对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?

这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。

这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表, scan命令或者其他的scan如SSCAN ,HSCAN,ZSCAN命令,可以不用阻塞主线程,并支持游标按批次迭代返回数据,所以是比较理想的选择。keys相比scan命令优点是,keys是一次返回,而scan是需要迭代多次返回。 但是会有一定的重复概率,在客户端做一次去重就可以了, scan命令的游标从0开始,也从0结束,每次返回的数据,都会返回下一次游标应该传的值,我们根据这个值,再去进行下一次的访问,如果返回的数据为空,并不代表没有数据了,只有游标返回的值是0的情况下代表结束。 整体所花费的时间会比直接用keys指令长。

SCAN的遍历顺序

我们的Redis中有3个key,我们每次只遍历一个一维数组中的元素。如上所示,SCAN命令的遍历顺序是0-2-1-3这个顺序看起来有些奇怪。我们把它转换成二进制就好理解一些了。00-10-01-11我们发现每次这个序列是高位加1的。普通二进制的加法,是从右往左相加、进位。而这个序列是从左往右相加、进位的。这一点我们在redis的源码中也得到印证。

这是因为需要考虑遍历时发生字典扩容与缩容的情况。我们来看一下在SCAN遍历过程中,发生扩容时,遍历会如何进行。加入我们原始的数组有4个元素,也就是索引有两位,这时需要把它扩充成3位,并进行rehash。原来挂接在xx下的所有元素被分配到0xx和1xx下。当我们即将遍历10时,dict进行了rehash,这时,scan命令会从010开始遍历,而000和100(原00下挂接的元素)不会再被重复遍历。

再来看看缩容的情况。假设dict从3位缩容到2位,当即将遍历110时,dict发生了缩容,这时scan会遍历10。这时010下挂接的元素会被重复遍历,但010之前的元素都不会被重复遍历了。所以,缩容时还是可能会有些重复元素出现的。

00.底层数据结构

请添加图片描述

Redis 的底层数据结构有六种,简单动态字符串、双向链表、压缩列表、哈希表、跳表和整数数组,String 的底层实现是简单动态字符串,List、Hash、Set 和 SortedSet 都有两种底层实现结构,这四种类型被称为集合类型,特点是一个 key 对应一个集合数据

键和值的数据结构是什么

Redis 用一个哈希表保存所有键值对,实现 key-value 快速访问。一个哈希表就是一个数组,数组每个元素叫哈希桶,每个哈希桶保存键值对数据。然而哈希桶中的元素不是 value 本身,而是指向 value 的指针,即 value 存储的内存地址。

请添加图片描述

如图,这个哈希表保存了所有键值对,哈希桶中的 entry 元素保存key 和value指针,哈希表能在 O(1) 时间复杂度快速查找键值对,所以我们只需要计算 key 的哈希值就能找到对应的哈希桶位置,进而找到对应的 entry 元素。不同类型的 value 都能被找到,不论是 String、List、Set、Hash。这种查找方式只需要进行一次哈希计算,不论数据规模多少,然而,在 Redis 中写入大量数据后,操作有时候会变慢,因为出现了哈希表的冲突以及 rehash 带来的操作阻塞。

哈希冲突

当哈希表中数据增加,新增的数据 key 哈希计算出的哈希值和老数据 key 的哈希值会在同一个哈希桶中,也就是说多个 key 对应同一个哈希桶。

链式哈希

Redis 中,同一个哈希桶中多个元素用一个链表保存,它们之间用指针连接,这就是链式哈希。如图所示,entry1、entry2 和 entry3 都保存在哈希桶 3 中,导致哈希冲突。entry1 增加个next 指针指向 entry2,entry2 增加next 指针指向 entry3,不论哈希桶 3 元素有多少个,都可以通过指针连接起来,形成一个链表,叫做哈希冲突链。

请添加图片描述

链式哈希会产生一个问题,随着哈希表数据越来越多,哈希冲突越来越多,单个哈希桶链表上数据越来越多,查找时间复杂度退化到 O(n),查找耗时增加,效率降低。

rehash

为解决这个问题,Redis 会对哈希表做 rehash 操作。rehash 也就是增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。

Redis 使用两个全局哈希表:哈希表 1 和哈希表 2,最开始新增数据默认存到哈希表 1,哈希表 2 没有被分配空间,当数据增加,Redis 开始执行 Rehash 操作:

  1. 给哈希表 2 分配更大空间,可以是当前哈希表 1 大小的两倍
  2. 把哈希表 1 的数据重新映射并拷贝到哈希表 2
  3. 释放哈希表 1 空间

rehash 后,从哈希表 1 切换到哈希表 2,哈希表 2 空间更多,哈希冲突更少,原来哈希表 1 留做下次 rehash 扩容备用,按同样的步骤把哈希表 2 的数据迁移到哈希表 1。在第二步涉及大量数据拷贝,如果一次性把哈希表 1 迁移完,耗时很长,会造成线程阻塞,无法处理其他请求,Redis 是怎么处理这个问题呢?它采用渐进式 rehash

渐进式 rehash

在第二步中,Redis 正常处理客户端请求,每处理一个请求,从哈希表 1 的第一个索引位置开始,把这个位置上的所有 entry 拷贝到哈希表 2 中。处理下一个请求时,把下一个索引位置的 entry 做同样操作。

请添加图片描述
渐进式 rehash 把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。

00.使用过Redis做异步队列么,你是怎么用的?

一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。

如果对方追问可不可以不用sleep呢?list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。

如果对方追问能不能生产一次消费多次呢?使用pub/sub主题订阅者模式,可以实现1:N的消息队列。

如果对方追问pub/sub有什么缺点?在消费者下线的情况下,生产的消息会丢失(因为消费者消费一半下线,东西会丢失),得使用专业的消息队列如rabbitmq等。

如果对方追问redis如何实现延时队列? 使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。

00.如果有大量的key需要设置同一时间过期,一般需要注意什么?

如果大量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。一般需要在时间上加一个随机值,使得过期时间分散一些。

00.Pipeline有什么好处,为什么要用pipeline?

可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。

00.是否使用过Redis集群,集群的原理是什么?

Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。

Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。

01.说一下在你项目中的redis的应用场景

1、缓存

缓存现在几乎是所有大中型网站都在用的必杀技,合理利用缓存提升网站的访问速度,还能大大降低数据库的访问压力。Redis 提供了键过期功能,也提供了灵活的键淘汰策略,所以,现在 Redis 用在缓存的场合非常多。

2、排行榜

Redis 提供的有序集合数据类结构能够实现复杂的排行榜应用。

3、计数器

视频网站的播放量,每次浏览 +1,并发量高时如果每次都请求数据库操作无疑有很大挑战和压力。Redis 提供的 incr 命令来实现计数器功能,内存操作,性能非常好,非常适用于这些技术场景。

4、分布式会话

相对复杂的系统中,一般都会搭建 Redis 等内存数据库为中心的 session 服务,session 不再由容器管理,而是由 session 服务及内存数据管理。

5、分布式锁

在并发高的场合中,可以利用 Redis 的 setnx 功能来编写分布式的锁,如果设置返回 1,说明获取锁成功,否则获取锁失败。

6、社交网络

点赞、踩、关注/被关注,共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库不适合这种类型的数据,Redis 提供的哈希,集合等数据结构能很方便的实现这些功能。

7、最新列表

Redis 列表结构,LPUSH 可以在列表头部插入一个内容 ID 作为关键字,LTRIM 可以用来限制列表的数量,这样列表永远为 N ,无需查询最新的列表,直接根据 ID 去到对应的内容也即可。

8、消息系统

消息队列是网站经常用的中间件,如 ActiveMQ,RabbitMQ,Kafaka 等流行的消息队列中间件,主要用于业务解耦,流量削峰及异步处理试试性低的业务。Redis 提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。

02.redis是单线程还是多线程

在一开始的时候,Redis采用的是单线程模型, Redis 是基于内存操作的,它的瓶颈在于机器的内存、网络带宽,而不是 CPU,因为在你 CPU 还没达到瓶颈时你的内存可能就先满了、或者带宽达到瓶颈了。因此 CPU 不是主要原因,那么自然就采用单线程了。更何况使用多线程还会面临一些额外的问题,比如共享资源的保护等等,对于一个 CPU 不是主要瓶颈的键值对数据库而言,采用单线程是非常合适的。

Redis 在 4.0 以及之后的版本中引入了惰性删除(也叫异步删除),这是由额外的线程执行的,意思就是我们可以使用异步的方式对 Redis 中的数据执行删除操作了,这样处理的好处就是不会使 Redis 的主线程卡顿,会把这些删除操作交给后台线程来执行。例如:unlink key、flushdb async、flushall async,举个例子:

127.0.0.1:6379set name hanser
OK
127.0.0.1:6379get name
"hanser"
127.0.0.1:6379unlink name  ## 这里是异步删除一个 key,同步的话则是 del name
(integer) 1
127.0.0.1:6379flushdb async
OK
127.0.0.1:6379flushall async
OK

通常情况下使用 del 指令可以很快的删除数据,但是当被删除的 key 是一个非常大的对象时,例如:删除的是包含了成千上万个元素的 hash 集合,那么 del 指令就会造成 Redis 主线程卡顿,因此使用惰性删除可以有效的避免 Redis 卡顿的问题。

除了惰性删除,像持久化、集群数据同步等等,都是由额外的子线程执行的,而 Redis 主线程则专注于网络 IO 和键值对读写。

Redis4.0 之前是单线程的,那既然是单线程为什么速度还能那么快?吞吐量还能那么高?

原因有以下几点:

基于内存操作:Redis 的所有数据都在内存中,因此所有的运算都是内存级别的,所以它的性能比较高
数据结构简单:Redis 的数据结构是为自身专门量身打造的,而这些数据结构的查找和操作的时间复杂度都是 O(1)
多路复用和非阻塞 I/O:Redis 使用 I/O 多路复用功能来监听多个 socket 连接客户端,这样就可以使用一个线程来处理多个情况,从而减少线程切换带来的开销,同时也避免了 I/O 阻塞操作,从而大大地提高了 Redis 的性能
避免上下文切换:因为是单线程模型,因此就避免了不必要的上下文切换和多线程竞争,这就省去了多线程切换带来的时间和性能上的开销,而且单线程不会导致死锁的问题发生

非阻塞 I/O 和 I/O 多路复用是什么?

首先我们可以使用 get 命令,获取一个 key 对应的 value,比如:

127.0.0.1:6379get name
"hanser"

那么问题来了,以上对于 Redis 服务端而言,都发生了哪些事情呢?

服务端必须要先监听客户端请求(bind/listen),然后当客户端到来时与其建立连接(accept),从 socket 中读取客户端的请求(recv),对请求进行解析(parse),这里解析出的请求类型是 get、key 是 “name”,再根据 key 获取对应 value,最后返回给客户端,也就是向 socket 写入数据(send)。

以上所有操作都是由 Redis 主线程依次执行的,但是里面会有潜在的阻塞点,分别是 accept 和 recv。当 Redis 监听到一个客户端有连接请求、但却一直未能成功建立连接,那么主线程会一直阻塞在 accept 函数这里,导致其它客户端无法和 Redis 建立连接。类似的,当 Redis 通过 recv 从客户端读取数据时,如果数据一直没有到达,那么 Redis 主线程也会一直阻塞在 recv 这一步,因此这就导致了 Redis 的效率会变得低下。

非阻塞 I/O

但很明显,Redis 不会允许这种情况发生,因为以上都是阻塞 I/O 会面临的情况,而 Redis 采用的是非阻塞 I/O,也就是将 socket 设置成了非阻塞模式。首先在 socket 模型中,调用 socket() 方法会返回 “主动套接字”,调用 bind() 方法绑定 IP 和 端口,再调用 listen() 方法将 “主动套接字” 转化为 “监听套接字”,最后 “监听套接字” 调用 accept() 方法等待客户端连接的到来,当和客户端建立连接时再返回 “已连接套接字”,而后续就通过 “已连接套接字” 来和客户端进行数据的接收与发送。

但是注意:我们说在 listen() 这一步,会将 “主动套接字” 转化为 “监听套接字”,而此时的 “监听套接字” 的类型是阻塞的,阻塞类型的 “监听套接字” 在调用 accept() 方法时,如果没有客户端来连接的话,就会一直处于阻塞状态,那么此时主线程就没法干其它事情了。所以在 listen() 的时候可以将其设置为非阻塞,而非阻塞的 “监听套接字” 在调用 accept() 时,如果没有客户端连接请求到达时,那么主线程就不会傻傻地等待了,而是会直接返回,然后去做其它的事情

类似的,我们在创建 “已连接套接字” 的时候也可以将其类型设置为非阻塞,因为阻塞类型的 “已连接套接字” 在调用 send() / recv() 的时候也会处于阻塞状态,比如当客户端一直不发数据的时候,“已连接套接字” 就会一直阻塞在 rev() 这一步。如果是非阻塞类型的 “已连接套接字”,那么当调用 recv() 但却收不到数据时,也不用处于阻塞状态,同样可以直接返回去做其它事情。

请添加图片描述

但是有两点需要注意:

  • 虽然 accept() 不阻塞了,在没有客户端连接时 Redis 主线程可以去做其它事情,但如果后续有客户端连接,Redis 要如何得知呢?因此必须要有一种机制,能够继续在 “监听套接字” 上等待后续连接请求,并在请求到来时通知 Redis。
  • send() / recv() 不阻塞了,相当于 I/O 的读写流程不再是阻塞的,读写方法都会瞬间完成并且返回,也就是它会采用能读多少就读多少、能写多少就写多少的策略来执行 I/O 操作,这显然更符合我们对性能的追求。但这样会面临一个问题,那就是当我们执行读取操作时,有可能只读取了一部分数据,剩余的数据客户端还没发过来,那么这些这些数据何时可读呢?同理写数据也是这种情况,当缓冲区满了,而我们的数据还没有写完,那么剩下的数据又何时可写呢?因此同样要有一种机制,能够在 Redis 主线程做别的事情的时候继续监听 “已连接套接字”,并且有数据可读写的时候通知 Redis。

这样才能保证 Redis 线程既不会像基本 IO 模型中一直在阻塞点等待,也不会无法处理实际到达的客户端连接请求和可读写的数据,而上面所提到的机制便是 I/O 多路复用。I/O 多路复用机制是指一个线程处理多个 IO 流,也就是我们经常听到的 select/poll/epoll,而 Linux 采用的是 epoll。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求,一旦有请求到达就会交给 Redis 线程处理,这样就实现了一个 Redis 线程处理多个 IO 流的效果。

请添加图片描述

上图就是基于多路复用的 Redis IO 模型,图中的 FD 就是套接字,可以是 “监听套接字”、也可以是 “已连接套接字”,Redis 会通过 epoll 机制来让内核帮忙监听这些套接字。而此时 Redis 线程或者说主线程,不会阻塞在某一个特定的套接字上,也就是说不会阻塞在某一个特定的客户端请求处理上。因此 Redis 可以同时和多个客户端连接并处理请求,从而提升并发性。但为了在请求到达时能够通知 Redis 线程,epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。

那么回调机制是怎么工作的呢?以上图为例,首先 epoll 一旦监测到 FD 上有请求到达时,就会触发相应的事件。这些事件会被放进一个队列中,Redis 主线程会对该事件队列不断进行处理,这样一来 Redis 就无需一直轮询是否有请求发生,从而避免资源的浪费。同时,Redis 在对事件队列中的事件进行处理时,会调用相应的处理函数,这就实现了基于事件的回调。因为 Redis 一直在对事件队列进行处理,所以能及时响应客户端请求,提升 Redis 的响应性能。

我们以实际的连接请求和数据读取请求为例,再解释一下。连接请求和数据读取请求分别对应 Accept 事件和 Read 事件,Redis 分别对这两个事件注册 accept 和 get 回调函数,当 Linux 内核监听到有连接请求或数据读取请求时,就会触发 Accept 事件或 Read 事件,然后内核就会回调 Redis 注册的 accept 函数或 get 函数。

Redis 6.0 中的多线程

Redis 6.0 引入了一些新特性,其中非常受关注的一个特性就是多线程。在 4.0 之前 Redis 是单线程的,因为单线程的优点很明显,不但降低了 Redis 内部实现的复杂性,也让所有操作都可以在无锁的情况下进行,并且不存在死锁和线程切换带来的性能以及时间上的消耗。但是其缺点也很明显,单线程机制导致 Redis 的 QPS(Query Per Second,每秒查询数)很难得到有效的提高(虽然已经够快了,但人毕竟还是要有更高的追求的)。

而 Redis 从 4.0 版本开始引入了多线程,但是此版本的多线程主要用于大数据量的异步删除,对于非删除操作的意义并不是很大。

Redis 6.0 中的多线程则是真正为了提高 I/O 的读写性能而引入的,它的主要实现思路是将主线程的 I/O 读写任务拆分给一组独立的子线程去执行,也就是说从 socket 中读数据和写数据不再由主线程负责,而是交给了多个子线程,这样就可以使多个 socket 的读写并行化了。这么做的原因就在于,虽然在 Redis 中使用了 I/O 多路复用和非阻塞 I/O,但我们知道数据在内核态空间和用户态空间之间的拷贝是无法避免的,而数据的拷贝这一步是阻塞的,并且当数据量越大时拷贝所需要的时间就越多。所以 Redis 在 6.0 引入了多线程,用于分摊同步读写 I/O 压力,从而提升 Redis 的 QPS。但是注意:Redis 的命令本身依旧是由 Redis 主线程串行执行的,只不过具体的读写操作交给独立的子线程去执行了(后面会详细说明 Redis 的主线程和子线程之间是如何协同的),而这么做的好处就是不需要为 Lua 脚本、事务的原子性而额外开发多线程互斥机制,这样一来 Redis 的线程模型实现起来就简单多了。因为和之前一样,所有的命令依旧是由主线程串行执行的,只不过具体的读写任务交给了子线程。

除了引入多线程,还可以将内核网络协议栈换成用户态网络协议栈(DPDK),让网络请求不在内核里进行,直接在用户态完成。因为 socket 无论是发送数据还是接收数据都需要经过内核,发送数据时会将数据从用户态拷贝到内核态的缓冲区中,再由内核进行发送;接收数据,也是由内核负责接收,然后再将数据从内核态的缓冲区中拷贝到用户态。因此两个节点上的应用程序进行 socket 通信时,实际上是两个节点的内核进行交互,至于每个节点的应用程序则都是和自己的内核进行交互。

虽然替换协议栈可以避免频繁地让内核参与网络请求处理,提升请求处理效率,但是 Redis 并没有采用这种做法。原因就是该做法要求 Redis 的整体架构中,需要添加对用户态网络协议栈的支持,需要修改 Redis 源码中和网络相关的部分,这会带来很多额外的开发工作量;而且新增代码还可以引入 bug,导致 Redis 程序不稳定,因此 Redis 6.0 中没有采用这种做法。

Redis 6.0 的主线程和子线程之间是如何协同的?

整体可以分为四个阶段:

阶段一:服务端和客户端建立 socket 连接,并分配子线程(处理线程)

首先,主线程负责接收建立连接请求,当有客户端请求到达时,主线程会创建和客户端的 socket 连接,该 socket 连接就是用来和客户端进行数据的传输的。只不过这一步不由主线程来做,主线程要做的事情是将该 socket 放入到全局等待队列中,然后通过轮询的方式选择子线程,并将队列中的 socket 连接分配给它,所以无论是从客户端读数据还是向客户端写数据,都由子线程来做。因为我们说 Redis 6.0 中引入多线程就是为了缓解主线程的 I/O 读写压力,而 I/O 读写这一步是阻塞的,所以应该交给子线程并行操作。

阶段二:子线程读取并解析请求

主线程一旦把 socket 连接分配给子线程,那么会进行阻塞状态,等待子线程完成客户端请求的读取和解析,得到具体的命令操作。由于可以有多个子线程,所以这个操作很快就能完成。

阶段三:主线程执行命令操作

等到子线程读取到客户端请求并解析完毕之后,然后再由主线程以单线程的方式执行命令操作,I/O 读写虽然交给了子线程,但是命令本身还是由 Redis 主线程执行的。

阶段四:子线程回写 socket、主线程清空全局队列

当主线程执行完命令操作时,还需要将结果写入缓冲区,而这一步显然要由子线程来做,因为是 I/O 读写。此时主线程会陷入阻塞,直到子线程将这些结果写回 socket 并返回给客户端。和读取一样,子线程将数据写回 socket 时,也是有多个线程在并行执行,所以写回 socket 的速度也很快。之后主线程会清空全局队列,等待客户端的后续请求。

请添加图片描述

如何开启多线程呢,需要修改 redis.conf 中的两个配置。

1. 设置 io-thread-do-reads 配置项为 yes,表示启用多线程。

io-thread-do-reads yes

2. 通过 io-threads 设置子线程的数量。

io-threads 3

表示开启 3 个子线程,但是注意,线程数要小于机器的 CPU 核数,线程数并不是越大越好。关于线程数的设置,官方的建议是如果为 4 核的 CPU,那么设置子线程数为 2 或 3;如果为 8 核的CPU,那么设置子线程数为 6。

最后关于 Redis 的性能,Redis 的作者在 2019 的 RedisConf 大会上提到,Redis6.0 引入的多线程 I/O 特性对性能的提升至少是一倍以上。国内也有人在阿里云使用 4 个线程的 Redis 版本和单线程的 Redis 版本进行比较测试,发现测试的结果和 Redis 作者说的一致,性能基本可以提高一倍。

03.redis存在线程安全的问题么

类似于02题,其核心处理是单线程的,所以线程安全

04.遇到过缓存穿透么

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。

解决方案:

(1) **对空值缓存:**如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟。缺点:对内存占有消耗过大。

(2) 设置可访问的名单(白名单):

使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。

采用布隆过滤器:(布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。

(3) **进行实时监控:**当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务

用户为什么能在前端发送一个后端不存在的数据??需要进行逻辑处理。

真实URL http://localhost:8080/user?id=1001 加密后的1001为afdafda79u92dsvdsaf 所以前端显示http://localhost:8080/user?id=afdafda79u92dsvdsaa

后端:afdafda79u92dsvdsaf解密得到100,若前端恶意拼接,解密失败,数据格式不正确

05.遇到过缓存击穿么

问题描述

热点key:某个key访问非常频繁,当key失效的时候有大量线程来构建缓存,导致负载增加,系统崩溃。

解决方案

key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。

1 使用锁,单机用synchronized,lock等,分布式用分布式锁。

2 缓存过期时间不设置,而是设置在key对应的value里。如果检测到存的时间超过过期时间则异步更新缓存。

3 在value设置一个比过期时间t0小的过期时间值t1,当t1过期的时候,延长t1并做更新缓存操作。

4 数据预热

06.如何避免缓存雪崩

缓存大量失效的时候,引发大量查询数据库

解决方案

如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩。 这个没有完美解决办法,但可以分析用户行为,尽量让失效时间点均匀分布。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。 缓存失效时的雪崩效应对底层系统的冲击非常可怕!

(1) 使用锁或队列

用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况

(2) 设置过期标志更新缓存:

记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。

(3) 将缓存失效时间分散开:

比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

(4)数据预热

可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀

07.redis是怎么删除过期key的(缓存时如何回收的)

redis 的 key 清理,也就是内存回收的时候主要分为:过期删除策略内存淘汰策略两部分。

过期删除策略删除到达过期时间的 key 。

第一种:定时检查删除

对于每一个设置了过期时间的 key 都会创建一个定时器,一旦达到过期时间都会删除。这种方式立即清除过期数据,对内存比较好,但是有缺点是:占用了大量 CPU 的资源去处理过期数据,会影响 redis 的吞吐量 和 响应时间。

第二种:惰性检查删除

当访问一个 key 的时候,才会判断该 key 是否过期,如果过期就删除。该方式能最大限度节省 CPU 的资源。但是对内存不太好,有一种比较极端的情况:出现大量的过期 key 没有被再次访问,因为不会被清除,导致占用了大量的内存。

第三种:定期检查删除

每隔一段时间,扫描redis 中过期key 的字典,并清除部分过期的key。这种方式是前俩种一种折中方法。不同的情况下,调整定时扫描时间间隔,让CPU 与 内存达到最优。

内存淘汰策略 redis 内存淘汰策略是指达到maxmemory极限时,使用某种算法来决定来清理哪些数据,以保证新数据存入。

第一类 不处理,等报错(默认的配置)

noeviction,发现内存不够时,不删除key,执行写入命令时直接返回错误信息。(Redis默认的配置就是noeviction)

第二类 从所有结果集中的key中挑选,进行淘汰

allkeys-random 就是从所有的key中随机挑选key,进行淘汰

allkeys-lru 就是从所有的key中挑选最近使用时间距离现在最远的key,进行淘汰

allkeys-lfu 就是从所有的key中挑选使用频率最低的key,进行淘汰。(这是Redis 4.0版本后新增的策略)

第三类 从设置了过期时间的key中挑选,进行淘汰

这种就是从设置了expires过期时间的结果集中选出一部分key淘汰,挑选的算法有:

volatile-random 从设置了过期时间的结果集中随机挑选key删除。

volatile-lru 从设置了过期时间的结果集中挑选上次使用时间距离现在最久的key开始删除

volatile-ttl 从设置了过期时间的结果集中挑选可存活时间最短的key开始删除(也就是从哪些快要过期的key中先删除)

volatile-lfu 从过期时间的结果集中选择使用频率最低的key开始删除(这是Redis 4.0版本后新增的策略)

08.如何进行缓存预热

1 可以通过缓存reload机制,预先去更新redis缓存,再即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀(但是由于不知道哪些是热数据,可能会造成很多数据没有缓存命中)

2 开发逻辑上也要对未命中缓存的击穿,穿透,雪崩问题,实施相应方案

09.Redis缓存与数据库一致性

一、实时同步

对强一致要求比较高的,应采用实时同步方案,即查询缓存查询不到再从DB查询,保存到缓存;更新缓

存时,先更新数据库,再将缓存的设置过期(建议不要去更新缓存内容,直接设置缓存过期)。

二、异步队列

对于并发程度较高的,可采用异步队列的方式同步,可采用kafka等消息中间件处理消息生产和消费。

用户进行高并发操作(读写)走Redis,MYSQL进行数据异步处理。 例如进行定时任务: 每天凌晨1点 将Redis中数据得到,更新到MYSQL(一次) 或者使用消息队列: RabbitMQ RocketMQ Kafka 。作用: 异步、流量销峰

10.数据库与缓存不一致如何解决

不一致产生的原因:我们在使用redis过程中,通常会这样做:先读取缓存,如果缓存不存在,则读取数据库。

不管是先写库,再删除缓存;还是先删缓存,再写库,都有可能出现数据不一致的情况。因为写和读是并发的,没法保证顺序,如果删了缓存,还没有来得及写库,另一个线程就来读取,发现缓存为空,则去数据库从库中读取数据写入缓存,此时缓存中为脏数据。如果先写了库,再删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。 如果是redis集群,或者主从模式,写主读从,由于redis复制存在一定的时间延迟,也有可能导致数据不一致。

请添加图片描述

(1+2)先一个写请求,淘汰缓存,写入数据库

(3+4+5)接着立刻一个读请求,读缓存,未命中缓存,读数据库从库,写缓存放入数据,以便后续的读能够cache hit(但是主从同步没有完成,缓存中放入了旧数据)

(6)最后,主从同步完成

导致的结果是:旧数据放入缓存,即使主从同步完成,后续仍然会从缓存一直读取到旧数据。

可以看到,加入缓存后,导致的不一致影响时间会很长,并且最终也不会达到一致。

问题分析

可以看到,这里提到的缓存与数据库数据不一致,根本上是由数据库主从不一致引起的。当主库上发生写操作之后,从库binlog同步的时间间隔内,读请求,可能导致有旧数据入缓存。假如数据库主从不一致没法彻底解决,引入缓存之后,binlog同步时间间隔内,也无法避免读旧数据。但是,有没有办法做到,即使引入缓存,不一致不会比“不引入缓存”更糟呢?这是更为实际的优化目标。

思路转化为:在从库同步完成之后,如果有旧数据入缓存,应该及时把这个旧数据淘汰掉。

不一致优化1

请添加图片描述

如上图所述,在并发读写导致缓存中读入了脏数据之后:

(6)主从同步

(7)通过工具订阅数据库从库的binlog,这里能够最准确的知道,从库数据同步完成的时间

(8)从库执行完写操作,向缓存再次发起删除,淘汰这段时间内可能写入缓存的旧数据

如此这般,至少能够保证,引入缓存之后,主从不一致,不会比没有引入缓存更坏。画外音:即使引入缓存,也只有一个很小的时间间隔,可能读到旧数据。

不一致优化2

【延时双删策略+缓存超时设置】结合起来。设置缓存过期时间,所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值,然后再写入缓存中;

延时双删策略

双删,其实就是删两次缓存的意思。延时,指的是在第一次删完缓存后,延迟一段时间,比如5秒或其他时间,然后再进行第二次删除缓存。

主要过程如下:先删除缓存;再写数据库;休眠一段时间;再次删除缓存;至于需要休眠多少,这个延迟时间很难评估,读者需要根据自己项目读数据响应时间具体给一个大概的值,然后延迟时间一般在读数据响应时间基础上,再加上几百毫秒,或者一秒即可,这样就可以确保写请求可以删除读请求造成的缓存脏数据。 第一次删除缓存可以采用同步方式删除,第二次删除缓存如果读者朋友担心同步删除会影响性能的话,可以采用异步线程去删; 如果第二次删除缓存失败了怎么办?当然是不断地循环尝试删除缓存,可以将删除失败的记录发送到消息队列,然后可以不断重试删除,可以配置最大重试次数,配置告警,直到删除成功。

11.redis简述一下主从不一致的问题?

redis的确默认是弱一致性。 主从库间命令复制是异步进行的 。Redis的主从复制最大的缺点就是延迟,主机负责写,从机负责备份,这个过程有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,从机器数量的增加也会使这个问题更加严重。主库收到写命令后,会发送给从库。但是,主库并不会等到从库实际执行完命令后,再把结果返回给客户端,而是主库自己在本地执行完命令后,就向客户端返回结果了。如果从库还没有执行主库同步过来的命令或执行不及时,主从库间的数据就不一致了。

slave启动成功连接到master后会发送一个sync命令;Master接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,以完成一次完全同步;slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。只要是重新连接master,一次完全同步(全量复制)将被自动执行。后续Master将新的所有收集到的修改命令依次传给slave,完成同步。

主要有两个原因:

1、主从库间的网络可能会有传输延迟,所以从库不能及时地收到主库发送的命令,导致命令执行延后

2、从库及时收到了主库的命令,但是正在处理其它复杂度高的命令(例如集合操作命令),导致命令阻塞,执行延后

解决

1 使用info replication 命令查看主库写命令进度(master_repl_offset)和从库复制写命令进度(slave_repl_offset),开发监控程序监控两者的差值,如果超过阀值则移除客户端对该从库的访问,避免数据不一致;如果两者差值恢复到阀值范围内,则重新调整客户端对从库的访问。

2 在配置中提供了必须有多少个client完成同步才能结束,可以配置同步因子将其变大,趋向于强一致性

3 wait 2 0 必须同步完才能结束,但是若有一个从下线了,则集群完蛋。

23点就有点违背redis的初衷了

12.描述一下持久化原理

1:RDB
Redis使用操作系统的多进程COW(copy on write)机制来实现快照持久化,Redis在持久化时会调用glibc的函数fork产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求。子进程刚刚产生的时候,它和父进程共享内存里面的代码段和数据段。子进程做数据持久化,不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。但是父进程会继续处理客户端的读写请求,然后对内存数据结构进行不断的修改。这时子进程相应的页面是没有变化的,还是进程产生时的那一瞬间的数据。子进程能看到的内存里面的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是Redis持久化叫“快照"的原因。接下来子进程就可以非常安心的遍历数据,进行序列化写磁盘了。

rdb的劣势:
①:fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
②:虽然Redis在fork的时候使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能
③:被备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改

rdb的优势:
①:适合大规模的数据恢复
②:对数据完整性和一致性要求不高更适合使用
③:节省磁盘空间
④:恢复速度快

2:AOF

AOF日志存储的是Redis服务器的顺序指令序列,AOF日志只记录对内存进行修改的指令记录。Redis在长期运行的过程中,AOF日志会越来越长,所以需要对AOF日志文件进行瘦身。Redis提供了bgrewriteaof指令用于对AOF日志进行瘦身,其原理就是开启一个子进程对内存进行遍历,转化成一系列的Redis的操作指令,序列化到一个新的AOF日志文件中,序列化完毕后再将操作期间发生的增量AOF日志追加到这个新的AOF日志文件中,追加完毕后就立即代替旧的AOF日志文件了,瘦身工作也就完成了。AOF日志是以文件的形式存在的,当程序对AOF日志文件进行写操作时,实际上是将内容写到了内核为文件描述符分配的一个内存缓冲中,然后内核会异步将数据刷回到磁盘的。Linux的glibc提供了fsync(int fd)函数可以将指定文件的内容强制从内核缓存刷到磁盘。只要Redis进程实时调用fsync函数就可以保证AOF日志不会丢失。但是fsync是一个磁盘IO操作,他很慢。一般在生产环境Redis同程是每隔1S左右同步一次,当然是可以进行配置的。以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来。AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)

优势:
备份机制更稳健 丢失数据概率更低
可读的日志文本,通过操作AOF稳健,可以处理误操作

劣势:
比起RDB占用更多的磁盘空间
恢复备份速度更慢
每次读写都同步的话 有一定的性能压力
存在个别bug,造成恢复不能

总结:
快照是通过开启子进程的方式进行的,他是一个比较耗资源的操作。
①:遍历整个内存,大块写磁盘会加重系统负载。
②:AOF的fsync是一个耗时的IO操作,它会降低Redis性能,同时也会增加系统IO负担。
所以通常在生产环境中,Redis主节点是不进行数据初始化的,持久化操作主要在从节点进行。从节点是备份节点,没有来自客户端请求的压力,它的操作系统资源往往比较充沛。

13.描述一下redis持久化方式?

// RDB原理:

Redis会复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程,来进行持久化。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。

// AOF原理:

AOF,它的出现是为了弥补RDB的不足(数据的不一致性),所以它采用日志的形式来记录每个写操作(读操作不记录),并追加到文件中,只许追加文件但不可以改写文件。Redis 重启会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。

AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename)。重写AOF文件的操作,并没有读取旧的AOF文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的AOF文件,这点和快照有点类似。重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。 Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发。当然,也可以在配置文件中进行配置。

会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),redis4.0版本后的重写,是指上就是把rdb 的快照,以二级制的形式附在新的AOF头部,作为已有的历史数据,替换掉原来的流水账操作。

14.redis中的事务三条指令是什么,第三条指令到达后执行失败了,怎么处理

multi
用来标记一个事务的开始。Redis会将后续的命令逐个放入队列中,然后使用EXEC命令原子化地执行这个命令序列。组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。

watch

监控某一个键,当事务在执行过程中,此键代码的值发生变化,则本事务放弃执行;否则,正常执行。当某个事务需要按条件执行时,就要使用这个命令将给定的键设置为受监控的。如果被监控的key值在本事务外有修改时,则本事务所有指令都不会被执行。Watch命令相当于关系型数据库中的乐观锁。

exec
用来执行事务队列中所有的命令。在一个事务中执行所有先前放入队列的命令,然后恢复正常的连接状态。
如果在把命令压入队列的过程中报错,则整个队列中的命令都不会执行,执行结果报错;如果在压队列的过程中正常,在执行队列中某一个命令报错,则只会影响本条命令的执行结果,其它命令正常运行;
返回值:这个命令的返回值是一个数组,其中的每个元素分别是原子化事务中的每个命令的返回值。

DISCARD 放弃队列运行

输入MULTI命令,输入的命令都会依次进入命令队列中,但不会执行。直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。命令队列的过程中可以使用命令DISCARD来放弃队列运行。

Redis事务三特性

单独的隔离操作
事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
没有隔离级别的概念
队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行

不保证原子性
事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

15.为什么使用setnx(redis实现分布式锁的指令)

使用Redis实现分布式锁最简单的方案是在获取锁之前先查询一下以该锁为key对应的value存不存在。SETNX my_key my_value PX milliseconds其中,NX表示只有当键key不存在的时候才会设置key的值,PX表示设置键key的过期时间,单位是毫秒。

如果存在,则说明该锁被其他客户端获取了,否则的话就尝试获取锁,获取锁的方法很简单,只要以该锁为key,设置一个随机的值就行了。设置最长TTL。比如,我们有一批任务需要由多个分布式线程处理,每个任务都有一个taskId,为了保证每个任务只被执行一次,在工作线程执行任务之前,先获取该任务的锁,锁的key可以为taskId。

但是:考虑这样一种情况:客户端A获取锁的时候设置了key的过期时间为2秒,然后客户端A在获取到锁之后,业务逻辑方法doSomething执行了3秒(大于2秒),当执行完业务逻辑方法的时候,客户端A获取的锁已经被Redis过期机制自动释放了,因此客户端A在获取锁经过2秒之后,该锁可能已经被其他客户端获取到了。当客户端A执行完doSomething方法之后接下来就是执行releaseLock方法释放锁了,由于前面说了,该锁可能已经被其他客户端获取到了,因此这个时候释放锁就有可能释放的是其他客户端获取到的锁。会出现释放了别的客户端申请的锁的问题,那么该如何进行改进呢?

有一个很简单的方法是,我们设置key的时候,将value设置为一个随机值r,当释放锁,也就是删除key的时候,不是直接删除,而是先判断该key对应的value是否等于先前设置的随机值,只有当两者相等的时候才删除该key,由于每个客户端产生的随机值是不一样的,这样一来就不会误释放别的客户端申请的锁了。

在redis主从结构下,出于性能的考虑,redis采用的是主从异步复制的策略,这会导致短时间内主库和从库数据短暂的不一致。试想,当某一客户端刚刚加锁完毕,redis主库还没有来得及和从库同步就挂了,之后从库中新选拔出的主库是没有对应锁记录的,这就可能导致多个客户端加锁成功,破坏了锁的互斥性。

如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?因为setnx和expire之间不是原子操作:我记得set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的!是原子操作

set key value [EX seconds] [PX milliseconds] [NX|XX]
EX seconds:设置失效时长,单位秒
PX milliseconds:设置失效时长,单位毫秒
NX:key不存在时设置value,成功返回OK,失败返回(nil)
XX:key存在时设置value,成功返回OK,失败返回(nil)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我顶得了

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值