黑马点评面试题整理

黑马点评面试题整理

总体

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. 主动更新策略要考虑什么?

  1. 更新数据库的时候要更新缓存还是删除缓存?
  • 更新缓存:每次更新数据库都更新缓存,这样无效写操作比较多
  • 删除缓存:更新数据库让缓存失效,查询时再更新缓存
  1. 如何保证缓存与数据库的操作同时成功或失败?
  • 单体系统:将缓存与数据库操作放在一个事务
  • 分布式系统:利用TCC等分布式事务方案
  1. 先操作缓存还是数据库?

一般先操作数据库,因为Redis读写更快,放在后面操作,延迟的概率要小一点

  1. 缓存更新策略的最佳实践方案:
  • 低一致性需求:使用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)。在实际项目中,通常需要根据业务场景(能否接受短暂的数据不一致?)、数据特性(是热点数据还是冷数据?)、系统规模来进行组合使用

例如:

  1. 对于缓存穿透:首先做好参数校验,这是最重要且性价比最高的。然后对无法校验的请求,使用布隆过滤器(数据量大时)或缓存空对象(数据量可控时)兜底。
  2. 对于缓存雪崩设置随机过期时间是基础操作。同时对极其关键的数据采用**“永不过期”+后台定时更新**的策略。
  3. 对于缓存击穿互斥锁方案更保证数据强一致,但性能有损耗,适合金融、库存等场景。逻辑过期方案性能更好,但只能保证最终一致,适合新闻、商品介绍等对延迟敏感但能容忍短暂不一致的场景。

互斥锁的逻辑处理图

逻辑过期的逻辑处理图

5. 布隆过滤器解析

定义:超高效的、会“误报”的“哨兵”

特点:

  • 告诉你一个东西肯定不会,可能会
  • 非常节省空间,远超传统的哈希表(如HashSet)
  • 查询速度极快(O(k),k为哈希函数个数)

核心组成部分:

  • 一个很长的二进制向量(位数组)
  • 一组哈系函数

工作原理:

  1. 写入(Add) - 如何标记一个数据存在?

假设我们要把商品ID 101 加入布隆过滤器。

  • 步骤一:用准备好的多个哈希函数分别计算 101 的哈希值。
  • 步骤二:每个哈希值都对位数组的长度取模,得到在位数组上对应的位置(格子)。
  • 步骤三:将这些位置上的格子从 0 设置为 1

  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. 秒杀券与普通券区别:
  • 普通券: 直接购买,无时间限制
  • 秒杀券: 限时抢购,库存有限,一人一单
  1. 订****单状态管理:
  • 1: 未支付 → 2: 已支付 → 3: 已核销
  • 支持取消(4)、退款(5-6)等状态
  1. 时间控制:
  • begin_time: 秒杀开始时间
  • end_time: 秒杀结束时间
  • 系统会检查时间有效性
  1. 业务逻辑的本质差别:普通优惠卷无限量供应,不需要做并发控制,并且需要长期有效,但是秒杀优惠卷的额度是有限制的,并且限时开放,需要处理并发。
  2. 数据库范式:如果将秒杀优惠卷设计到一张表格,违反了数据库设计的第三范式,比如stock可能会长时间为null或者0,而分离表可以避免这种问题,并且还能增加库存索引,提高查询效率。
  3. 扩展性:易于添加新的优惠卷类型,增加新的字段。
  4. 事务边界清晰:这样事务要是进行回滚,回滚的数据也会更加清晰,涉及的边界更加精确。

3.业务流程步骤

概述:管理员创建秒杀券 → 用户发起秒杀 → Lua脚本验证 → 异步订单处理 → 数据库持久化

阶段一:秒杀券创建

  1. 管理员创建秒杀券 (VoucherController.addSeckillVoucher)
  • 保存优惠券基本信息到 tb_voucher
  • 保存秒杀信息到 tb_seckill_voucher
  • 关键:将库存同步到Redis (seckill:stock:{voucherId})

阶段二:用户秒杀购买

  1. 用户发起秒杀请求 (VoucherOrderController.seckillVoucher)
  • 调用 VoucherOrderServiceImpl.seckillVoucher 方法
  1. Lua脚本执行 (seckill.lua)
lua
复制代码
-- 检查库存是否充足 -- 检查用户是否已购买过 -- 扣减Redis库存 -- 记录用户购买信息到Redis -- 将订单信息加入消息队列
  1. 异步订单处理
  • 使用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.分布式锁的实现和特性?

特性:

  • 可见性(多个线程能看到)
  • 互斥性
  • 高可用
  • 高性能(拿锁快)
  • 安全性(死锁)

实现:

