Redis相关

Table of Contents

Redis 5 种基本数据类型对应的底层数据结构实现如下表所示:

String List Hash Set Zset
SDS LinkedList/ZipList/QuickList Dict、ZipList Dict、Intset ZipList、SkipList

Redis为什么快? Redis 的模型是什么?

  1. 基于内存存储数据,减少了磁盘 I/O 的时间消耗。
  2. 数据结构简单高效,例如字符串、哈希表、列表、集合、有序集合等,能很好地满足各种场景的需求,并且操作的时间复杂度低。
  3. 单线程模型避免了多线程的上下文切换和并发控制的开销。
  4. I/O多路复用机制:Redis采用了I/O多路复用机制,提高了网络I/O并发性。
  5. Redis 从 6.0 版本开始,在某些操作上引入了 多线程 ,比如网络 I/O 读写。但 Redis 核心的数据结构操作仍然是单线程的。

Redis 的模型主要基于以下几个方面:

  1. 数据存储结构:Redis 支持多种数据结构,如字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)等。
  2. 内存管理:通过高效的内存分配和回收策略,充分利用内存空间。
  3. 通信协议:使用简单的文本协议 RESP(REdis Serialization Protocol)与客户端进行通信。
  4. 持久化机制:提供了 RDB(Redis Database)和 AOF(Append Only File)两种持久化方式,以保证数据的安全性和可靠性。
  5. 主从复制:支持主从架构,实现数据的备份和扩展读性能。
  6. 集群模式:在大规模数据存储和高并发访问场景下,可以使用 Redis 集群来实现数据的分布式存储和负载均衡。

Redis集群讲讲? 可用性怎么保证? 集群和哨兵的优缺点? 如何选举? 扩容怎么做?

  • Redis 集群是一种分布式架构,它通过将数据分布在多个节点上来实现横向扩展。
  • Redis 集群的可用性主要通过主从复制和故障转移来保证。每个节点都有一个或多个从节点,当主节点出现故障时,从节点会通过选举机制晋升为主节点,继续提供服务。
  • 集群的优点包括:实现了数据的分布式存储和高扩展性,能够处理大量的数据和高并发请求。缺点是配置和管理相对复杂。
  • 哨兵的优点是配置相对简单,能够自动监控和故障转移。缺点是在数据存储和扩展性方面不如集群。
  • 在 Redis 集群中,主节点的选举通常基于 Raft 算法或者类似的分布式一致性算法。
  • 对于 Redis 集群的扩容,可以通过添加新的节点,并对数据进行重新分片来实现。

Redis的缓存失效策略? 缓存过期策略?

  • Redis 提供了多种缓存失效策略,包括定时删除、惰性删除和定期删除。
  • 定时删除 策略会在设置的过期时间到达时立即删除缓存数据。这种策略可以确保缓存数据及时被清理,但可能会在过期时间到达时对性能产生一定的影响。
  • 惰性删除 策略则是在访问缓存数据时检查其是否过期,如果过期则删除。这种策略可以减少不必要的删除操作,但可能会导致过期数据在缓存中存在一段时间。
  • 定期删除 策略是每隔一段时间对缓存进行一次检查,并删除过期的数据。这种策略可以在一定程度上平衡性能和过期数据的清理,但需要合理设置检查的时间间隔。
  • 此外,Redis 还支持结合 LRU(最近最少使用)和 LFU(最不经常使用)策略进行内存管理,以更好地管理缓存数据。

Redis持久化怎么做的? RDB时子进程需要复制一整份父进程的内存数据吗?

  • Redis 有两种主要的持久化方式,RDB(Redis Database)和 AOF(Append Only File)。
  • RDB 持久化是通过创建一个经过压缩的二进制文件来保存 Redis 数据库在某个时间点上的数据集。
  • 可以通过手动执行 SAVE 命令或者配置自动触发的条件来生成 RDB 文件。
  • AOF 持久化则是将 Redis 执行的写命令记录到日志文件中,通过回放这些命令来恢复数据。

