
实战#
基于session登录#

- 校验登录状态
用户在请求的时候,会从cookie中携带JsessionId到后台,后台通过JsessionId从session中拿到用户信息,如果没有session信息,则进行拦截,如果有session信息,则将用户信息保存到threadLocal中,并放行
校验登陆状态 用拦截器
为什么ThreadLoacl变量需要回收
ThreadLocal是用来隔离线程数据的。- 不回收的风险:内存泄漏 + 数据污染。
redis实现共享session
UserDto隐藏用户敏感信息
拦截器实现token更新

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

如何保证缓存和数据库的数据⼀致性?#
主动更新的方案:

采用 Cache Aside Pattern (旁路缓存模式/双写方案)
- 读操作:先读缓存,未命中则读DB并回填缓存
- 写操作:先更新DB,再删除缓存 删除缓存而非更新缓存 一种懒加载 (Lazy Loading) 思想,即使删除操作重复,其性能开销很小而且操作具有幂等性 如何保证缓存与数据库的操作同时成功/同时失败?
单体系统:将缓存与数据库操作放在同一个事务分布式系统:利用TCC等分布式事务方案 如果第二步“删除缓存”失败了怎么办?
缓存穿透#
是什么:查询一个一定不存在的数据。由于缓存中没有(缓存未命中),请求将直接转向后端存储(如数据库)。当大量此类请求同时发生时,就会给后端存储带来巨大的压力
- 查询的数据在缓存和数据库中都确定不存在。
- 高并发的恶意攻击或非预期的业务流量

- 缓存空对象 查询一个 key 返回为空时,仍然将这个“空结果”缓存起来,但为其设置一个较短的TTL 优点 简单;效果显著 缺点:额外的内存消耗(大量不同key);一致性问题
- 布隆过滤 请求进入redis先判断存在不存在 不存在直接拒绝 缺点 误判的风险
缓存雪崩#
同一时间大量缓存key失效或者redis服务宕机
- 给不同的Key的TTL添加随机值,让其在不同时间段分批失效
- 利用Redis集群提高服务的可用性(使用一个或者多个哨兵(
Sentinel)实例组成的系统,对redis节点进行监控,在主节点出现故障的情况下,能将从节点中的一个升级为主节点,进行故障转义,保证系统的可用性。 ) - 给缓存业务添加降级限流策略
- 给业务添加多级缓存(浏览器访问静态资源时,优先读取浏览器本地缓存;访问非静态资源(ajax查询数据)时,访问服务端;请求到达Nginx后,优先读取Nginx本地缓存;如果Nginx本地缓存未命中,则去直接查询Redis(不经过Tomcat);如果Redis查询未命中,则查询Tomcat;请求进入Tomcat后,优先查询JVM进程缓存;如果JVM进程缓存未命中,则查询数据库)
缓存击穿 hotkey问题#
高并发访问且缓存重建业务复杂的key突然失效了,大量请求访问瞬间给数据库带来巨大冲击
逻辑过期针对突然失效 直接不设置ttl了 手动设置expire 配合合适的内存淘汰策略
保证了可用性
互斥锁 保证了强一致性

CAP 定理 一个 分布式系统 最多只能同时满足一致性(Consistency)、**可用性(Availability)和分区容错性(Partition tolerance)**这三项中的两项
- 一致性(Consistency) C
- 可用性(Availability) A
- 分区容错性(Partition Tolerance) P
互斥锁的实现 利用redis命令 set NX#
原理:
- 缓存查询:线程访问数据,首先查询缓存。
- 锁获取:若缓存未命中,线程尝试获取互斥锁。
- 数据库操作:
- 成功获取锁的线程:查询数据库,将数据回填到缓存中,然后释放锁。
- 未能获取锁的线程:进入阻塞或自旋状态,短暂休眠后再次尝试查询缓存(而非直接尝试获取锁)。
- 缓存命中:当持有锁的线程完成缓存回填后,其他线程在重试时将直接命中缓存,避免了对数据库的冲击。
根本原因在于,从 “线程第一次检查缓存发现为空” 到 “该线程成功获取到锁” 这两个操作之间,存在一个时间差。在这个时间窗口内,缓存的状态可能已经被其他线程改变了
逻辑过期#
hot key的缓存直接不设置TTL 而是通过逻辑过期时间判断是否需要重建 重建缓存也通过互斥锁保证单线程执行 但是利用独立线程异步执行 其他线程直接返回旧数据
无侵入增加逻辑过期字段 用组合或者继承 组合更优
封装redis工具类#
泛型方法
泛型类型参数列表须位于方法的修饰符(如 public, static 等)之后,且在方法返回类型之前。
函数式接口
秒杀场景#
多人抢券
全局ID生成器#
在分布式系统下 要求
- 唯一性 高可用 高性能 递增性(利于索引建立)安全性(不能让用户看出规律啊)
基于redis自增策略:
为了ID的安全性,我们不直接使用Redis自增的数值,而是拼接一些其他信息