名称MySQLRedisZookeeper
互斥利用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 会因超时自动删除这个锁,导致锁失效,其他线程就能获取到锁,造成数据不一致。
  • 解决方案:看门狗。
  1. 如果你没有显式地指定锁的超时时间(leaseTime),Redisson 会启动一个看门狗守护线程。
  2. 默认情况下,锁的 TTL 是 30 秒。
  3. 看门狗会每隔 10 秒(TTL时间的 1/3)检查一下客户端是否还持有这个锁。
  4. 如果客户端仍然持有锁(即业务还没执行完),看门狗就会重置锁的 TTL,将其重新延长到 30 秒
  5. 只要业务没执行完,并且客户端没有崩溃,这个锁就会一直被持有,直到你手动调用 unlock()

注意:如果你在加锁时显式指定了超时时间(例如 lock.lock(10, TimeUnit.SECONDS)),看门狗机制就会失效,Redisson 不会为这个锁续期。到期后锁会自动释放,这可能存在业务未执行完锁就释放的风险。

10.Redisson的锁重试机制

  1. 核心流程:加锁失败后发生了什么?

当你调用 lock.lock() 时,背后的重试流程如下:

  1. 首次尝试获取锁
    Redisson 客户端会首先执行之前提到的 Lua 脚本,尝试原子性地在 Redis 中创建锁。
  • 成功:直接返回,流程结束。
  • 失败:Lua 脚本会返回当前锁剩余的存活时间(ttl,单位毫秒)。
  1. 订阅锁释放频道
    由于第一次尝试失败,客户端会订阅一个与这个锁名称相关的 Redis 频道(Channel),例如 redisson_lock__channel:{myLock}。这个频道专门用于广播该锁的释放消息。
  2. 进入重试循环
    客户端进入一个循环,在这个循环中它会:
  • 等待通知:利用 Java 的 Semaphore 或其他同步工具,在本地阻塞等待,直到收到来自步骤 2 中频道的通知消息,或者等待超时。

  • 再次尝试获取锁

  • 如果收到了锁释放的通知,它会立刻被唤醒,然后再次执行 Lua 脚本尝试获取锁。

  • 如果等待超时了(这个超时时间通常基于第一次尝试返回的 ttl),它也会被唤醒并再次尝试获取锁。(这是一种兜底策略,防止因网络问题导致的通知丢失)。

  • 检查尝试结果

  • 尝试成功:跳出循环,获取锁成功。

  • 尝试再次失败:收到新的 ttl,然后重复步骤 3,继续等待和重试。

  1. 取消订阅
    一旦成功获取到锁(或最终超时),客户端会取消对那个锁释放频道的订阅,以避免不必要的资源浪费。

这个过程的精髓在于:通过 Redis 的发布/订阅功能,将主动的、频繁的轮询(Polling)转变为被动的、高效的事件驱动通知。客户端只有在锁很可能被释放时才会被唤醒并尝试,极大地减少了网络通信和对 Redis 的压力。

  1. 关键配置参数

重试行为可以通过参数进行精细控制,主要在两个方法中体现:

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**,看门狗续期机制将会失效
  1. 重试机制的优势与设计考量

  2. 减少网络和计算开销
    与简单的“循环-轮询”方案相比,Redisson 的“订阅-通知”机制避免了无用的空转和大量的 Redis 命令请求,非常高效。

  3. 高实时性
    一旦锁被释放,消息会通过 Redis 频道立即广播给所有订阅的客户端,它们可以近乎实时地发起争抢,减少了获取锁的延迟。

  4. 避免活锁
    在极高并发下,如果所有客户端同时被通知并同时发起请求,可能会造成瞬间拥堵。Redisson 在内部做了一些优化(例如在重试前加入非常短暂的随机延迟),来错开大量客户端的请求时间,提高单个客户端的成功率。

  5. 可靠性
    即使发布/订阅消息由于网络问题丢失,客户端也有基于 ttl 的等待超时作为备份机制,确保不会永久等待下去。

11.Redisson锁的MutiLock原理

核心答案:

MultiLock(联锁) 是一种将多个独立锁组合成一个逻辑锁的机制,遵循 “全部或nothing” 原则。

核心原理:

  1. 原子性加锁:客户端必须按顺序成功获取所有 constituent 子锁才算加锁成功。只要有一个子锁获取失败,立即释放所有已获得的子锁并进行重试,确保不会部分持锁。
  2. 统一租约:所有子锁都拥有相同的超时时间,并由一个看门狗线程统一续期,保证生命周期一致。
  3. 原子性释放:解锁时,会依次释放所有子锁。

与 RedLock 的关键区别:

  • 目的不同:MultiLock 是为了同时锁定多个不同资源(如订单、库存);而 RedLock 是为了提高一把锁的可用性,在多个 Redis 节点上创建同一把锁的副本。
  • 锁定对象:MultiLock 锁多个不同 Key;RedLock 在多个实例上锁同一个 Key。

一句话总结: MultiLock 通过客户端协调的“全部成功+失败回滚”机制,实现对多个资源的原子性加锁操作。