对于您提到的 RDB 时子进程是否需要复制一整份父进程的内存数据,答案是子进程会复制父进程的内存数据,但是采用了写时复制(Copy-On-Write)技术来优化内存使用。在生成 RDB 文件期间,父进程的修改操作会以新的内存页进行,不会影响子进程正在复制的内容。

Redis主从复制的原理?

  • Redis 主从复制的原理主要包括以下几个方面:
  • 首先,当建立主从关系后,从服务器会向主服务器发送 SYNC 命令。
  • 主服务器接收到 SYNC 命令后,会执行 BGSAVE 命令生成 RDB 快照文件,并将期间的写命令记录到缓冲区。
  • 然后,主服务器将 RDB 快照文件发送给从服务器,从服务器接收并载入这个 RDB 文件,完成数据的初始化。
  • 之后,主服务器会将缓冲区中的写命令发送给从服务器,从服务器执行这些命令,以保持与主服务器的数据一致性。
  • 此后,主服务器每次接收到写命令时,都会将命令同步给从服务器。
  • 从服务器在接收到主服务器传来的命令后,会执行这些命令,从而实现主从数据的同步。

Redis你们常用的数据结构都有哪些? skiplist结构是怎么样的?

Redis 常用的数据结构有:

  • String(字符串)
  • Hash(哈希)
  • List(列表)
  • Set(集合)
  • Zset(有序集合)

关于 skiplist 结构,它本质上也是一种查找结构,用于解决算法中的查找问题。它由 William Pugh 发明,最早出现于他在 1990 年发表的论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》。skiplist 数据结构是一个特殊的数据结构,它没法归属到基于平衡树或哈希表的查找问题解法中。

Redis事务怎么保证?

  • 首先,Redis 事务具有隔离性,在事务执行过程中,不会被其他客户端的命令打断。
  • 其次,Redis 事务具有原子性,要么所有命令都执行成功,要么所有命令都不执行。
  • 但需要注意的是,Redis 事务在执行过程中,如果出现语法错误,整个事务都会失败;
  • 但如果是运行时错误,比如对数据类型操作不当,错误命令之前的命令会执行成功,错误命令及之后的命令不会执行。

在实际开发中,您是如何运用 Redis 事务来解决业务问题的呢?

举个例子,比如在电商系统中,当用户下单时,需要同时

  • 扣减库存
  • 生成订单记录
  • 更新用户积分

  这一系列操作就可以放在 Redis 事务中进行,保证要么全部成功,要么全部失败,避免出现部分操作成功而部分操作失败导致的数据不一致问题。

Redis分布式锁你们怎么用的? 什么是锁超时和误删锁? 你们怎么解决的?

  • 在 Redis 中,分布式锁是指使用 Redis 实现的分布式锁机制,旨在解决分布式系统中的并发问题。
  • 实现 Redis 分布式锁的最简单的方法就是在 Redis 中创建一个 key,这个 key 有一个失效时间(TTL),以保证锁最终会被自动释放掉。当客户端释放资源(解锁)的时候,会删除掉这个 key。
  • 锁超时 是指锁在 Redis 中设置的失效时间(TTL)到期,导致锁被自动释放。
  • 误删锁 是指在释放锁时,错误地删除了其他客户端持有的锁。

为了解决锁超时和误删锁的问题,可以采用以下方法:

  • 锁续约:在获取锁后,定期延长锁的失效时间,以确保锁不会超时。
  • 锁标记:在获取锁时,为锁设置一个唯一的标记,在释放锁时,只有标记与锁匹配的客户端才能释放锁,以避免误删锁。

布隆过滤器的原理是什么? 优缺点是什么?

  • 布隆过滤器的原理是:
    • 当一个元素被加入集合时,
    • 通过 K 个散列函数将这个元素映射成一个位数组中的 K 个点(offset),
    • 把它们置为 1。
    • 检索时,我们只要看看这些点是不是都是 1 就(大约)知道集合中有没有它了。
  • 它的优点包括:
    • 节省空间:相比传统的集合存储方式,布隆过滤器可以大大节省存储空间。
    • 高效性:布隆过滤器的查询和插入操作都非常高效,可以在常数时间内完成。
    • 可扩展性:可以通过增加位数组的大小和散列函数的数量来提高布隆过滤器的精度。
  • 缺点包括:
    • 误判率:布隆过滤器可能会产生误判,即把不属于集合的元素误认为属于集合。
    • 不可删除:布隆过滤器不支持删除操作,因为删除一个元素可能会影响其他元素的判断。

