redis相关

发布于:2025-05-19 ⋅ 阅读:(17) ⋅ 点赞:(0)

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,就在该层级插入新节点。删除操作类似,先找到要删除的节点,然后在所有包含该节点的层级中移除它。

双写一致性问题

“在缓存和数据库双写一致性的问题上,我通常采用 Redisson 分布式读写锁配合延迟双删策略来保证一致性。”

具体做法是:

  1. 加分布式写锁(Redisson RReadWriteLock 写锁)

    保证同一份数据在更新期间只有一个线程能执行写操作,其他线程阻塞或排队,避免并发写入导致的脏数据。
  2. 更新数据库

  3. 删除缓存

  4. 延迟一段时间后再删一次缓存(比如 500ms)

    避免极端情况,刚删完缓存就有并发请求读了旧数据进缓存,再次删掉刚写入的旧缓存值。

1.为什么要分布式锁

定义:分布式锁是一种跨进程、跨机器保证多节点环境下同一份资源互斥访问的机制。

跟本地锁(synchronizedReentrantLock)不一样,分布式系统里多台机器、多个 JVM 实例要抢占同一份资源,靠本地锁是无效的,必须靠能跨机器通信的锁。

分布式锁的常见实现方案

🚀 方案 1:基于 Redis 实现

通过 setnxset key value nx ex 实现抢锁

原理

  • SET key value NX EX 30

    • NX:不存在才设置

    • EX 30:超时时间 30s,防死锁

谁能抢到这个 key,谁就拿到锁
释放锁时:要确保只有加锁的客户端才能释放(避免误删)

📦 Redisson 就是基于这个封装的分布式锁中间件。

🚀 方案 2:基于 Zookeeper 实现

利用 Zookeeper 的临时顺序节点

原理

  • 客户端在某个目录下创建临时顺序节点

  • 谁的节点序号最小,谁拿到锁

  • 执行完成后删除节点

  • 其他客户端监听上一个节点,自动排队

优点:天然支持顺序 + 自动超时删除
缺点:Zookeeper 性能较 Redis 差,适合分布式协调,不适合超高并发

📖 Redisson 分布式锁原理细拆

📌 1️⃣ 加锁机制

调用:

RLock lock = redissonClient.getLock("order_lock");

👉 本质是通过 Redis 的 SET key value NX PX expireTime 命令 实现:

  • key:order_lock

  • value:UUID+线程ID(用来唯一标识当前持锁客户端)

  • NX:如果 key 不存在才设置(防止别人覆盖)

  • PX:锁超时时间(比如30秒)

  • expireTime 是具体的超时数值

 PX毫秒级,EX秒级

📌 2️⃣ 看门狗机制(Watchdog)

Redisson 默认有个锁自动续期机制,叫看门狗 Watchdog,你锁超时时间默认是 30s,但如果业务执行时间长,看门狗会每隔 10 秒自动给这把锁续期 30s,直到业务执行完成释放锁。

作用
✅ 防止业务执行慢,锁过期了别人进来了,造成脏数据。
✅ 防止死锁(因为有超时时间,异常时不会永远持有)

📌 3️⃣ 公平锁 & 非公平锁

🚀 非公平锁(默认)

谁来抢,谁先拿,抢占式,不保证先来先得。

🚀 公平锁

按照请求顺序排队,先来先得。

RLock fairLock = redissonClient.getFairLock("order_lock");

场景:如果订单支付或结算场景对顺序有严格要求,可以用公平锁。

📌 4️⃣ 读写锁

在 Redisson 中,读写锁是分开的:

RReadWriteLock rwLock = redissonClient.getReadWriteLock("order_lock"); RLock readLock = rwLock.readLock(); RLock writeLock = rwLock.writeLock();

特点

  • 读读共享

  • 写写互斥

  • 读写互斥

场景
比如外卖系统的商户店铺信息修改,修改时其他人不能查,查询能并发查。


📌 5️⃣ 联锁(MultiLock)

如果你的缓存是多Redis实例(比如有两个Redis节点A、B),要同时加上多台Redis的锁:


RLock lock1 = redissonClientA.getLock("order_lock"); RLock lock2 = redissonClientB.getLock("order_lock"); RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2); multiLock.lock();