超卖问题-乐观锁#

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文件,配置反向代理和负载均衡(默认轮询就行)
可能用户两次请求被均衡到两个tomcat上那线程 1,3都能抢的到哇
synchronized 是基于单个 JVM 实例的锁机制,无法跨越多个 JVM 实例
分布式锁#
所有希望访问同一个共享资源的客户端,必须竞争同一个名字的锁(同一个Key)。 //这里“共享资源”并不是优惠券的库存,而是指定用户的下单资格
- 可见性:多个线程都能看到相同的结果。 注意:这里说的可见性并不是并发编程中指的内存可见性,只是说多个进程之间都能感知到变化的意思
基于SETNX实现的分布式锁:
- 死锁问题 利用ex参数 设置锁的ttl - 保证故障时依然能释放锁,避免死锁,提高安全性
- 锁误删问题
利用线程标识锁(用UUID标识,在一个JVM中,ThreadId地址,但是集群模式,有多个JVM,可能会出现ThreadId重复的情况)
释放锁需要先判断标识一致 再删除
分布式锁的原子性问题#
释放锁需要先判断标识一致 再删除 不是原子操作 极端情况依旧可能导致锁误删 Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性
-- 这里的KEYS[1]就是传入锁的key
-- 这里的ARGV[1]就是线程标识
-- 比较锁中的线程标识与线程标识是否一致
if (redis.call('get', KEYS[1]) == ARGV[1]) then
-- 一致则释放锁
return redis.call('del', KEYS[1])
end
return 0
-- java
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}lua在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完成抢单业务,将下单业务同步变异步

为了解决阻塞队列的不足:
redis消息队列#
消息队列也被称为消息代理(Message Broker) 解耦 不受jvm内存限制;数据安全 持久化 消息至少被消费一次
基于Pub/Sub实现消息队列#

基于List实现消息队列#
需要注意的是,当队列中没有消息时,RPOP和LPOP操作会返回NULL,而不像JVM阻塞队列那样会阻塞并等待消息,所以我们这里应该使用BRPOP或者BLPOP来实现阻塞效果
缺点:是remove and get无法避免消息丢失
基于Stream的消息队列#

$可能会出现漏读
STREAM类型消息队列的XREAD命令特点 1. 消息可回溯 2. 一个消息可以被多个消费者读取 3. 可以阻塞读取 4. 有漏读消息的风险
消费者组#
多个消费者划分到一个组中,监听同一个队列
- 消息分流
- 消息标识 记录最后一个被处理的消息
- 消息确认 - 消费者获取消息后,消息处于pending状态,并存入一个pending-list,当处理完成后,需要通过XACK来确认消息,标记消息为已处理,才会从pending-list中移除
创建删除消费者组:
XGROUP CREATE/DESTORY/CREATECONSUMER/DELCONSUMER
从消费者组/pending list中读取消息:#
正常情况下 不要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 模式
- 拉模式/读扩散
- 推模式/写扩散
- 推拉结合/读写混合 用户少,这里采用写扩散,博文发布成功后,立即将其ID主动推送(Push)到所有粉丝的收件箱(Redis ZSet),通过增加写操作的复杂度,来换取用户读取Feed时极低的时间复杂度和极高的性能
如何优化?—— 引入消息队列 (Message Queue)
高效滚动分页#
动态的信息流(Feed Stream)场景下,传统的“分页”模式(通常指基于 LIMIT 和 OFFSET 的分页)是不可行的,会有内容重复或遗漏问题
解决方法: 基于游标的分页 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]plaintextBitMap——签到#
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统计来说,这完全可以忽略。