Redis缓存穿透,缓存击穿,缓存雪崩

  • 缓存穿透: 处理不存在的数据请求,避免对后端数据库造成压力。
  • 缓存击穿: 处理突然大量请求同一数据的情况。
  • 缓存雪崩: 避免大量缓存数据同时过期,导致数据库压力过大。

一个缓存系统一般流程如下:

  • 前台请求,后台先从缓存中取数据
  • 取到直接返回结果,
  • 取不到时从数据库中取,数据库取到更新缓存,并返回结果,
  • 数据库也没取到,那直接返回空结果。

缓存穿透

  • 缓存穿透是指 缓存和数据库中都没有的数据 ,而用户不断发起请求。
  • 由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
  • 在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
  • 如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
  • 解决方案:
    • 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
    • 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击

缓存击穿

  • 缓存击穿是 指缓存中没有但数据库中有的数据 (一般是缓存时间到期),
  • 这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
  • 解决方案:
    • 1、设置热点数据永远不过期。
    • 2、接口限流与熔断,降级。重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些服务不可用时候,进行熔断,失败快速返回机制。
    • 3、布隆过滤器。bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小
    • 4、加互斥锁

缓存雪崩

  • 缓存雪崩是指 缓存中数据大批量到过期时间
  • 而查询数据量巨大,引起数据库压力过大甚至down机。
  • 和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
  • 解决方案:
    • 1 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
    • 2 如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中。
    • 3 设置热点数据永远不过期。

Redis数据类型,String内部实现?sds?介绍sds的自动扩容机制。

在 Redis 中,String 类型可以存储任何形式的文本数据,如文本字符串、二进制数据、JSON、XML 等。

在 Redis 的内部实现中,String 类型的数据结构可以是以下几种:

  1. 简单动态字符串(Simple Dynamic String, SDS)
    • 这是 Redis 自己实现的一种字符串数据结构,它与 C 语言中的字符数组相比,有以下特点:
      • SDS 维护了字符串的长度,这样获取字符串长度的操作可以是 O(1) 复杂度。
      • SDS 能够自动扩展,当对字符串进行修改时,如果超出了当前分配的空间,SDS 会自动进行空间扩展。
      • SDS 是二进制安全的,可以存储任何形式的二进制数据。
  2. 字节数组
    • 当 String 类型的数据被识别为整数(在一定范围内)时,Redis 可能会使用字节数组来存储这些数据,以节省内存。
  3. 压缩列表(ZipList)
    • 在 Redis 3.0 之前,当 String 类型的数据量较小时,可能会使用压缩列表来存储。压缩列表是一种内存高效的数据结构,它将多个数据项紧凑地存储在一起,以减少内存占用。
  4. 整数
    • 当 String 类型的数据是整数且在一定范围内时,Redis 可能会直接使用整数类型来存储,这样可以节省内存空间。

在 Redis 3.0 之后,对于小对象的存储,Redis 引入了一个新的数据结构叫做 Quicklist ,它是一个压缩列表和普通列表的混合体,用于存储大量的小对象。

总的来说,Redis 会根据存储的数据的特点和大小来选择合适的内部数据结构,以实现内存效率和性能的最优化。

Redis有哪几种持久化方式?

  1. RDB(Redis Database Snapshot)
    • 这种持久化方式是通过创建数据集的时间点快照来实现的。在指定的时间间隔内,Redis 会将内存中的数据快照保存到硬盘上的 RDB 文件中。
    • 优点: 恢复速度快,且只保存某一时刻的数据状态,文件体积小。
    • 缺点: 可能会丢失最近的数据,因为在快照之间的修改操作在故障时会丢失。
  2. AOF(Append Only File)
    • 这种持久化方式记录每次写操作命令,并在服务器启动时通过重新执行这些命令来恢复数据。
    • 优点: 是数据完整性更高,可以设置不同的 fsync 策略来控制数据的安全性。
    • 缺点: 文件体积可能会比 RDB 大,且恢复速度可能慢于 RDB。
  3. 混合持久化
    • Redis 4.0 引入了混合持久化方式,它结合了 RDB 和 AOF 的优点。
    • 在这种模式下,Redis 首先以 RDB 格式保存当前数据状态,然后继续以 AOF 格式记录新的写操作。这样既可以快速恢复数据,又可以保证数据的完整性。

