Chlx's Live

Back

Pasted image 20260106121848

实战#

基于session登录#

Pasted image 20250930190038

  1. 校验登录状态
    用户在请求的时候,会从cookie中携带JsessionId到后台,后台通过JsessionId从session中拿到用户信息,如果没有session信息,则进行拦截,如果有session信息,则将用户信息保存到threadLocal中,并放行

校验登陆状态 用拦截器

为什么ThreadLoacl变量需要回收

  • ThreadLocal 是用来隔离线程数据的。
  • 不回收的风险:内存泄漏 + 数据污染

redis实现共享session Pasted image 20251001105357 Pasted image 20251001105438 UserDto隐藏用户敏感信息

拦截器实现token更新 Pasted image 20251001141826

cache#

缓存的作用 1. 降低后端负载 2. 提高读写效率,降低响应时间 缓存的成本 3. 数据一致性成本 4. 代码维护成本 5. 运维成本(一般采用服务器集群,需要多加机器,机器就是钱)

缓存更新策略#

Pasted image 20251001173203

如何保证缓存和数据库的数据⼀致性?#

主动更新的方案:

Pasted image 20251001181915

采用 Cache Aside Pattern (旁路缓存模式/双写方案)

  • 读操作:先读缓存,未命中则读DB并回填缓存
  • 写操作:先更新DB,再删除缓存 删除缓存而非更新缓存 一种懒加载 (Lazy Loading) 思想,即使删除操作重复,其性能开销很小而且操作具有幂等性 如何保证缓存与数据库的操作同时成功/同时失败?
  • 单体系统:将缓存与数据库操作放在同一个事务
  • 分布式系统:利用TCC等分布式事务方案 如果第二步“删除缓存”失败了怎么办? Pasted image 20251002161031

缓存穿透#

是什么:查询一个一定不存在的数据。由于缓存中没有(缓存未命中),请求将直接转向后端存储(如数据库)。当大量此类请求同时发生时,就会给后端存储带来巨大的压力

  • 查询的数据在缓存和数据库中都确定不存在。
  • 高并发的恶意攻击或非预期的业务流量 Pasted image 20251002210002
  1. 缓存空对象 查询一个 key 返回为空时,仍然将这个“空结果”缓存起来,但为其设置一个较短的TTL 优点 简单;效果显著 缺点:额外的内存消耗(大量不同key);一致性问题
  2. 布隆过滤 请求进入redis先判断存在不存在 不存在直接拒绝 缺点 误判的风险

缓存雪崩#

同一时间大量缓存key失效或者redis服务宕机

  1. 给不同的Key的TTL添加随机值,让其在不同时间段分批失效
  2. 利用Redis集群提高服务的可用性(使用一个或者多个哨兵(Sentinel)实例组成的系统,对redis节点进行监控,在主节点出现故障的情况下,能将从节点中的一个升级为主节点,进行故障转义,保证系统的可用性。 )
  3. 给缓存业务添加降级限流策略
  4. 给业务添加多级缓存(浏览器访问静态资源时,优先读取浏览器本地缓存;访问非静态资源(ajax查询数据)时,访问服务端;请求到达Nginx后,优先读取Nginx本地缓存;如果Nginx本地缓存未命中,则去直接查询Redis(不经过Tomcat);如果Redis查询未命中,则查询Tomcat;请求进入Tomcat后,优先查询JVM进程缓存;如果JVM进程缓存未命中,则查询数据库)

缓存击穿 hotkey问题#

高并发访问缓存重建业务复杂的key突然失效了,大量请求访问瞬间给数据库带来巨大冲击 Pasted image 20251002210720 逻辑过期针对突然失效 直接不设置ttl了 手动设置expire 配合合适的内存淘汰策略 保证了可用性 互斥锁 保证了强一致性

Pasted image 20251002211453

CAP 定理 一个 分布式系统 最多只能同时满足一致性(Consistency)、**可用性(Availability)分区容错性(Partition tolerance)**这三项中的两项

  • 一致性(Consistency) C
  • 可用性(Availability) A
  • 分区容错性(Partition Tolerance) P