场景
多Redis实例容灾,防止单节点挂掉造成锁失效。


📌 6️⃣ 红锁(RedLock)

阿里、京东、拼多多这些超高可用项目常用的多节点分布式锁。

核心原理
同时在N个Redis节点上获取锁,超半数成功才算成功。

Redisson 也封装好了:

RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3); redLock.lock();

场景
多Redis实例分布在不同机房,不同地理位置,确保一致性和高可用。

📖 总结:为什么这样设计?

模块 原理/机制 解决问题
Redis锁 SET NX PX 保证互斥
看门狗机制 自动续期 业务慢时防止锁超时脏写
公平锁/非公平锁 是否排队 保证顺序一致性/提高性能
读写锁 读读共享、写写互斥 提升读多写少场景的并发性能
联锁 同时给多个Redis节点上锁 多实例容灾防锁丢失
红锁 超半数节点上锁,才算成功 多机房容灾,保证高可用一致性

 Redis分布式锁的问题

SET key value NX PX 定义: 

  • key:order_lock

  • value:UUID+线程ID(用来唯一标识当前持锁客户端)

  • NX:如果 key 不存在才设置(防止别人覆盖)

  • PX:锁超时时间(比如30秒)

1.Redis分布式锁用 SETNX + EXPIRE 的隐患

📉 问题:

这个是两个原子操作

  • SETNX 成功后

  • EXPIRE 再设超时

如果程序在SETNX成功,执行EXPIRE前宕机、异常、超时、网络抖动等情况

👉 这个 lockKey 就会变成永不过期的死锁

1.正规写法 → SET NX EX (从2.6.12开始)

setnxexpire 合并成一个原子操作:

SET key value NX PX timeout

特点

  • 原子性保证:要么都成功,要么都不成功

  • 超时机制内置:防止死锁

2 .Redisson 原理

Redisson 内部也是用 SET NX PX 实现分布式锁,外加:

  • 看门狗机制(自动续期,业务异常能释放)

  • 锁值设置为 UUID + 线程ID,保证锁的可重入性和唯一性

 ** 面试题标准答法

:为什么早期 setnx + expire 有风险?

因为这是两个非原子操作,setnx 成功后如果程序异常或宕机,未执行 expire,这把锁就永远存在,形成死锁。在高并发场景下,可能导致大量无效锁积压,Redis内存溢出,影响系统可用性。

:怎么解决?

使用 SET NX PX 原子命令,保证加锁和设置超时时间在一个原子操作内完成。或者用 Redisson 这样的分布式锁框架,封装了自动续期和超时保护机制。

 2.忘了释放锁

📌 1️⃣ 程序异常没走 unlock()

比如:


java

复制编辑