Redis热键(Hot Key)是什么?怎么解决?

  • 热键(Hot Key)是指在缓存系统中,某些键(Key)由于被频繁访问而成为系统的性能瓶颈。
  • 当这些键的访问量远高于其他键时,它们可能会导致缓存服务器的负载不均衡,甚至可能影响到后端数据库的性能。

解决方法: 简单来讲就是:

  • 采用多级缓存
  • 热点key拆分

具体方法如下:

  1. 分散键名
    • 通过在键名中增加前缀或哈希散列的方式,将热点数据分散到不同的键上,从而避免单个键的访问量过大。
  2. 缓存预热
    • 在系统低峰期提前加载热点数据到缓存中,减少高峰时段的数据库压力。
  3. 本地缓存
    • 对于热键数据,可以考虑在应用层使用本地缓存,减少对分布式缓存系统的访问。
  4. 多级缓存
    • 引入多级缓存机制,例如浏览器缓存、Nginx缓存、应用层缓存(如 EhCache、Caffeine)和分布式缓存服务(如 Redis),通过逐级访问来优化读写性能和减少对后端数据库的压力。
  5. 读写分离
    • 对于读多写少的场景,可以将读操作和写操作分离,通过多个副本来分担读操作的压力。
  6. 使用队列
    • 在缓存更新时使用消息队列异步更新缓存,避免直接对缓存的大量写操作导致的压力。
  7. 限流
    • 对热键的访问进行限流,避免单个键的访问量过大影响系统稳定性。

数据库和缓存怎么保持一致性?

为了保证缓存与数据库的数据一致性,除了延时双删,还可以采用以下方法:

  1. 消息队列 通过消息队列异步更新缓存,当数据库更新后,发送一个消息到消息队列,然后由消费者更新缓存,这样可以解耦数据库和缓存的更新操作,提高系统的响应性能。
  2. 订阅数据库变更日志 如MySQL的Binlog,通过订阅数据库的变更日志来异步更新缓存,这样可以保证只要数据库数据变更成功,缓存最终也会得到更新。
  3. 给缓存设置过期时间 适用于读多写少的场景,通过设置合理的过期时间,可以让缓存数据在一定时间后失效,从而读取数据库中的最新数据。
  4. 先更新数据库,再删除缓存 这种策略可以避免在数据库更新和缓存更新之间的并发问题,但需要处理可能的缓存击穿和雪崩问题。
  5. 使用分布式锁 在更新操作时,使用分布式锁来保证同一时间只有一个进程可以更新数据,从而避免并发写入导致的数据不一致。
  6. 数据版本控制 在数据中引入版本号或时间戳,通过版本控制来处理数据更新和缓存一致性。

本地锁 vs 分布式锁

  • 对于单机多线程来说,在 Java 中,我们通常使用 ReentrantLock 类、 synchronized 关键字这类 JDK 自带的 本地锁 来控制一个 JVM 进程内的多个线程对本地共享资源的访问。
  • 分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果 多个JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。于是, 分布式锁 就诞生了。
  • 举个例子:系统的订单服务一共部署了 3 份,都对外提供服务。用户下订单之前需要检查库存,为了防止超卖,这里需要加锁以实现对检查库存操作的同步访问。由于订单服务位于不同的 JVM 进程中,本地锁在这种情况下就没办法正常工作了。我们需要用到分布式锁,这样的话,即使多个线程不在同一个 JVM 进程中也能获取到同一把锁,进而实现共享资源的互斥访问。

分布式锁应该具备哪些条件?