互斥锁的实现 利用redis命令 set NX#

原理:

  • 缓存查询:线程访问数据,首先查询缓存。
  • 锁获取:若缓存未命中,线程尝试获取互斥锁。
  • 数据库操作
    • 成功获取锁的线程:查询数据库,将数据回填到缓存中,然后释放锁
    • 未能获取锁的线程:进入阻塞或自旋状态,短暂休眠后再次尝试查询缓存(而非直接尝试获取锁)。
  • 缓存命中:当持有锁的线程完成缓存回填后,其他线程在重试时将直接命中缓存,避免了对数据库的冲击。

Pasted image 20251003150717 根本原因在于,从 “线程第一次检查缓存发现为空”“该线程成功获取到锁” 这两个操作之间,存在一个时间差。在这个时间窗口内,缓存的状态可能已经被其他线程改变了

逻辑过期#

hot key的缓存直接不设置TTL 而是通过逻辑过期时间判断是否需要重建 重建缓存也通过互斥锁保证单线程执行 但是利用独立线程异步执行 其他线程直接返回旧数据

无侵入增加逻辑过期字段 用组合或者继承 组合更优

封装redis工具类#

泛型方法 泛型类型参数列表须位于方法的修饰符(如 public, static 等)之后,且在方法返回类型之前。 函数式接口

秒杀场景#

多人抢券

全局ID生成器#

在分布式系统下 要求

  • 唯一性 高可用 高性能 递增性(利于索引建立)安全性(不能让用户看出规律啊) 基于redis自增策略: 为了ID的安全性,我们不直接使用Redis自增的数值,而是拼接一些其他信息 Pasted image 20251003232312 Pasted image 20251003234826

超卖问题-乐观锁#

Pasted image 20251004144019

UPDATE product
SET stock = stock - 1
WHERE id = 10 AND stock > 0; -- 使用`stock`来充当版本号
sql

“乐观锁”(CAS思想)其操作的原子性,确实是依赖并借助了 MySQL(特指 InnoDB 存储引擎)底层的行级排他锁 (Exclusive Row Lock) 来保证、

初级乐观锁stock来充当版本号能解决超卖 但是成功率低,因为大家一起去进行扣减只有一个能成功,其他的人在处理时,他们在扣减时,库存已经被修改过了,所以此时其他线程都会失败 简单修改一下即可:只要数据库中的库存大于0,都能顺利完成扣减库存操作

一人一单#

在判断库存是否充足之后,根据我们保存的订单数据,判断用户订单是否已存在

为了线程安全 加悲观锁 一人一单,所以这个锁,应该只加在单个用户上,锁对象应该是userId 注意对userID的字符串调用intern()方法:如果创建的字符串对象在常量池中有,那么就不会在堆中创建新的String对象 锁可能会先于事务提交而释放 如果当前方法被Spring的事务控制,你在内部加锁,可能会导致当前方法事务还没有提交,但是锁已经释放了,所以我们选择将当前方法整体包裹起来,确保事务不会出现问题 内部调用导致事务失效AopContext.currentProxy()来获取当前对象的代理对象,再用代理对象调用方法

集群环境下的并发问题#

单机模拟集群: 将springboot服务启动两份,端口分别为8081和8082(命令行参数设置) 修改nginx的config目录下的nginx.conf文件,配置反向代理和负载均衡(默认轮询就行) Pasted image 20251004164546 可能用户两次请求被均衡到两个tomcat上那线程 1,3都能抢的到哇 synchronized 是基于单个 JVM 实例的锁机制,无法跨越多个 JVM 实例

分布式锁#

所有希望访问同一个共享资源的客户端,必须竞争同一个名字的锁(同一个Key)。 //这里“共享资源”并不是优惠券的库存,而是指定用户的下单资格

  1. 可见性:多个线程都能看到相同的结果。 注意:这里说的可见性并不是并发编程中指的内存可见性,只是说多个进程之间都能感知到变化的意思