lock.lock(); try { // 业务逻辑 int a = 1 / 0; // 抛异常 } finally { lock.unlock(); }

如果 lock.unlock() 写在 finally 之外,一旦业务中间出异常或者线程中断,锁就永远没人释放


📌 2️⃣ 程序宕机或重启

服务器直接挂了,或者 JVM 直接 down 掉,锁的 value 是内存里的 UUID+线程ID,没机会去执行 unlock 逻辑。

如果没有超时时间,这把锁就永远存在于 Redis 里,别人也抢不到,形成死锁。


📌 3️⃣ 锁超时时间太长,但业务其实很快完成了,锁迟迟不释放

比如你加了个 60s 的超时锁,业务2s就完事了,但你没 unlock,这个锁还会傻傻等到60s才自动释放,这段时间内别人都抢不到锁,影响并发效率。


📌 忘记释放锁带来的问题:

  • 死锁:资源永远没人释放,其他请求都卡死

  • 库存超卖、数据脏写:如果没有抢到锁的线程继续执行,出现并发脏数据问题

  • 内存溢出:大量死锁积压,Redis内存被占满

  • 系统雪崩:多个服务依赖这把锁,整个系统阻塞排队,雪崩


📌 怎么解决?

✅ 1️⃣ 超时时间保护

锁一定要带超时时间,比如:

SET key value NX PX 30000

就算忘记释放,30s后自动过期,防止死锁。

✅ 2️⃣ try-finally 强制释放

所有 lock.lock() 必须配对 finally

lock.lock(); try { // 业务 } finally { lock.unlock(); }

✅ 3️⃣ 看门狗机制(自动续期)

像 Redisson 分布式锁有内置的看门狗机制

  • 默认锁30s

  • 每10s自动续期

  • 保证业务未完成期间锁不超时

  • 一旦业务完成,finally unlock释放 

3.释放了他人的锁

假如线程A和线程B,都使用lockKey加锁。线程A加锁成功了,但是由于业务功能耗时时间很长,超过了设置的超时时间。这时候,redis会自动释放lockKey锁。此时,线程B就能给lockKey加锁成功了,接下来执行它的业务操作。恰好这个时候,线程A执行完了业务功能,接下来,在finally方法中释放了锁lockKey。这不就出问题了,线程B的锁,被线程A释放了

📌 怎么解决?

✅ 方案一:加 requestId 校验

我们在加锁的时候,锁的 value 设成 requestId(UUID+线程ID)

然后解锁前,判断当前锁是不是自己加的

lua脚本:

if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

👉 保证谁加的锁,谁来删,别人不允许删!

Redisson 内部就是用这个 Lua 脚本实现的 ✅


✅ 方案二:看门狗机制

Redisson自带看门狗机制

  • 默认锁超时时间是30s

  • 业务执行期间,每隔10s自动续期,把过期时间重新设为 30s

  • 只要线程活着,锁就不会被释放

保证业务执行完之前,锁都在,自然也就没有超时释放的问题了。

 4.大量失败请求

📌 自旋重试加锁机制

📌 什么是自旋锁?

在一定时间内,线程不停尝试获取锁,如果失败就休眠一小会儿,再次尝试。
超过设定超时时间还没抢到锁,就返回加锁失败。

📦 典型实现:

long end = System.currentTimeMillis() + 500; // 最多自旋500ms while (System.currentTimeMillis() < end) { if (tryLock()) { return true; } Thread.sleep(50); // 失败后休眠50ms } return false; // 超时还没抢到就放弃


📌 自旋锁好处 🚀

  • 避免长时间阻塞:自己控制重试+超时时间

  • 适合锁持有时间短的场景:比如高并发秒杀/库存扣减操作,几毫秒完成,不用排队,抢不到就自旋

  • 灵活控制重试频率和间隔,降低CPU和网络开销


📌 风险 ⚠️

  • 大量线程高频请求 Redis,占用 CPU 和网络资源,导致 Redis 瞬时QPS暴涨

  • 如果业务高峰期,频繁自旋容易导致 Redis 突破瓶颈,出现雪崩

  • 自旋时间、间隔设置不合理,容易超时或者浪费性能


📌 实战优化方案 ✅

1️⃣ 设置最大重试次数 + 自旋超时时间

防止无限死循环卡死线程池

2️⃣ 适当 Thread.sleep() 或随机 sleep

避免一窝蜂同一毫秒请求,形成请求洪峰

Thread.sleep(ThreadLocalRandom.current().nextInt(20, 50));

3️⃣ 合理设置锁超时时间,尽量配合看门狗机制

4️⃣ 热点key拆分

热点锁加前缀、hashTag,减少锁粒度,降低同一key竞争


📌 面试标准答法 ✅

:分布式锁中自旋重试机制怎么实现?优缺点是什么?


自旋重试是指在短时间内不断尝试获取锁,失败就休眠一小段时间,再重试,直到超时退出。
优点是减少线程阻塞,适合高并发、锁持有时间短的场景。
缺点是大量线程自旋会占用CPU和网络资源,若高峰期控制不当,容易导致Redis雪崩。

优化方法

  • 限制最大重试次数和总超时时间

  • 适当 sleep 或随机 sleep

  • 配合看门狗机制自动续期

  • 热点key拆分,降低锁粒度

5.锁重入问题

📌 什么是 Redis 锁重入?

锁重入就是:

同一个线程在持有锁的情况下,可以再次获取同一把锁,而且不会被阻塞。

比如 Java 的 ReentrantLock 就是可重入的:

lock.lock();

lock.lock(); // 这里线程不会阻塞

反例:如果锁是不可重入的,第二次lock会死等,导致死锁。.

📌 为什么 Redis 锁默认不能重入?

因为:

  • Redis 的锁是基于 setnx(SET if Not eXists)实现的

  • 不记录线程id / requestId

你用 setnx 加一次锁就是加一次,不区分是谁加的。
如果同一个线程在持锁期间再次加锁,Redis会返回false,不能成功加锁。

📌 怎么解决?

✅ 方法1:Redisson 实现了可重入锁

Redisson在内部:

  • 每个线程/请求维护一个 requestId

  • 每次加锁的时候判断:

    • 如果是自己的锁,就递增重入次数

    • 如果是别人持有,才会阻塞或重试

释放锁时

  • 每释放一次,重入次数减1

  • 直到次数为0,再释放redis的key

👉 实现了和 ReentrantLock 一样的效果

📦 简单示意:

RLock lock = redissonClient.getLock("order_lock"); lock.lock(); // 第一次加锁 lock.lock(); // 同线程内第二次加锁,重入成功 lock.unlock(); // 重入次数-1,不删key lock.unlock(); // 重入次数=0,删除redis key

📌 面试标准答法 ✅

:Redis分布式锁能重入吗?


原生 Redis分布式锁基于 setnx 实现,不支持锁重入。
如果同一个线程在持锁期间再次加锁,Redis会返回false,无法重入。

如果要实现可重入锁,可以使用 Redisson。
Redisson内部通过requestId和重入次数计数,判断是否是当前线程持有锁,实现可重入效果。

📌 什么是 Redis 分布式锁竞争问题?

锁竞争就是多个线程/服务/节点同时争抢同一个资源锁,导致:

  • 加锁失败

  • 自旋等待/超时

  • 或者死锁、锁被误删、业务错乱等情况

分布式场景里,多个节点都用 setnxRedisson 去抢 lockKey,这就天然存在竞争。


📌 常见竞争问题类型

✅ ① 高并发自旋导致 Redis 压力剧增

  • 大量请求高频率 setnx

  • Redis CPU 飙升,QPS暴涨,其他服务也受影响
    👉 解决:限速+休眠+退避策略(Redisson内置了)


✅ ② 锁超时释放,业务未完成,其他线程加锁成功

场景:

  • A线程拿到锁,耗时操作,超过锁的 expireTime,Redis自动释放锁

  • B线程立马抢到锁,执行新业务

  • A线程最后释放锁,把B的锁误删,导致锁失效,业务冲突

👉 解决:

  • requestId唯一标识+释放时校验

  • Redisson的看门狗机制,自动续期,防超时释放


✅ ③ 自旋死等 or 超时失败

  • A持有锁,B、C、D不断自旋或等待

  • 如果超时没抢到,只能 fail fast,业务降级或失败返回

👉 解决:

  • Redisson内置等待超时+重试机制

  • 可以结合阻塞队列+异步通知优化抢锁成功后通知等待线程


✅ ④ 锁粒度过大,导致串行化,影响系统吞吐

  • 比如锁全站订单的 order_lock

  • 所有订单相关业务都串行,严重拖慢整体响应

👉 解决:

  • 热点key拆分 → 多个细粒度锁

    • order_lock_001

    • order_lock_002

  • 或用hash槽分布式锁


✅ ⑤ 锁不可重入,递归/嵌套调用死锁

刚刚你问过,我就不重复了,Redisson可重入锁搞定!


📌 面试标准答法 ✅

:Redis分布式锁存在哪些竞争问题,怎么解决?


常见竞争问题:

  1. 高并发自旋抢锁,Redis压力大
    → 加退避策略+限速

  2. 锁超时释放,业务未完成,其他线程抢到锁
    → 看门狗机制自动续期+释放时校验requestId

  3. 自旋超时失败,影响业务
    → 超时快速失败+异步通知机制优化

  4. 锁粒度过大,业务串行化,系统吞吐下降
    → 热点key拆分,细粒度锁拆分

  5. 锁不可重入,递归调用死锁
    → 使用Redisson可重入锁,记录线程Id+重入次数



网站公告

今日签到

点亮在社区的每一天
去签到