基本的分布式锁

  • 互斥:
    • 任意一个时刻,锁只能被一个线程持有。
  • 高可用:
    • 锁服务是高可用的,当一个锁服务出现问题,能够自动切换到另外一个锁服务。并且,即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。这一般是通过超时机制实现的。
  • 可重入:
    • 一个节点获取了锁之后,还可以再次获取锁。

好的分布式锁

在基本条件下,还需满足

  • 高性能:
    • 获取和释放锁的操作应该快速完成,
    • 并且不应该对整个系统的性能造成过大影响。
  • 非阻塞:
    • 如果获取不到锁,不能无限期等待,避免对系统正常运行造成影响。

常见分布式锁实现方案如下:

  • 基于关系型数据库比如 MySQL 实现分布式锁。
  • 基于分布式协调服务 ZooKeeper 实现分布式锁。
  • 基于分布式键值存储系统比如 Redis 、Etcd 实现分布式锁。

  关系型数据库的方式一般是通过 唯一索引或者排他锁 实现。不过,一般不会使用这种方式,问题太多比如 性能太差不具备锁失效机制 。所以一般基于 Redis 实现分布式锁;

基于 Redis 实现分布式锁

SETNX

  • 不论是本地锁还是分布式锁,核心都在于 互斥
  • 在 Redis 中, SETNX 命令是可以帮助我们实现互斥。SETNX 即 SET if Not eXists (对应 Java 中的 setIfAbsent 方法),如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在, SETNX 啥也不做。
  • 释放锁的话,直接通过 DEL 命令删除对应的 key 即可。
    • 其中,在释放锁时,为了防止误删到其他的锁,这里我们建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断。选用 Lua 脚本是为了保证 解锁操作的原子性
// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

锁过期时间

  • 这种方式实现的分布式锁,如果释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问。为了避免锁无法被释放,我们可以想到的一个解决办法就是: 给这个 key(也就是锁) 设置一个过期时间
  • 一定要保证设置指定 key 的值和过期时间是一个原子操作!!! 不然的话,依然可能会出现锁无法被释放的问题。(在lua脚本中,包含设置锁和设置锁超时时间)

锁续期

仅仅设置锁超时时间,同样存在漏洞:

  • 如果操作共享资源的时间 大于 过期时间,就会出现锁提前过期的问题,进而导致分布式锁直接失效。
  • 如果锁的超时时间设置过长,又会影响到性能。
  • 如果操作共享资源的操作还未完成,锁过期时间能够自己续期就好了!这个时候就需要 锁自动续期 了。
  • Redisson 就是这个问题现成的解决方案。

Redisson

  • Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,不仅仅包括多种分布式锁的实现。
  • 并且,Redisson 还支持 Redis 单机、Redis Sentinel、Redis Cluster 等多种部署架构。
  • Redisson 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 Watch Dog( 看门狗),如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。
  • 看门狗名字的由来于 getLockWatchdogTimeout() 方法,这个方法返回的是看门狗给锁续期的过期时间,默认为 30 秒(redisson-3.17.6)。
//默认 30秒,支持修改
private long lockWatchdogTimeout = 30 * 1000;

public Config setLockWatchdogTimeout(long lockWatchdogTimeout) {
    this.lockWatchdogTimeout = lockWatchdogTimeout;
    return this;
}
public long getLockWatchdogTimeout() {
   return lockWatchdogTimeout;
}
  • 默认情况下,每过 10 秒,看门狗就会执行续期操作,将锁的超时时间设置为 30 秒。
  • 看门狗续期前也会先判断是否需要执行续期操作,需要才会执行续期,否则取消续期操作。
  • Watch Dog 通过调用 renewExpirationAsync() 方法实现锁的异步续期。renewExpirationAsync 方法其实是调用 Lua 脚本实现的续期,这样做主要是为了保证续期操作的原子性。
  • 以 Redisson 的分布式可重入锁 RLock 为例来说明如何使用 Redisson 实现分布式锁:
// 1.获取指定的分布式锁对象
RLock lock = redisson.getLock("lock");
// 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制
// 只有未指定锁超时时间,才会使用到 Watch Dog 自动续期机制。
lock.lock();