Pasted image 20251007122547 基于SETNX实现的分布式锁:

  • 死锁问题 利用ex参数 设置锁的ttl - 保证故障时依然能释放锁,避免死锁,提高安全性
  • 锁误删问题 Pasted image 20251007160038 利用线程标识锁(用UUID标识,在一个JVM中,ThreadId地址,但是集群模式,有多个JVM,可能会出现ThreadId重复的情况) 释放锁需要先判断标识一致 再删除

分布式锁的原子性问题#

释放锁需要先判断标识一致 再删除 不是原子操作 极端情况依旧可能导致锁误删 Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性

在RedisTemplate中,可以利用execute方法去执行lua脚本

当项目编译打包后,src/main/resources/ 中的所有文件会被复制到 target/classes/ 目录下。故ClassPathResource默认就去resources根目录找

利用Redisson实现分布式锁#

基于SETNX实现的分布式锁存在以下问题: 不可重入 不可重试 超时释放 主从不一致风险 引入Redisson 依赖 新建配置类即可

Redisson可重入锁原理#

利用 Redis 的 Hash 数据结构存储锁信息,并结合 Lua 脚本保证操作的原子性 Fields:

  • UUID:threadId: 锁的持有者标识。UUID 是 Redisson 客户端实例的唯一ID,threadId 是持有锁的线程ID。这个组合唯一标识了一个线程。
  • Value: 重入次数。每次一个线程重入,这个值就加 1,释放减1
如何解决“业务执行时间超过 TTL”的问题?#

因为锁的有效期 timeout 难以预估 锁续期 原理:Watchdog,看门狗机制的核心就是通过异步的后台线程来完成锁的自动续期 显式地提供 leaseTime 参数。一旦提供了 leaseTime,看门狗将不会被启用

黑马redis p67 原理视频讲解

分布式锁主从一致问题#

Redisson锁的MutiLock原理 不用主从了 加锁的逻辑写入到每一个redis节点

不使用redis实现一个进程级别的伪分布式锁#

用一个ReentrantLock作为一个类的私有变量,实例获取到了就执行逻辑

秒杀优化#

在redis里完成秒杀资格的判断(基于Lua脚本redis.call('sismember', orderKey, userId) == 1 判断用户是否下单) 根据lua返回结果然后打包订单信息扔到阻塞队列里面去 这里的业务就完成了

线程池需要@PostConstruct 其任务就是从阻塞队列不断取出订单

秒杀业务需要在类初始化之后,就立即执行,所以这里需要用到@PostConstruct注解

关键:先利用Redis完成抢单业务,将下单业务同步变异步 Pasted image 20251009192838

为了解决阻塞队列的不足:

redis消息队列#

消息队列也被称为消息代理(Message Broker) 解耦 不受jvm内存限制;数据安全 持久化 消息至少被消费一次

基于Pub/Sub实现消息队列#

Pasted image 20251009225301

基于List实现消息队列#

需要注意的是,当队列中没有消息时,RPOP和LPOP操作会返回NULL,而不像JVM阻塞队列那样会阻塞并等待消息,所以我们这里应该使用BRPOP或者BLPOP来实现阻塞效果

缺点:是remove and get无法避免消息丢失

基于Stream的消息队列#

Pasted image 20251009225741 Pasted image 20251009230042

$可能会出现漏读

STREAM类型消息队列的XREAD命令特点 1. 消息可回溯 2. 一个消息可以被多个消费者读取 3. 可以阻塞读取 4. 有漏读消息的风险

消费者组#

多个消费者划分到一个组中,监听同一个队列

  • 消息分流
  • 消息标识 记录最后一个被处理的消息
  • 消息确认 - 消费者获取消息后,消息处于pending状态,并存入一个pending-list,当处理完成后,需要通过XACK来确认消息,标记消息为已处理,才会从pending-list中移除 创建删除消费者组:XGROUP CREATE/DESTORY/CREATECONSUMER/DELCONSUMER
从消费者组/pending list中读取消息:#

Pasted image 20251010104353 正常情况下 不要noack,id用>即可

总结: STREAM类型消息队列的XREADGROUP命令的特点 1. 消息可回溯 2. 可以多消费者争抢消息,加快消费速度 3. 可以阻塞读取 4. 没有消息漏读风险 5. 有消息确认机制,保证消息至少被消费一次

