编程导航黑马点评话题讨论

黑马点评

11 参与
分享

快来分享你的内容吧~

点击登录,快来和大家讨论吧~
表情
图片
话题
打卡
综合
交流
文章
问答

我想问一下学过黑马点评的redis项目的同学,我现在遇到一个问题,在用乐观锁的CAS方法解决超卖问题 并且我也是按照视频去判断库存是否大于0,当我用jmeter测试时,发现异常率为100%,并且voucher_order却有2条数据

黑马点评面试题整理

黑马点评面试题整理 ========= 总体 -- ### 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. 更新数据库的时候要更新缓存还是删除缓存? * 更新缓存:每次更新数据库都更新缓存,这样无效写操作比较多 * 删除缓存:更新数据库让缓存失效,查询时再更新缓存 2. 如何保证缓存与数据库的操作同时成功或失败? * 单体系统:将缓存与数据库操作放在一个事务 * 分布式系统:利用TCC等分布式事务方案 3. 先操作缓存还是数据库? 一般先操作数据库,因为Redis读写更快,放在后面操作,延迟的概率要小一点 4. 缓存更新策略的最佳实践方案: * 低一致性需求:使用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. 对于**缓存击穿**:**互斥锁**方案更保证数据强一致,但性能有损耗,适合金融、库存等场景。**逻辑过期**方案性能更好,但只能保证最终一致,适合新闻、商品介绍等对延迟敏感但能容忍短暂不一致的场景。 **互斥锁的逻辑处理图** ![](https://pic.code-nav.cn/post_picture/1828819762856710146/1TMbrkeCpsyV1pkd.webp) **逻辑过期的逻辑处理图** ![](https://pic.code-nav.cn/post_picture/1828819762856710146/Kv5QxlaqQZL9rWNl.webp) ### 5. 布隆过滤器解析 **定义:超高效的、会“误报”的“哨兵”** 特点: * 告诉你一个东西肯定不会,可能会 * 非常节省空间,远超传统的哈希表(如HashSet) * 查询速度极快(O(k),k为哈希函数个数) 核心组成部分: * 一个很长的二进制向量(位数组) * 一组哈系函数 工作原理: 1. 写入(Add) - 如何标记一个数据存在? 假设我们要把商品ID `101` 加入布隆过滤器。 * **步骤一**:用准备好的多个哈希函数分别计算 `101` 的哈希值。 * **步骤二**:每个哈希值都对位数组的长度取模,得到在位数组上对应的位置(格子)。 * **步骤三**:将这些位置上的格子从 `0` 设置为 `1`。 ![](https://pic.code-nav.cn/post_picture/1828819762856710146/voUylHm71kDopINa.webp) 2. 查询(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. **订****单状态管理**: * 1: 未支付 → 2: 已支付 → 3: 已核销 * 支持取消(4)、退款(5-6)等状态 3. **时间控制**: * 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 方法 2. **Lua脚本执行** (seckill.lua) ```lua -- 检查库存是否充足 -- 检查用户是否已购买过 -- 扣减Redis库存 -- 记录用户购买信息到Redis -- 将订单信息加入消息队列 ``` 3. **异步订单处理** * 使用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 会因超时自动删除这个锁,导致锁失效,其他线程就能获取到锁,造成数据不一致。 * **解决方案**:看门狗。 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`,单位毫秒)。 2. **订阅锁释放频道**: 由于第一次尝试失败,客户端会**订阅**一个与这个锁名称相关的 Redis 频道(Channel),例如 `redisson_lock__channel:{myLock}`。这个频道专门用于广播该锁的释放消息。 3. **进入重试循环**: 客户端进入一个循环,在这个循环中它会: * **等待通知**:利用 Java 的 `Semaphore` 或其他同步工具,在本地阻塞等待,直到收到来自步骤 2 中频道的通知消息,或者等待超时。 * **再次尝试获取锁**: * 如果**收到了锁释放的通知**,它会立刻被唤醒,然后再次执行 Lua 脚本尝试获取锁。 * 如果**等待超时了**(这个超时时间通常基于第一次尝试返回的 `ttl`),它也会被唤醒并再次尝试获取锁。(这是一种兜底策略,防止因网络问题导致的通知丢失)。 * **检查尝试结果**: * **尝试成功**:跳出循环,获取锁成功。 * **尝试再次失败**:收到新的 `ttl`,然后重复步骤 3,继续等待和重试。 4. **取消订阅**: 一旦成功获取到锁(或最终超时),客户端会取消对那个锁释放频道的订阅,以避免不必要的资源浪费。 这个过程的精髓在于:**通过 Redis 的发布/订阅功能,将主动的、频繁的轮询(Polling)转变为被动的、高效的事件驱动通知**。客户端只有在锁很可能被释放时才会被唤醒并尝试,极大地减少了网络通信和对 Redis 的压力。 2. 关键配置参数 重试行为可以通过参数进行精细控制,主要在两个方法中体现: 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**`**,看门狗续期机制将会失效**。 3. 重试机制的优势与设计考量 1. **减少网络和计算开销**: 与简单的“循环-轮询”方案相比,Redisson 的“订阅-通知”机制避免了无用的空转和大量的 Redis 命令请求,非常高效。 2. **高实时性**: 一旦锁被释放,消息会通过 Redis 频道立即广播给所有订阅的客户端,它们可以近乎实时地发起争抢,减少了获取锁的延迟。 3. **避免活锁**: 在极高并发下,如果所有客户端同时被通知并同时发起请求,可能会造成瞬间拥堵。Redisson 在内部做了一些优化(例如在重试前加入非常短暂的随机延迟),来错开大量客户端的请求时间,提高单个客户端的成功率。 4. **可靠性**: 即使发布/订阅消息由于网络问题丢失,客户端也有基于 `ttl` 的等待超时作为备份机制,确保不会永久等待下去。 ### 11.Redisson锁的MutiLock原理 **核心答案:** MultiLock(联锁) 是一种将多个独立锁组合成一个逻辑锁的机制,遵循 **“全部或nothing”** 原则。 **核心原理:** 1. **原子性加锁**:客户端必须**按顺序**成功获取所有 constituent 子锁才算加锁成功。只要有一个子锁获取失败,**立即释放所有已获得的子锁**并进行重试,确保不会部分持锁。 2. **统一租约**:所有子锁都拥有**相同的超时时间**,并由一个看门狗线程**统一续期**,保证生命周期一致。 3. **原子性释放**:解锁时,会依次释放所有子锁。 **与 RedLock 的关键区别:** * **目的不同**:MultiLock 是为了**同时锁定多个不同资源**(如订单、库存);而 RedLock 是为了**提高一把锁的可用性**,在多个 Redis 节点上创建同一把锁的副本。 * **锁定对象**:MultiLock 锁多个不同 Key;RedLock 在多个实例上锁同一个 Key。 **一句话总结:** MultiLock 通过客户端协调的“全部成功+失败回滚”机制,实现对多个资源的原子性加锁操作。 主要使用场景: 1. **分布式事务(最经典)** * **场景**:在电商下单流程中,创建订单、扣减库存、扣减用户账户余额这三个操作必须作为一个原子单元执行。 * **用法**:使用 MultiLock 同时锁定 `订单ID`、`商品SKU`、`用户ID` 这三把锁。只有全部锁定成功,才执行后续业务逻辑,从而防止其他事务干扰,保证数据一致性。 2. **批量数据处理** * **场景**:需要批量更新一组用户的状态,要求要么全部更新成功,要么全部不更新,中间状态不可见。 * **用法**:获取这批所有 `用户ID` 对应的锁组成的 MultiLock。锁定成功后进行批量更新,避免在更新过程中,单个用户被其他操作修改,导致整体数据不一致。 3. **全局唯一性检查与操作** * **场景**:用户注册时,需要同时检查“用户名”和“手机号”是否都被占用。要求检查的那一刻两者都未被注册,才能创建新账号,防止在两次检查的间隙被其他请求插入。 * **用法**:使用 MultiLock 锁定 `用户名` 和 `手机号` 对应的资源,然后执行查询和插入操作。 ### **12.为什么下单流程不用Lua脚本而用联锁?** “这是一个关于**数据边界**和**工具职责**的问题。 * **Lua脚本**的原子性能力**仅限于Redis内部**的数据操作。而在经典架构下,电商的核心数据如订单、库存、余额通常保存在MySQL等传统数据库中,Redis更多用作缓存。Lua脚本无法跨出Redis去操作MySQL中的数据。 * **Redisson的联锁 (MultiLock)** 是一种**应用层的协调机制**。它的思路是:**‘锁住资源’而非‘操作数据’**。它通过同时获取代表这些不同资源的分布式锁,在逻辑上划定一个临界区。只有拿到所有锁的线程,才有资格去依次执行操作MySQL、调用服务等复杂业务逻辑。 Redis消息队列 --------- 为什么使用消息队列? 当前整个秒杀业务流程是串行化的,查询优惠卷、查询订单、删减库存、创建订单都是走的数据库,mysql本身并发能力就较弱,还加上了分布式锁,整个业务耗时流程长,并发能力弱。 ![](https://pic.code-nav.cn/post_picture/1828819762856710146/vMcjbuE9IzYHy1ST.webp "当前业务需要全部访问数据库") ![](https://pic.code-nav.cn/post_picture/1828819762856710146/xrl3EMOejxXgUX5j.webp "优化后的业务流程,秒杀优惠卷不需要经常访问数据库,直接在redis做查询") 三种结构好的,这是一份为你准备的、精炼的 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 2. 创建请求: 打开Postman,点击"New"按钮创建一个新的请求。在弹出的窗口中,选择请求的类型(GET、POST等),填入请求的URL,选择请求的Header、Body等信息。 3. 设置请求Header: 如果接口需要传递Header信息,可以在Postman中设置。点击请求的Headers选项卡,添加需要的Header信息,比如Authorization等。 4. 设置请求Body: 对于POST请求或者其他需要传递Body的请求,可以在Postman中设置请求的Body。可以选择不同的Body格式,比如form-data、raw、x-www-form-urlencoded等,并填入相应的参数。 5. 发送请求: 填好请求信息后,点击Send按钮发送请求。Postman会显示请求的响应信息,包括状态码、响应体等。 6. 查看响应: 在发送请求后,可以查看Postman显示的响应信息,包括响应的状态码、响应体等。可以根据需要进行断言、验证响应的正确性。 7. 保存请求: 如果需要保存请求,可以点击Save按钮保存请求信息,方便以后再次使用。 通过这些步骤,你可以使用Postman进行接口测试,验证接口的正确性和稳定性。 JMeter:秒杀系统如何做接口压力测试? 确定性能测试目标和指标: 在进行性能测试之前,我们需要先确定测试的目标和指标。在秒杀系统中,我们主要关注以下指标: 系统的吞吐量:即在一定时间内能够处理的请求数量; 系统的响应时间:即从发起请求到接收响应的时间; 系统的并发数:即同时处理的请求数量; 系统的错误率:即请求失败的比例。 通过确定这些指标,我们可以更好地了解系统的性能瓶颈,并进行优化 1,创建测试计划: 首先,我们需要创建一个测试计划。在 jmeter 中,测试计划是一个顶层元素,包含了所有的测试元素。 在测试计划中,我们需要添加线程组和 HTTP 请求。 线程组是一组并发请求的集合,它定义了一组并发用户,并指定了每个用户的行为。在秒杀系统中,我们可以将线程组的数量设置为需要测试的并发数。 HTTP 请求是一个发送 HTTP 请求的元素,它可以模拟客户端向服务器发送请求的过程。我们需要使用 HTTP 请求来模拟秒杀系统的请求。 在添加 HTTP 请求时,我们需要填写请求的 URL 和请求参数。在秒杀系统中,我们需要将登录参数化,以便模拟多个用户同时登录的场景。同时,我们需要使用循环控制器来模拟循环请求接口并发 100 2,设置测试参数和参数化 在 jmeter 中,我们可以使用 CSV 数据文件来设置测试参数和参数化。CSV 文件是一个以逗号分隔的文本文件,可以包含多个行和列,每个单元格都可以包含一个值。 在 CSV 文件中,我们可以存储多个用户名和密码,然后在测试中使用变量引用这些值。这样就可以模拟多个用户同时登录的场景。 3,运行测试并分析结果: 在设置完测试参数和参数化之后,我们可以运行测试并分析结果。在测试运行期间,我们可以使用 jmeter 的图表和报告功能来监测系统的性能指标,并查找性能瓶颈。 在测试结束后,我们需要对测试结果进行分析和总结。通过对测试结果的分析,我们可以找到系统的性能瓶颈。