// 手动给锁设置过期时间,不具备 Watch Dog 自动续期机制
//  lock.lock(10, TimeUnit.SECONDS);

// 3.执行业务
...
// 4.释放锁
    lock.unlock();

如何实现可重入锁?

  • 所谓可重入锁指的是在一个线程中可以多次获取同一把锁。
  • 比如一个线程在执行一个带锁的方法,该方法中又调用了 另一个需要相同锁 的方法,则该线程可以直接执行调用的方法即可重入 ,而 无需重新获得锁
  • 像 Java 中的 synchronized 和 ReentrantLock 都属于可重入锁。
  • 可重入分布式锁的实现核心思路是:
    • 线程在获取锁的时候判断是否为自己的锁,如果是的话,就不用再重新获取了。
    • 为此,我们可以为每个锁关联一个可重入计数器和一个占有它的线程。
    • 当可重入计数器大于 0 时,则锁被占有,需要判断占有该锁的线程和请求获取锁的线程是否为同一个。

Redisson 内置了多种类型的锁

  • 可重入锁(Reentrant Lock)
  • 自旋锁(Spin Lock)
  • 公平锁(Fair Lock)
  • 多重锁(MultiLock)
  • 红锁(RedLock)
  • 读写锁(ReadWriteLock)。

Redis 如何解决集群情况下分布式锁的可靠性?

  • 为了避免单点故障,生产环境下的 Redis 服务通常是集群化部署的。
  • Redis 集群下,由于 Redis 集群数据同步到各个节点时是异步的,如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。
  • 针对这个问题,Redis 之父 antirez 设计了 Redlock 算法 来解决。
    • Redlock 算法的思想是让客户端向 Redis 集群中的多个独立的 Redis 实例依次请求申请加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。
    • 即使部分 Redis 节点出现问题,只要保证 Redis 集群中有半数以上的 Redis 节点可用,分布式锁服务就是正常的。
    • Redlock 是直接操作 Redis 节点的,并不是通过 Redis 集群操作的,这样才可以避免 Redis 集群主从切换导致的锁丢失问题。
    • Redlock 实现比较复杂,性能比较差,发生时钟变迁的情况下还存在安全性隐患。
    • 实际项目中不建议使用 Redlock 算法,成本和收益不成正比,可以考虑基于 Redis 主从复制+哨兵模式实现分布式锁。

Redis事务支持回滚吗?如果订单取消,但是redis里的秒杀商品库存扣减了,怎么回滚?

  • Redis 的事务不支持回滚(Rollback)。
  • Redis 的事务功能是通过 MULTIEXECDISCARDWATCH 命令来实现的。
    • MULTI 命令开始一个事务,将后续的命令放入事务中,
    • EXEC 命令执行事务中的所有命令,
    • DISCARD 命令取消事务。但是,如果 EXEC 命令执行,那么事务中的所有命令都会执行,即使其中某些命令失败了,也不会回滚。
  • 如果需要在某些条件下回滚事务,需要在应用层面进行处理。
    • 例如,你可以在执行 EXEC 之前,先检查所有命令是否都能成功执行,如果不能,就不执行 EXEC ,从而避免事务中的命令被执行。

对于订单取消但 Redis 里的秒杀商品库存已经扣减的情况,可以采取以下策略来处理:

  1. 预扣库存: 在用户下单时,可以先预扣库存,但不立即减少库存数量,而是将订单与库存绑定,如果用户在一定时间内未支付,系统自动释放库存。
  2. 订单超时机制: 设置订单的超时时间,如果用户在超时时间内未完成支付,系统自动取消订单并回滚库存。
  3. 消息队列: 使用消息队列来处理订单和库存的扣减操作。当订单生成时,发送一个扣减库存的消息到队列,如果订单取消,发送一个回滚库存的消息到队列。
  4. 乐观锁: 在库存数据上使用版本号或时间戳,每次扣减库存时检查版本号或时间戳是否一致,如果一致则更新库存并更新版本号或时间戳,如果不一致则放弃操作。
  5. 补偿事务: 在订单取消时,手动执行一个补偿事务来增加库存数量,以回滚之前的扣减操作。
  6. 分布式事务: 如果业务场景复杂,需要跨多个服务或数据库进行事务管理,可以考虑使用分布式事务解决方案,如两阶段提交(2PC)或三阶段提交(3PC)。

