黑马点评面试题整理
黑马点评面试题整理
总体
1. 介绍一下你写的这个项目?
这是一个基于位置服务的社交平台,类似大众点评;为用户提供附近商家信息、用户点评、优惠卷秒杀、达人探店、好友关注、用户签到等功能,用户可以通过平台发现附近的美食、休闲娱乐等商家,并进行在线预订、评论。
Session
1. session共享有什么问题?
每个Tomcat都有自己的session,假设用户第一次访问第一台tomcat,并且将自己的信息存储到第一台tomcat的session中,但是第二次这个用户访问到第二台服务器的时候,第二台tomcat没有第一台服务器的session
2. 解决session共享问题?
早期解决方案:session拷贝(同步)
但是这也有问题:当服务器的压力过大,session拷贝数据,可能会出现延迟
3. 为什么用redis代替session实现登陆注册功能?
session的数据是存储于服务器端的,服务器的数据量非常大的时候,就容易造成内存不足
redis是基于内存的高性能数据库,读写速率非常快
4. 如何解决集群的session共享问题?
Redis分布式session代替Tomcat的Session存储,能够在分布式多机环境下保证获取用户登录的一致性
5. 使用Redis代替Session的业务流程的时候,设计key结构,为什么使用Hash代替String存储用户信息?Hash存储与String存储有什么区别?
如果使用String存储的时候,会用json存储全部用户信息,这样会占据大量的存储空间。如果使用Hash存储的时候,拿取用户信息会方便很多,方便对用户每个属性进行独立的更新和查询操作。
缓存(商品查询缓存)
1. 缓存更新的策略有什么?
内存淘汰:Redis自动进行,当Redis内存超过我们设定的max-memery时,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)超时剔除:当我们给Redis设置了过期时间TTL之后,Redis会将超时的数据进行删除,方便我们继续使用缓存主动更新:我们可以手动调用方法把缓存删除掉,通常用于解决缓存和数据库不一致问题
| 内存淘汰 | 超时剔除 | 主动更新 | |
|---|---|---|---|
| 说明 | 不用自己维护,利用Redis的内存淘汰机制,当内存不足时自动淘汰部分数据。下次查询时更新缓存。 | 给缓存数据添加TTL时间,到期后自动删除缓存。下次查询时更新缓存。 | 编写业务逻辑,在修改数据库的同时,更新缓存。 |
| 一致性 | 差 | 一般 | 好 |
| 维护成本 | 无 | 低 | 高 |
-
业务场景
-
低一致性需求:使用内存淘汰机制,例如店铺类型的查询缓存(因为这个很长一段时间都不需要更新)
-
高一致性需求:主动更新,并以超时剔除作为兜底方案,例如店铺详情查询的缓存
2. 数据库和缓存不一致的解决方案?
-
由于我们的缓存数据源来自数据库,而数据库的数据是会发生变化的,因此,如果当数据库中数据发生变化,而缓存却没有同步,此时就会有一致性问题存在,其后果是
-
用户使用缓存中的过时数据,就会产生类似多线程数据安全问题,从而影响业务,产品口碑等
-
那么如何解决这个问题呢?有如下三种方式
-
Cache Aside Pattern 人工编码方式:缓存调用者在更新完数据库之后再去更新缓存,也称之为双写方案
-
Read/Write Through Pattern:缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题。但是维护这样一个服务很复杂,市面上也不容易找到这样的一个现成的服务,开发成本高
-
Write Behind Caching Pattern:调用者只操作缓存,其他线程去异步处理数据库,最终实现一致性。但是维护这样的一个异步的任务很复杂,需要实时监控缓存中的数据更新,其他线程去异步更新数据库也可能不太及时,而且缓存服务器如果宕机,那么缓存的数据也就丢失了
3. 主动更新策略要考虑什么?
- 更新数据库的时候要更新缓存还是删除缓存?
- 更新缓存:每次更新数据库都更新缓存,这样无效写操作比较多
- 删除缓存:更新数据库让缓存失效,查询时再更新缓存
- 如何保证缓存与数据库的操作同时成功或失败?
- 单体系统:将缓存与数据库操作放在一个事务
- 分布式系统:利用TCC等分布式事务方案
- 先操作缓存还是数据库?
一般先操作数据库,因为Redis读写更快,放在后面操作,延迟的概率要小一点
- 缓存更新策略的最佳实践方案:
-
低一致性需求:使用Redis自带的内存淘汰机制
-
高一致性需求:主动更新,并以超时剔除作为兜底方案
-
读操作:未命中,查询数据库,写入缓存,并设定超时时间
-
写操作:写数据库再删缓存,redis的读写速度更快一点
4. 如何解决缓存穿透、缓存击穿和缓存雪崩问题?
| 名称 | 现象 | 原因 | 解决方案 | 解决方案存在的问题 | 应对策略 |
|---|---|---|---|---|---|
| 缓存穿透 | 查询一个数据库都不存在的数据,这样用户的查询会直接绕过缓存打到数据库上面,对数据库造成压力 | 业务设计不合理(例如:爬虫爬取了一系列不存在的ID,但是后台并没有对这些不存在的ID做处理,比如布隆过滤器)。恶意攻击,故意伪造大量不存在的key发起请求。 | 缓存空对象布隆过滤器:将所有可能存在的key哈希到一张位图中 | 缓存空对象占用空间数据短时间不一致【如果用户在缓存空对象的五分钟内,添加了一条数据,这个时候缓存和数据库的数据不一致了】)内存占用较少,但是用二进制表示可能存在误判问题 | 设置一个较短时间(如1-5分钟),平衡内存消耗和数据延迟设置一个管理后台或接口,在数据被创建的时候,主动清理缓存如果想要减少误判,可以增加数组大小和函数数量,但是这样会增加内存 |
| 缓存雪崩 | 在同一时间内,大量缓存的key失效,或者Redis服务器宕机,造成大量请求到达数据库 | 设置缓存时采用了相同的过期时间(比如业务高峰期开始时,同时批量添加了缓存,并都设置了1小时的过期时间)。 | 给不同的key的TTL增加随机值利用Redis集群提高服务器的可用性降级限流多级缓存缓存永不过期(逻辑过期) | 1. 设置随机过期时间治标不治本:只是分散了缓存失效的时间点,降低了风险,但没有从根本上解决“大量缓存同时失效”的可能性。如果缓存服务重启,所有数据依然会同时被加载,并且过期时间相近。难以规划:过于随机的过期时间可能使得缓存的失效模式变得不可预测,给运维和问题排查带来一点点小麻烦。缓存永不过期 (逻辑过期)实现复杂度高 :这不再是简单的setex命令,需要将值和过期时间封装成一个新对象(例如{value: obj, expireTime: 1730000000000}),业务代码每次读取都需要解析和判断,增加了代码的复杂性。数据一致性风险:如果异步更新缓存失败,用户将一直读到脏数据。内存压力:理论上数据永远不会被自动淘汰,如果不再访问的冷数据很多,会造成内存的浪费。 | 结合“永不过期”+“后台更新”或“定时更新”策略使用。为了防止异步刷新缓存数据失败,需要有一套良好的重试和告警机制来保证更新成功。内存配合使用LRU(最近最少使用)淘汰策略或定期清理冷数据 |
| 缓存击穿 | 单个热点key突然失效 | 互斥锁逻辑过期 | 1. 互斥锁 (Mutex Lock) / 分布式锁性能瓶颈 (Performance Bottleneck):锁机制会强制让其他线程等待,即使是在高并发场景下,这在一定程度上降低了系统的吞吐量和增加了请求的延迟(latency)。如果重建缓存的过程很慢(如是一个复杂计算或慢查询),情况会更糟。死锁风险 (Deadlock Risk):如果获取锁的线程在更新缓存时意外挂掉,没有释放锁,可能会导致其他所有线程永远被阻塞。复杂度:需要引入分布式锁组件(如Redisson)或自己用SETNX实现,增加了系统复杂性。2. 逻辑过期永不过期数据不一致性 (Data Inconsistency):这是最大的问题。在异步更新线程完成之前,所有用户访问到的都是过期的旧数据。对于金融、库存等对实时性要求极高的场景,这是不可接受的。代码复杂度:同样需要封装数据对象和实现异步更新逻辑,复杂度高。 | 互斥锁解决死锁风险:为锁设置一个超时时间,这样即使持有锁的线程崩溃,锁也会自动释放。但这又可能引入新的问题:如果业务操作比超时时间长,锁可能被提前释放,导致多个线程同时去更新缓存。 |
没有任何一个方案是银弹(Silver Bullet)。在实际项目中,通常需要根据业务场景(能否接受短暂的数据不一致?)、数据特性(是热点数据还是冷数据?)、系统规模来进行组合使用。
例如:
- 对于缓存穿透:首先做好参数校验,这是最重要且性价比最高的。然后对无法校验的请求,使用布隆过滤器(数据量大时)或缓存空对象(数据量可控时)兜底。
- 对于缓存雪崩:设置随机过期时间是基础操作。同时对极其关键的数据采用**“永不过期”+后台定时更新**的策略。
- 对于缓存击穿:互斥锁方案更保证数据强一致,但性能有损耗,适合金融、库存等场景。逻辑过期方案性能更好,但只能保证最终一致,适合新闻、商品介绍等对延迟敏感但能容忍短暂不一致的场景。
互斥锁的逻辑处理图