主要使用场景:

  1. 分布式事务(最经典)
  • 场景:在电商下单流程中,创建订单、扣减库存、扣减用户账户余额这三个操作必须作为一个原子单元执行。
  • 用法:使用 MultiLock 同时锁定 订单ID商品SKU用户ID 这三把锁。只有全部锁定成功,才执行后续业务逻辑,从而防止其他事务干扰,保证数据一致性。
  1. 批量数据处理
  • 场景:需要批量更新一组用户的状态,要求要么全部更新成功,要么全部不更新,中间状态不可见。
  • 用法:获取这批所有 用户ID 对应的锁组成的 MultiLock。锁定成功后进行批量更新,避免在更新过程中,单个用户被其他操作修改,导致整体数据不一致。
  1. 全局唯一性检查与操作
  • 场景:用户注册时,需要同时检查“用户名”和“手机号”是否都被占用。要求检查的那一刻两者都未被注册,才能创建新账号,防止在两次检查的间隙被其他请求插入。
  • 用法:使用 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 实现消息队列主要有三种方式:

  1. List 最简单,能持久化但无法可靠地实现多消费者。
  2. Pub/Sub 是发布订阅模型,用于广播,但不持久化,可靠性差。
  3. Stream 是最完善的方案,它引入了消费者组和消息确认机制,解决了消息丢失和多消费者负载均衡的问题,是生产环境中构建可靠异步任务系统的首选。我们在秒杀项目中就是用它来异步下单,保证最终一致性的。”

点赞功能

  • 用户可以对博客进行点赞和取消点赞
  • 显示每个博客的点赞总数
  • 显示点赞排行榜(前5名点赞用户)
  • 按点赞数对博客进行排序(热门博客)

测试

一,怎么使用使用Postman进行接口测试?
1,安装Postman

  1. 创建请求: 打开Postman,点击"New"按钮创建一个新的请求。在弹出的窗口中,选择请求的类型(GET、POST等),填入请求的URL,选择请求的Header、Body等信息。
  2. 设置请求Header: 如果接口需要传递Header信息,可以在Postman中设置。点击请求的Headers选项卡,添加需要的Header信息,比如Authorization等。
  3. 设置请求Body: 对于POST请求或者其他需要传递Body的请求,可以在Postman中设置请求的Body。可以选择不同的Body格式,比如form-data、raw、x-www-form-urlencoded等,并填入相应的参数。
  4. 发送请求: 填好请求信息后,点击Send按钮发送请求。Postman会显示请求的响应信息,包括状态码、响应体等。
  5. 查看响应: 在发送请求后,可以查看Postman显示的响应信息,包括响应的状态码、响应体等。可以根据需要进行断言、验证响应的正确性。
  6. 保存请求: 如果需要保存请求,可以点击Save按钮保存请求信息,方便以后再次使用。

通过这些步骤,你可以使用Postman进行接口测试,验证接口的正确性和稳定性。

JMeter:秒杀系统如何做接口压力测试?

确定性能测试目标和指标:

在进行性能测试之前,我们需要先确定测试的目标和指标。在秒杀系统中,我们主要关注以下指标:

系统的吞吐量:即在一定时间内能够处理的请求数量;

系统的响应时间:即从发起请求到接收响应的时间;

系统的并发数:即同时处理的请求数量;

系统的错误率:即请求失败的比例。

通过确定这些指标,我们可以更好地了解系统的性能瓶颈,并进行优化

1,创建测试计划:

首先,我们需要创建一个测试计划。在 jmeter 中,测试计划是一个顶层元素,包含了所有的测试元素。

在测试计划中,我们需要添加线程组和 HTTP 请求。

线程组是一组并发请求的集合,它定义了一组并发用户,并指定了每个用户的行为。在秒杀系统中,我们可以将线程组的数量设置为需要测试的并发数。

HTTP 请求是一个发送 HTTP 请求的元素,它可以模拟客户端向服务器发送请求的过程。我们需要使用 HTTP 请求来模拟秒杀系统的请求。

在添加 HTTP 请求时,我们需要填写请求的 URL 和请求参数。在秒杀系统中,我们需要将登录参数化,以便模拟多个用户同时登录的场景。同时,我们需要使用循环控制器来模拟循环请求接口并发 100

2,设置测试参数和参数化

在 jmeter 中,我们可以使用 CSV 数据文件来设置测试参数和参数化。CSV 文件是一个以逗号分隔的文本文件,可以包含多个行和列,每个单元格都可以包含一个值。

在 CSV 文件中,我们可以存储多个用户名和密码,然后在测试中使用变量引用这些值。这样就可以模拟多个用户同时登录的场景。

3,运行测试并分析结果:

在设置完测试参数和参数化之后,我们可以运行测试并分析结果。在测试运行期间,我们可以使用 jmeter 的图表和报告功能来监测系统的性能指标,并查找性能瓶颈。

在测试结束后,我们需要对测试结果进行分析和总结。通过对测试结果的分析,我们可以找到系统的性能瓶颈。

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
面向对象的王者很迟缓
下载 APP