跟着做黑马点评的项目,然后redis突然连接不上了。 这个redis我是跟着b站视频配置的,但配在vm虚拟机里,用的是centOS 7的操作系统,跟着视频改好了,然后也是能正常连接的。当时是只有一个问题,就是开个十来分钟,虚拟机就会卡主得重启才能再连接使用。但是前几天打开之后,连接后运行代码突然虚拟机报错【NMI watchdog:BUG : soft lockup -CPU#0 stuck for 21s!】,不懂虚拟机啊然后我直接重启了,后面直接报错长鸣!到今天重新打开,虚拟机倒是看着一切正常,但就是连不上redis了!

如何优雅地进行缓存预热?

# 一、问题和解决方案 场景:**缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。** 缓存服务宕机也会导致缓存雪崩现象,导致所有的请求都落到了数据库上。 为了保证非数据不占用太多内存空间,我们设置了逻辑过期时间。 但是如果热点数据出现过期就会造成缓存穿透、雪崩这些问题。为了解决这些问题,我们需要对已经过期或者将要过期的数据进行缓存重建。 重新导入数据到Redis中,并且重新设置逻辑过期时间。缓存重建需要对一些热点数据进行预热。之前我是这么预热的 ``` @Test void testSaveShop() { //测试id=1,时间10s redisUtils.saveShop2Redis(1L, 120L); } ``` 这样一个一个的写入id号,效率着实有点太慢了,而且万一不记得了,程序就会出现报错了。 针对缓存重建问题,我这里介绍使用缓存预热的两种方法来实现 # 二、缓存预热两种方案 ## 1、定时任务 使用`@EnableScheduling`开启定时任务 ### 1)获取ID列表 我们在mapper上创建方法,获取数据id号 ``` @Select("SELECT id FROM tb_shop") List<Integer> selectAllIds(); ``` ### 2)缓存重建逻辑 这段缓存重建的逻辑: 先根据传入的id号从数据库中获取值, 封装逻辑过期时间和数据,最后将数据进行写入 ``` //缓存重建 public void saveShop2Redis(Long id, Long expireSecond) { String key = CACHE_SHOP_KEY + id; //1、查询店铺数据 Shop shop = shopMapper.selectById(id); //2、封装逻辑过期时间 RedisData redisData = new RedisData(); redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSecond)); redisData.setData(shop); //3、写入redis stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData)); } ``` ### 3)定时任务缓存类 因为不需要特别复杂的逻辑,所以我这里就使用最为简单的spring自带的定时任务。 创建缓存重建定时任务类 - 首先开启定时任务注解 - 调用shopMapper方法,查询所有id号,遍历 - 将遍历的id号传入saveShop2Redis方法中,并设置逻辑过期时间60秒 - 日志打印输出 ``` @Component @Slf4j public class CachePreheatTask { @Resource private RedisUtils redisUtils; @Resource private ShopMapper shopMapper; // 执行缓存预热任务的方法 @Scheduled(cron = "0 19 9 * * ?") public void preheatCache() { // 执行缓存预热逻辑 List<Integer> selectAllIds = shopMapper.selectAllIds(); for (Integer allId : selectAllIds) { //测试id=1,时间10s redisUtils.saveShop2Redis(Long.valueOf(allId), 60L); log.debug("缓存数据预热成功,id为:{}" ,allId); } } } ``` 这里的表达式:cron = "0 19 9 * * ?" 表示的是每天上午9点19执行定时任务 调用 `selectAllIds`方法,把数据库表中的id查询出来,然后进行遍历 再利用for循环,把每次查询出的id传给`saveShop2Redis`方法进行缓存重建 为了方便测试,我设置逻辑过期时间为60秒。 `@Scheduled`注解是Spring框架中用于创建定时任务的注解,它有三个不同类型的参数:`cron`、`fixedDelay`、`fixedRate`,分别用于不同的定时任务需求。 > 1. `cron`参数:用于指定一个cron表达式,可以精确控制任务的执行时间。cron表达式是一个字符串,包含六个或七个空格分隔的时间字段,用于指定秒、分、时、日、月、周几等时间点。例如,`"0 * * * * ?"`表示每分钟执行一次。 > > 2. `fixedDelay`参数:用于指定任务执行结束后到下一次任务开始的间隔时间,单位为毫秒。即任务的执行周期是任务结束后延迟指定的时间后再执行。 > > 例如,`@Scheduled(fixedDelay = 1000)`表示任务执行结束后延迟1秒后再执行。 > > 3. `fixedRate`参数:用于指定任务开始执行后到下一次任务开始的间隔时间,单位为毫秒。即任务的执行周期是任务开始后固定的时间间隔再执行。例如,`@Scheduled(fixedRate = 1000)`表示任务开始后每隔1秒执行一次。 这些参数可以根据实际需求来选择,`cron`表达式适用于需要精确控制执行时间的场景,`fixedDelay`适用于任务执行时间不固定的场景,`fixedRate`适用于固定频率执行任务的场景。 | 表达式 | 意义 | | | -------------------- | -------------: | ---- | | 每隔5秒钟执行一次 | */5 * * * * ? | | | 每隔1分钟执行一次 | 0 * /1 * * * ? | | | 每天1点执行一次 | 0 0 1 * * ? | | 每天23点55分执行一次 | 0 55 23 * * ? | 这样就能达到我想要的效果,可以随便设置定时任务的执行时间,这样就可以提前进行预热了。 ## 2、消息队列 下面再介绍一种可以进行数据预热的方式——消息队列。 思考一下:我们的诉求是什么? 我们需要将数据进行预热,那我们是不是要拿到数据的id号。 拿到了id号呢,我们怎么让程序自动地去执行这段重建逻辑呢? 对的,使用消息队列,把id号传给消息队列,然后在项目启动的时候,让生产者去发送这个消息。消费者拿到消息之后,就会去执行重建的逻辑了。 ```mermaid graph TD 消息shopID号 --> 消息队列 --> 发送消息 --> 消费消息 --> 缓存重建 ``` 这里一些配置什么的我就不写了,都是固定的, ### 1)生产者代码 ``` @Component public class MyMessageProducer { @Resource private RabbitTemplate rabbitTemplate; // 向指定交换机发送消息 public void sendMessage(String exchange, String routingKey, String message) { //将消息发送到指定的交换机和路由键 rabbitTemplate.convertAndSend(exchange,routingKey,message); } } ``` ### 2)消费者代码 ``` @Component @Slf4j public class MyMessageConsumer { @Resource private RedisUtils redisUtils; /** * 接收消息的方法 * * @param message * @param channel * @param deliveryTag */ //使用@SneakyThrows注解简化异常处理 @SneakyThrows //使用该注解指定程序要监听的队列,,并设置消息的确认机制为手动 @RabbitListener(queues = {"hmdp_queue"}, ackMode = "MANUAL") //@Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag 用于从消息头中获取投递标签deliveryTag //在mq中,每条消息都会被分配一个唯一投递标签,用于标识该消息在通道中的投递状态和顺序,使用该注解可以从消息头中获取该投递标签,并将其赋值给deliveryTag参数, public void receiveMessage(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) { long shopId = Long.parseLong(message); redisUtils.saveShop2Redis(shopId, 60L); log.info("收到消息,传入的shopId为:{}", message+",缓存数据预热成功!"); //手动确认消息,消息确认标志设置为false,消息才能被确认 channel.basicAck(deliveryTag, false); } } ``` ### 3)创建队列交换机 在程序执行前创建好交换机和对列 ``` public class biInitMain { public static void main(String[] args) { try { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("192.168.88.130"); factory.setPort(5672); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); String EXCHANGE_NAME = "hmdp_exchange"; channel.exchangeDeclare(EXCHANGE_NAME, "direct"); // 声明一个队列,并且设置持久化消息 String queueName = "hmdp_queue"; String ROUTING_KEY="hmdp_routingKey"; channel.queueDeclare(queueName, true, false, false, null); //队列绑定交换机,routing_key用于指定消息应该发送到哪个队列。 channel.queueBind(queueName, EXCHANGE_NAME, ROUTING_KEY); } catch (Exception e) { e.printStackTrace(); } } } ``` ### 4)发送消息 用于获取热点id并将其发送到消息队列。这个程序应该只执行一次,以确保不会重复发送相同的id。 `@PostConstruct`注解会让项目启动时初始化这段代码,被执行一次。这样消息也就被发送给消费者了,缓存重建的逻辑也就执行成功了 ``` @PostConstruct public void init() { myMessageProducer.sendMessage("hmdp_exchange","hmdp_routingKey",String.valueOf(1L)); // 启动项目时初始化bloomFilter //bloomFilter = bloomFilterManager.create("bloomShopID", expectedInsertions, falseProbability); //List<Integer> list = shopMapper.selectAllIds(); //for (Integer shopId : list) { // bloomFilter.add(BLOOM_FILTER_SHOP + shopId); //} } ``` 看看控制台 <img src="https://pic.code-nav.cn/post_picture/1654343837738381313/5WjgmPfXgfenSjdd.webp" alt="image.png" width="100%" /> 其实代码到这里还是有点小问题的,细心的兄弟应该看出这里的问题了。 对的,之前使用定时任务,获取的是所有数据的id,获取的是所有的数据。 而这次消息队列改造,传入的是一个固定的id值。其实这里应该需要去获取一些热点数据id,再将这些id号传给方法。其中涉及到日志记录、监控数据判断是否是热点数据。 到这里我的缓存预热就结束了,其实就类似于项目的一个小优化的一样