逻辑过期的逻辑处理图

5. 布隆过滤器解析
定义:超高效的、会“误报”的“哨兵”
特点:
- 告诉你一个东西肯定不会,可能会
- 非常节省空间,远超传统的哈希表(如HashSet)
- 查询速度极快(O(k),k为哈希函数个数)
核心组成部分:
- 一个很长的二进制向量(位数组)
- 一组哈系函数
工作原理:
- 写入(Add) - 如何标记一个数据存在?
假设我们要把商品ID 101 加入布隆过滤器。
- 步骤一:用准备好的多个哈希函数分别计算
101的哈希值。 - 步骤二:每个哈希值都对位数组的长度取模,得到在位数组上对应的位置(格子)。
- 步骤三:将这些位置上的格子从
0设置为1。

- 查询(Check) - 如何判断一个数据是否存在?
现在用户请求查询商品ID 99999(一个可能不存在的数据)。
-
步骤一:用同一组哈希函数计算
99999的哈希值,并取模得到一组位置。 -
步骤二:检查位数组中这些位置上的值:
-
如果其中有任何一个位置的值为
**0**-> 那么可以肯定地说,99999绝对不存在于布隆过滤器中! -
如果所有位置的值都是
**1**-> 那么只能说,99999可能存在(原因看下面的缺点)。
为什么“都是1”却只是“可能存在”? 因为其他数据可能碰巧把其中的某些位设置成了1(这被称为哈希碰撞)。比如,101 设置了位置2、5、13,102 设置了位置5、8、14。当查询 99999 时,如果计算出的位置是 [5, 8, 13],这三个位置恰好都被其他数据设为1了,布隆过滤器就会误以为 99999 也存在。
秒杀优惠券业务流程详解
1. 核心数据表结构
tb_voucher(优惠券表)
- 存储优惠券基本信息:标题、规则、支付金额、抵扣金额等
- type 字段区分普通券(0)和秒杀券(1)
tb_seckill_voucher(秒杀优惠券表)
- 与优惠券表一对一关系
- 存储秒杀特有信息:库存、开始时间、结束时间
tb_voucher_order(优惠券订单表)
- 记录用户购买优惠券的订单信息
- 包含订单状态、支付方式、时间等
2. 🎯 业务特点:为什么秒杀优惠卷需要单独建表格?
- 秒杀券与普通券区别:
- 普通券: 直接购买,无时间限制
- 秒杀券: 限时抢购,库存有限,一人一单
- 订****单状态管理:
- 1: 未支付 → 2: 已支付 → 3: 已核销
- 支持取消(4)、退款(5-6)等状态
- 时间控制:
- begin_time: 秒杀开始时间
- end_time: 秒杀结束时间
- 系统会检查时间有效性
- 业务逻辑的本质差别:普通优惠卷无限量供应,不需要做并发控制,并且需要长期有效,但是秒杀优惠卷的额度是有限制的,并且限时开放,需要处理并发。
- 数据库范式:如果将秒杀优惠卷设计到一张表格,违反了数据库设计的第三范式,比如stock可能会长时间为null或者0,而分离表可以避免这种问题,并且还能增加库存索引,提高查询效率。
- 扩展性:易于添加新的优惠卷类型,增加新的字段。
- 事务边界清晰:这样事务要是进行回滚,回滚的数据也会更加清晰,涉及的边界更加精确。
3.业务流程步骤
概述:管理员创建秒杀券 → 用户发起秒杀 → Lua脚本验证 → 异步订单处理 → 数据库持久化
阶段一:秒杀券创建
- 管理员创建秒杀券 (VoucherController.addSeckillVoucher)
- 保存优惠券基本信息到 tb_voucher
- 保存秒杀信息到 tb_seckill_voucher
- 关键:将库存同步到Redis (seckill:stock:{voucherId})
阶段二:用户秒杀购买
- 用户发起秒杀请求 (VoucherOrderController.seckillVoucher)
- 调用 VoucherOrderServiceImpl.seckillVoucher 方法
- Lua脚本执行 (seckill.lua)
▼lua复制代码-- 检查库存是否充足 -- 检查用户是否已购买过 -- 扣减Redis库存 -- 记录用户购买信息到Redis -- 将订单信息加入消息队列
- 异步订单处理
- 使用Redis Stream消息队列处理订单
- VoucherOrderHandler 持续监听消息队列
- 异步创建数据库订单记录
4. 🛡️ 关键技术保障
高并发处理
- ✅ Redis + Lua脚本: 保证库存扣减的原子性
- ✅ 分布式锁: Redisson实现,防止重复下单
- ✅ 消息队列: Redis Stream异步处理订单
防超卖机制
- ✅ Re****dis预扣减: 快速响应,减少数据库压力
- ✅ 数据库乐观锁: stock = stock - 1 WHERE stock > 0
- ✅ 一人一单: 用户ID + 优惠券ID唯一性检查
性能优化
- ✅ 缓存热点数据: 秒杀信息存储在Redis
- ✅ 异步处理: 订单创建异步化,提高响应速度
- ✅ 单线程处理: 避免数据库并发冲突
5.📊 数据流转
▼lua复制代码-- 1. 检查Redis库存是否充足 -- 2. 检查用户是否已购买过 (防重复下单) -- 3. 扣减Redis库存 -- 4. 记录用户购买信息到Redis -- 5. 将订单信息加入Redis Stream消息队列 -- 6. 返回结果: 0=成功, 1=库存不足, 2=重复下单
6.乐观锁和悲观锁是什么策略思想?用来干什么?
| 名称 | 具体 | 解决问题 | 缺陷 |
|---|---|---|---|
| 乐观锁 | 认为别人不会同时修改数据,于是不上锁,但如果发现别人修改数据,就会放弃执行操作(检查数据版本号) | 高并发读,低并发写场景(如评论更新) | 失败率高(经常返回 nil),需重试逻辑 |
| 悲观锁 | 认为别人会同时修改数据,于是在执行数据的时候就加上锁,执行完之后,才会释放锁 | 保证强一致性(如秒杀扣库存) | 性能差(等待锁)、可能死锁 |
7.使用 Redis + Lua脚本实现对用户秒杀资格的预检,同时用乐观锁解决秒杀产生的超卖问题
Redis执行一条命令的时候是具备原子性的,因为Redis执行命令是单线程的,不存在线程安全的问题,但当执行多条Redis命令时,就不是的了,我们把多条Redis指令放到Lua脚本中,Redis会把Lua脚本作为一个整体执行,保证了原子性,无需加锁,天然互斥。
流程是(就是seckillVoucher方法):
先获取用户ID和订单ID,再将优惠券ID,用户ID和订单ID传给Lua脚本执行,进行资格预检,根据 Lua 脚本的返回值(0: 成功,1: 库存不足,2: 重复下单)返回对应的错误信息。将订单信息异步发送到 RabbitMQ 队列,由消费者处理后续逻辑,最后返回订单ID给前端。
8.如何解决超卖问题?
使用 Redis 原子操作或 Lua 脚本,而不是传统锁机制。
为什么不使用乐观锁,高并发下失败率极高,重试开销大,悲观锁性能差,并且容易死锁。而原子命令性能极致,实现简单。Lua脚本还可以复杂的原子操作。
分布式锁
1.redis分布式锁是什么?是如何实现一人一单的?
分布式锁:满足分布式系统或者集群模式下多线程可见并且可斥,将锁监视器提取出来
判断用户订单是否存在,获取订单id,为了防止用户故意开多线程抢优惠劵,使用悲观锁解决该问题,把一人一单的逻辑加到一个方法里面,上面再加事务标志
2.分布式锁的实现和特性?
特性:
- 可见性(多个线程能看到)
- 互斥性
- 高可用
- 高性能(拿锁快)
- 安全性(死锁)
实现:
| 名称 | MySQL | Redis | Zookeeper |
|---|---|---|---|
| 互斥 | 利用MySQL本身的互斥锁机制 | 利用setnx类似的互斥命令 | 利用节点的唯一性和有序性实现互斥 |
| 高可用 | 好 | 好 | 好 |
| 高性能 | 一般 | 好 | 一般 |
| 安全性 | 断开连接,自动释放锁 | 利用锁超时时间,到期释放 | 临时节点,断开连接自动释放 |
3.如何用一条语句来完成加锁操作?
setnx
4.redis分布式锁的实现思路
获取锁:setnx命令-互斥或非阻塞
释放锁:del key命令-手动释放(del key)或超时释放(expire lock 10)
思路总结:利用set nx ex获取锁(set nx充满互斥性)(利用set ex保证故障时锁依然能被释放【为了避免死锁 提高安全性】)
5.redis分布式锁误删情况
现象:在老业务阻塞的情况下,线程超时被释放了,这个时候有新业务创建了,并创建了相同的锁,这个时候老业务不阻塞了,完成业务,就会把这个相同的锁删除。
为了解决这个问题,在获取锁的时候存入线程标识UUID,在释放锁时判断是否自己线程的锁
6.分布式锁的原子性问题
在判断完锁是否属于自己后,准备释放锁,但是这个时候线程被阻塞了(jvm里面有垃圾回收机制)
7.自研Redis分布式锁和Redisson有何区别?
- 可重入:自研默认不可重入;Redisson 内置计数可重入。(可重入允许同一个线程获取同一个锁)
- 续约:自研需自行续期;Redisson 看门狗自动续约。
- 解锁安全:自研需 Lua 校验 UUID 再删;Redisson内置原子校验。
- 等待与公平:自研多为自旋/睡眠;Redisson支持阻塞等待/公平锁。
- 锁型丰富度:自研多为互斥锁;Redisson有读写锁、联锁、红锁等。
- 容错与可观测:自研需自保;Redisson内置重试、日志、指标。
- 性能:自研极简路径最轻;Redisson略有额外开销但换稳定性与效率。
- 结合项目:入口 Lua 判重+预扣;落库用 Redisson 做“一人一单”。
8.Redisson可重入式锁原理
解决思路:redis里面记录的时候在value的位置存储map结构(key记录线程名称、value记录重复次数),当同一个线程想要再次获取锁的时候,只需要将value增加1,当value为0的时候,锁才会被真正释放。
什么时候会碰到需要可重入锁的场景呢?比如递归问题、模板方法模式(一个加锁的模板方法可能会调用一个由子类实现的抽象方法)
9.Redisson的WatchDog机制
这是一个非常重要的机制,用于避免死锁和自动续期。
- 问题:如果持有锁的线程业务执行时间超过了锁默认的 TTL(生存时间),那么 Redis 会因超时自动删除这个锁,导致锁失效,其他线程就能获取到锁,造成数据不一致。
- 解决方案:看门狗。
- 如果你没有显式地指定锁的超时时间(
leaseTime),Redisson 会启动一个看门狗守护线程。 - 默认情况下,锁的 TTL 是 30 秒。
- 看门狗会每隔 10 秒(TTL时间的 1/3)检查一下客户端是否还持有这个锁。
- 如果客户端仍然持有锁(即业务还没执行完),看门狗就会重置锁的 TTL,将其重新延长到 30 秒。
- 只要业务没执行完,并且客户端没有崩溃,这个锁就会一直被持有,直到你手动调用
unlock()。
注意:如果你在加锁时显式指定了超时时间(例如 lock.lock(10, TimeUnit.SECONDS)),看门狗机制就会失效,Redisson 不会为这个锁续期。到期后锁会自动释放,这可能存在业务未执行完锁就释放的风险。
10.Redisson的锁重试机制
- 核心流程:加锁失败后发生了什么?
当你调用 lock.lock() 时,背后的重试流程如下:
- 首次尝试获取锁:
Redisson 客户端会首先执行之前提到的 Lua 脚本,尝试原子性地在 Redis 中创建锁。
- 成功:直接返回,流程结束。
- 失败:Lua 脚本会返回当前锁剩余的存活时间(
ttl,单位毫秒)。
- 订阅锁释放频道:
由于第一次尝试失败,客户端会订阅一个与这个锁名称相关的 Redis 频道(Channel),例如redisson_lock__channel:{myLock}。这个频道专门用于广播该锁的释放消息。 - 进入重试循环:
客户端进入一个循环,在这个循环中它会:
-
等待通知:利用 Java 的
Semaphore或其他同步工具,在本地阻塞等待,直到收到来自步骤 2 中频道的通知消息,或者等待超时。 -
再次尝试获取锁:
-
如果收到了锁释放的通知,它会立刻被唤醒,然后再次执行 Lua 脚本尝试获取锁。
-
如果等待超时了(这个超时时间通常基于第一次尝试返回的
ttl),它也会被唤醒并再次尝试获取锁。(这是一种兜底策略,防止因网络问题导致的通知丢失)。 -
检查尝试结果:
-
尝试成功:跳出循环,获取锁成功。
-
尝试再次失败:收到新的
ttl,然后重复步骤 3,继续等待和重试。
- 取消订阅:
一旦成功获取到锁(或最终超时),客户端会取消对那个锁释放频道的订阅,以避免不必要的资源浪费。
这个过程的精髓在于:通过 Redis 的发布/订阅功能,将主动的、频繁的轮询(Polling)转变为被动的、高效的事件驱动通知。客户端只有在锁很可能被释放时才会被唤醒并尝试,极大地减少了网络通信和对 Redis 的压力。
- 关键配置参数
重试行为可以通过参数进行精细控制,主要在两个方法中体现:
A. 无参 lock() 方法
▼java复制代码RLock lock = redisson.getLock("myLock"); lock.lock(); // 无限重试
- 行为:默认会一直重试,直到成功获取锁。
- 原理:内部是一个
while(true)循环,配合发布/订阅机制不断尝试。必须确保最后有unlock(),否则其他线程会永远等待。
B. 带超时参数的 tryLock() 方法
▼java复制代码boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS); // 等待时间(重试时间):10秒 // 锁持有时间:30秒(如果成功获取)
waitTime(第一个参数):最大重试等待时间。在这段时间内,客户端会不断地执行上述“等待-重试”流程。如果在waitTime内成功获取锁,则返回true;如果超过waitTime仍未成功,则停止重试并返回false。leaseTime(第二个参数):锁的自动释放时间。如果成功获取锁,锁将在leaseTime后自动过期释放。如果设置了**leaseTime**,看门狗续期机制将会失效。
-
重试机制的优势与设计考量
-
减少网络和计算开销:
与简单的“循环-轮询”方案相比,Redisson 的“订阅-通知”机制避免了无用的空转和大量的 Redis 命令请求,非常高效。 -
高实时性:
一旦锁被释放,消息会通过 Redis 频道立即广播给所有订阅的客户端,它们可以近乎实时地发起争抢,减少了获取锁的延迟。 -
避免活锁:
在极高并发下,如果所有客户端同时被通知并同时发起请求,可能会造成瞬间拥堵。Redisson 在内部做了一些优化(例如在重试前加入非常短暂的随机延迟),来错开大量客户端的请求时间,提高单个客户端的成功率。 -
可靠性:
即使发布/订阅消息由于网络问题丢失,客户端也有基于ttl的等待超时作为备份机制,确保不会永久等待下去。
11.Redisson锁的MutiLock原理
核心答案:
MultiLock(联锁) 是一种将多个独立锁组合成一个逻辑锁的机制,遵循 “全部或nothing” 原则。
核心原理:
- 原子性加锁:客户端必须按顺序成功获取所有 constituent 子锁才算加锁成功。只要有一个子锁获取失败,立即释放所有已获得的子锁并进行重试,确保不会部分持锁。
- 统一租约:所有子锁都拥有相同的超时时间,并由一个看门狗线程统一续期,保证生命周期一致。
- 原子性释放:解锁时,会依次释放所有子锁。
与 RedLock 的关键区别:
- 目的不同:MultiLock 是为了同时锁定多个不同资源(如订单、库存);而 RedLock 是为了提高一把锁的可用性,在多个 Redis 节点上创建同一把锁的副本。
- 锁定对象:MultiLock 锁多个不同 Key;RedLock 在多个实例上锁同一个 Key。
一句话总结: MultiLock 通过客户端协调的“全部成功+失败回滚”机制,实现对多个资源的原子性加锁操作。
主要使用场景:
- 分布式事务(最经典)
- 场景:在电商下单流程中,创建订单、扣减库存、扣减用户账户余额这三个操作必须作为一个原子单元执行。
- 用法:使用 MultiLock 同时锁定
订单ID、商品SKU、用户ID这三把锁。只有全部锁定成功,才执行后续业务逻辑,从而防止其他事务干扰,保证数据一致性。
- 批量数据处理
- 场景:需要批量更新一组用户的状态,要求要么全部更新成功,要么全部不更新,中间状态不可见。
- 用法:获取这批所有
用户ID对应的锁组成的 MultiLock。锁定成功后进行批量更新,避免在更新过程中,单个用户被其他操作修改,导致整体数据不一致。
- 全局唯一性检查与操作
- 场景:用户注册时,需要同时检查“用户名”和“手机号”是否都被占用。要求检查的那一刻两者都未被注册,才能创建新账号,防止在两次检查的间隙被其他请求插入。
- 用法:使用 MultiLock 锁定
用户名和手机号对应的资源,然后执行查询和插入操作。
12.为什么下单流程不用Lua脚本而用联锁?
“这是一个关于数据边界和工具职责的问题。
- Lua脚本的原子性能力仅限于Redis内部的数据操作。而在经典架构下,电商的核心数据如订单、库存、余额通常保存在MySQL等传统数据库中,Redis更多用作缓存。Lua脚本无法跨出Redis去操作MySQL中的数据。
- Redisson的联锁 (MultiLock) 是一种应用层的协调机制。它的思路是:‘锁住资源’而非‘操作数据’。它通过同时获取代表这些不同资源的分布式锁,在逻辑上划定一个临界区。只有拿到所有锁的线程,才有资格去依次执行操作MySQL、调用服务等复杂业务逻辑。
Redis消息队列
为什么使用消息队列?
当前整个秒杀业务流程是串行化的,查询优惠卷、查询订单、删减库存、创建订单都是走的数据库,mysql本身并发能力就较弱,还加上了分布式锁,整个业务耗时流程长,并发能力弱。