介绍一下redis延时双删

延时双删的步骤

  • 第一次删除:
    • 当更新或删除数据库中的数据时,首先删除 Redis 中对应的缓存数据。
    • 这一步确保了在数据更新后, 任何新的请求都不会从缓存中读取到旧的数据
  • 等待一段时间:
    • 在删除缓存后,不立即进行下一步操作,而是等待一个短暂的延时(例如几百毫秒)。
    • 这个延时的目的是为了让那些在 删除缓存之前就已经到达的请求 有足够的时间完成对缓存的访问。
  • 第二次删除:
    • 在延时之后,再次删除 Redis 中对应的缓存数据。
    • 这一步是为了确保那些 在延时期间到达的请求 不会读取到已经被标记为删除的数据。

延时双删的原理

  • 解决缓存击穿问题:
    • 缓存击穿是指一个缓存数据过期或被删除后,新的请求直接打到数据库上,导致数据库压力突然增大。
    • 延时双删通过在删除缓存后等待一段时间,减少这种直接打到数据库的请求。
  • 减少缓存与数据库的不一致时间:
    • 通过两次删除操作,尽可能地减少了缓存数据和数据库数据不一致的时间窗口。

Redisson 延迟队列原理是什么?有什么优势?

  • Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,比如多种分布式锁的实现、延时队列。
  • 我们可以借助 Redisson 内置的延时队列 RDelayedQueue 来实现延时任务功能。
  • Redisson 的延迟队列 RDelayedQueue 是基于 Redis 的 SortedSet 来实现的。
  • SortedSet 是一个有序集合,其中的每个元素都可以设置一个分数,代表该元素的权重。
  • Redisson 利用这一特性,将需要延迟执行的任务插入到 SortedSet 中,并给它们设置相应的过期时间作为分数。
  • Redisson 使用 zrangebyscore 命令扫描 SortedSet 中过期的元素,然后将这些过期元素从 SortedSet 中移除,并将它们加入到就绪消息列表中。
  • 就绪消息列表是一个阻塞队列, 有消息进入就会被监听到。 这样做可以避免对整个 SortedSet 进行轮询,提高了执行效率。

相比于 Redis 过期事件监听实现延时任务功能,这种方式具备下面这些优势:

  • 减少了丢消息的可能:DelayedQueue 中的消息会被持久化,即使 Redis 宕机了,根据持久化机制,也只可能丢失一点消息,影响不大。
  • 消息不存在重复消费问题:每个客户端都是从同一个目标队列中获取任务的,不存在重复消费的问题。

订单超卖问题怎么解决?

  1. Mysql锁:
    • 乐观锁: 在数据库中使用乐观锁机制,通过版本号或时间戳来确保库存更新的原子性。
    • 悲观锁: 在关键的库存扣减操作中使用悲观锁,确保在操作期间库存数据不会被其他事务修改。
  2. 预扣库存: 在用户下单时预扣库存,但暂时不减少实际库存,直到支付完成后再确认扣减。
  3. 库存预留: 对于高需求商品,可以在用户下单后立即预留库存,设置一个合理的预留时间,如果用户在这段时间内未完成支付,则释放库存。
  4. 队列机制: 使用消息队列来管理订单和库存扣减操作,确保操作的 顺序执行
  5. 分布式锁: 使用Redis分布式锁来保证跨多个节点的库存扣减操作的一致性。
    • 查询库存 和 扣减库存这个操作不是原子性的。可能会引起多线程并发问题。
    • 所以用基于redission的分布式锁可以解决这个问题。
    • lua脚本实现原子操作。
  6. 限流: 对于高并发场景,通过限流来控制同时处理的订单数量,减少超卖的风险。
  7. 订单取消策略: 设定订单取消策略,对于超卖的订单,自动取消并给予用户适当的补偿。

Date: 2024-09-06 Fri 22:09