黑马点评里的一点坑

<html> <head></head> <body> <div class="content ql-editor"> <p>jdk版本17,mybatis-plus版本3.4.3(课程提供的pom)</p> <p>使用lamdaQuery:</p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-type">User</span> <span class="ql-token hljs-variable">user</span> <span class="ql-token hljs-operator">=</span> lambdaQuery().eq(User::getPhone, phone).one(); </div> </div> <p>报错如下:</p> <div class="ql-code-block-container"> <div class="ql-code-block"> 2024-04-17 21:40:56.653 ERROR 26564 --- [nio-8081-exec-1] com.hmdp.config.WebExceptionAdvice : org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.builder.BuilderException: Error evaluating expression 'ew.sqlSegment != null and ew.sqlSegment != '' and ew.nonEmptyOfWhere'. Cause: org.apache.ibatis.ognl.OgnlException: sqlSegment [java.lang.ExceptionInInitializerError] </div> </div> <p>查了好多,原因是高版本jdk不兼容低版本mybatis-plus</p> <p><a href="https://blog.csdn.net/bb_dragon/article/details/135419255" target="_blank">Mybatis-plus3.4.3下使用lambdaQuery报错_could not initialize class com.baomidou.mybatisplu-CSDN博客</a></p> <p>解决方案:</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>降低jdk版本(不推荐)</li> <li data-list="bullet"><span class="ql-ui"></span>使用mybatis-plus最新版,目前是:3.5.6</li> </ol> <p><br></p> </div> </body> </html>

项目 登录 跟着黑马点评做登录功能的时候,遇到不解的bug: 1. 首次登录后直接再次跳转到登录界面,没有访问/me的消息(p1:完全没有任何其他请求信息) 2. 重新打开一个浏览器窗口,点击“我的”,顺利访问/me接口,说明session信息已保存,且登录校验过程正常 (p2) 为什么第一次登录之后校验不通过呢?(没有访问/me?校验不通过?)

#资源# #黑马点评# #笔记# 黑马点评项目二刷笔记,共计约15w字符,如果对你有所帮助,欢迎点赞、收藏。如果笔记中存在错误恳请及时告诉在在下,在下不胜感激[抱拳]

请问黑马点评这个项目,鱼皮的简历上写了这些性能提升,有没有佬测过啊

黑马点评,登录部分,图解剖🔎。欢迎指责问题

<html> <head></head> <body> <div class="content ql-editor"> <p>Session 实现登录</p> <p></p> <p><br></p> <p>Redis 实现登录</p> <p></p> </div> </body> </html>

请问在黑马点评项目中,这些百分比的测试是自己直接在浏览器发请求,然后F12看时间吗[大哭] 资源 技术 求职 项目 阅读 #提问# Java 后端

下载 APP