三种结构好的,这是一份为你准备的、精炼的 Redis 消息队列面试总结。
实现方案总结
Redis 提供了三种主流的消息队列实现方式,各有其适用场景。
1. 基于 List 结构
-
模型:使用
LPUSH/RPOP(或BRPOP) 模拟单向队列。 -
优点:
-
消息可持久化,重启不丢失。
-
保证消息有序性。
-
缺点:
-
不支持多消费者:一个消息只能被一个消费者消费。
-
需要自己实现消息确认机制:消费者处理消息时如果崩溃,消息会永久丢失(因为已经被
RPOP移出队列)。 -
适用场景:简单的单向任务队列,对消息丢失不敏感的场景。
2. 基于 Pub/Sub (发布订阅)
-
模型:生产者向频道 (
channel) 发布消息,所有订阅该频道的消费者都能收到。 -
优点:
-
支持多生产、多消费的广播模式。
-
实时性高。
-
缺点:
-
数据不持久化:消息是“fire and forget”,如果消费者当时下线,消息将彻底丢失。
-
无法堆积消息:没有缓冲区,超过处理能力的消息会直接丢弃。
-
适用场景:实时消息通知、服务状态广播等对消息可靠性要求不高的场景。
3. 基于 Stream (主流推荐)
-
模型:是 Redis 5.0 后专门设计的更强大的消息队列数据结构。支持消费者组(Consumer Group)。
-
核心优势:
-
消息持久化:所有消息都会被记录。
-
支持多消费者模式:通过消费者组,可以让多个消费者共同竞争消费一个队列,负载均衡。
-
提供消息确认机制:消费者处理完消息后必须发送
ACK,否则消息会重新被投递给其他消费者,防止消息丢失。 -
支持消息回溯:可以重新读取历史消息。
-
缺点:
-
功能复杂:学习和使用的门槛比前两者高。
-
是 AP 系统:基于 Redis 主从复制,在极端故障情况下可能丢失极小部分数据(异步复制问题)。
-
适用场景:绝大多数需要可靠消息队列的场景,如秒杀订单异步处理、实时流处理等。它是 Redis 中最接近专业消息队列(如 Kafka, RocketMQ)的实现。
面试回答指南
“Redis 实现消息队列主要有三种方式:
- List 最简单,能持久化但无法可靠地实现多消费者。
- Pub/Sub 是发布订阅模型,用于广播,但不持久化,可靠性差。
- Stream 是最完善的方案,它引入了消费者组和消息确认机制,解决了消息丢失和多消费者负载均衡的问题,是生产环境中构建可靠异步任务系统的首选。我们在秒杀项目中就是用它来异步下单,保证最终一致性的。”
点赞功能
- 用户可以对博客进行点赞和取消点赞
- 显示每个博客的点赞总数
- 显示点赞排行榜(前5名点赞用户)
- 按点赞数对博客进行排序(热门博客)
测试
一,怎么使用使用Postman进行接口测试?
1,安装Postman
- 创建请求: 打开Postman,点击"New"按钮创建一个新的请求。在弹出的窗口中,选择请求的类型(GET、POST等),填入请求的URL,选择请求的Header、Body等信息。
- 设置请求Header: 如果接口需要传递Header信息,可以在Postman中设置。点击请求的Headers选项卡,添加需要的Header信息,比如Authorization等。
- 设置请求Body: 对于POST请求或者其他需要传递Body的请求,可以在Postman中设置请求的Body。可以选择不同的Body格式,比如form-data、raw、x-www-form-urlencoded等,并填入相应的参数。
- 发送请求: 填好请求信息后,点击Send按钮发送请求。Postman会显示请求的响应信息,包括状态码、响应体等。
- 查看响应: 在发送请求后,可以查看Postman显示的响应信息,包括响应的状态码、响应体等。可以根据需要进行断言、验证响应的正确性。
- 保存请求: 如果需要保存请求,可以点击Save按钮保存请求信息,方便以后再次使用。
通过这些步骤,你可以使用Postman进行接口测试,验证接口的正确性和稳定性。
JMeter:秒杀系统如何做接口压力测试?
确定性能测试目标和指标:
在进行性能测试之前,我们需要先确定测试的目标和指标。在秒杀系统中,我们主要关注以下指标:
系统的吞吐量:即在一定时间内能够处理的请求数量;
系统的响应时间:即从发起请求到接收响应的时间;
系统的并发数:即同时处理的请求数量;
系统的错误率:即请求失败的比例。
通过确定这些指标,我们可以更好地了解系统的性能瓶颈,并进行优化
1,创建测试计划:
首先,我们需要创建一个测试计划。在 jmeter 中,测试计划是一个顶层元素,包含了所有的测试元素。
在测试计划中,我们需要添加线程组和 HTTP 请求。
线程组是一组并发请求的集合,它定义了一组并发用户,并指定了每个用户的行为。在秒杀系统中,我们可以将线程组的数量设置为需要测试的并发数。
HTTP 请求是一个发送 HTTP 请求的元素,它可以模拟客户端向服务器发送请求的过程。我们需要使用 HTTP 请求来模拟秒杀系统的请求。
在添加 HTTP 请求时,我们需要填写请求的 URL 和请求参数。在秒杀系统中,我们需要将登录参数化,以便模拟多个用户同时登录的场景。同时,我们需要使用循环控制器来模拟循环请求接口并发 100
2,设置测试参数和参数化
在 jmeter 中,我们可以使用 CSV 数据文件来设置测试参数和参数化。CSV 文件是一个以逗号分隔的文本文件,可以包含多个行和列,每个单元格都可以包含一个值。
在 CSV 文件中,我们可以存储多个用户名和密码,然后在测试中使用变量引用这些值。这样就可以模拟多个用户同时登录的场景。
3,运行测试并分析结果:
在设置完测试参数和参数化之后,我们可以运行测试并分析结果。在测试运行期间,我们可以使用 jmeter 的图表和报告功能来监测系统的性能指标,并查找性能瓶颈。
在测试结束后,我们需要对测试结果进行分析和总结。通过对测试结果的分析,我们可以找到系统的性能瓶颈。