Stream消息队列实现异步秒杀下单#

项目启动时,开启一个线程任务,尝试获取stream.orders中的消息,完成下单 stringRedisTemplate.opsForStream().read SACK stream.orders g1 id

SortedSet——博客和点赞#

@TableField(exist = false) 用来解决实体类中有的属性但是数据表中没有的字段

Redis Sortedset 集合来判断是否点赞过(按点赞顺序展示点赞用户) 没有isMember方法,通过查询score来判断集合中是否有该元素

博主与粉丝的关系,数据库中有一张tb_follow表来标示

共同关注 Redis集合求交集SINTER

Feed流#

推荐算法 Timeline 模式

  1. 拉模式/读扩散
  2. 推模式/写扩散
  3. 推拉结合/读写混合 用户少,这里采用写扩散,博文发布成功后,立即将其ID主动推送(Push)到所有粉丝的收件箱(Redis ZSet),通过增加写操作的复杂度,来换取用户读取Feed时极低的时间复杂度和极高的性能

如何优化?—— 引入消息队列 (Message Queue)

高效滚动分页#

动态的信息流(Feed Stream)场景下,传统的“分页”模式(通常指基于 LIMITOFFSET 的分页)是不可行的,会有内容重复或遗漏问题

解决方法: 基于游标的分页 Redis 的 List不支持游标 故使用SortedSet

ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
plaintext

参数解释:

  • key: Sorted Set 的键名。
  • max: 查询范围的最大分数值(score)
  • min: 查询范围的最小分数值(score),默认0即可
  • [LIMIT offset count]: 在查询出的结果内部,再进行偏移和计数

解决SQL的in查询结果顺序是未定义的: 使用 ORDER BY FIELD() 来自定义排序顺序

GEO数据结构——附件商户#

底层数据结构确实是 Sorted Set 元素添加是O(log(N)) ,N是sorted set的元素数量

  • Geohash 算法的核心是生成一个代表地理位置的二进制位序列
  • Geohash 字符串是这个二进制序列的一种Base32 编码,便于人类和通用系统使用。
  • Redis Geo 的 score 是这个二进制序列的整数表示,是为了适配 Sorted Set 的数据结构并实现极致的查询性能
GEOSEARCH key [FROMMEMBER member] [FROMLONLAT longitude latitude] [BYRADIUS radius m|km|ft|mi]   
[BYBOX width height m|km|ft|mi] [ASC|DESC] [COUNT count [ANY]] [WITHCOORD] [WITHDIST] [WITHHASH]
plaintext

BitMap——签到#

Redis中是利用String类型数据结构实现BitMap,因此最大上限是512M,转换为bit则是2^32个bit位

  • SETBIT:向指定位置(offset)存入一个0或1
  • BITCOUNT:统计BitMap中值为1的bit位的数量
  • BITFIELD:操作(查询、修改、自增)BitMap中bit数组中的指定位置(offset)的值

这里签到业务就用 用户和data 拼接做key bitmap标记哪一天签到了

签到统计#

对于redis返回的结果 从后往前遍历每个bit位统计连续的1 获取本月所有签到 BITFIELD key GET u[dayOfMonth] 0

UV 统计#

  • UV:全称Unique Visitor,也叫独立访客量,是指通过互联网访问、浏览这个网页的自然人。1天内同一个用户多次访问该网站,只记录1次。
  • PV:全称Page View,也叫页面访问量或点击量,用户每访问网站的一个页面,记录1次PV,用户多次打开页面,则记录多次PV。往往用来衡量网站的流量。

HyperLogLog#

HyperLogLog(HLL)是从Loglog算法派生的概率算法,用户确定非常大的集合基数,而不需要存储其所有值

  • Redis中的HLL是基于string结构实现的,单个HLL的内存永远小于16kb!作为代价,其测量结果是概率性的,有小于0.81%的误差。不过对于UV统计来说,这完全可以忽略。
Redis 实战篇
https://lixuan.live/blog/redis-shi-zhan-pian
Author Chlx
Published at 2025年10月24日
Comment seems to stuck. Try to refresh?✨