简单梳理一下使用的相关知识以及功能等
1.基于Session实现登录的功能
把短信验证码存储在session 登陆成功后用户信息也保存在session
将用户信息存储在 ThreadLocal 中的主要作用是在多线程环境中实现线程私有的数据存储。
ThreadLocal 是 Java 中一个用于提供线程局部变量的类。线程局部变量是指每个线程都有自己独立的变量副本,一个线程无法访问其他线程的局部变量。ThreadLocal 实例通常用于在多线程环境下保持对象的独立性,每个线程可以拥有自己的变量副本,互不干扰。
ThreadLocal 的实现原理主要依赖于 Thread 类的成员变量 ThreadLocalMap。每个线程都有一个私有的 ThreadLocalMap 实例,用于存储该线程的局部变量。ThreadLocalMap 是一个自定义的 Map,它的 key 是 ThreadLocal 实例,value 是对应线程的局部变量。
- set 方法:
当调用 ThreadLocal 的 set 方法时,实际上是通过当前线程的 ThreadLocalMap 将 ThreadLocal 实例作为 key,将要存储的值作为 value 放入 map 中。因为每个线程都有自己的 ThreadLocalMap,所以不同线程之间的局部变量不会相互干扰。
- get 方法:
当调用 ThreadLocal 的 get 方法时,实际上是从当前线程的 ThreadLocalMap 中根据 ThreadLocal 实例获取对应的值。因为每个线程都有自己的 map,所以获取的是该线程独立的局部变量。
- remove 方法:
当调用 ThreadLocal 的 remove 方法时,会从当前线程的 ThreadLocalMap 中移除对应的 ThreadLocal 实例,防止内存泄漏。
2.登录校验拦截器
Java 项目中,拦截器通常用于拦截请求和响应,执行一些额外的逻辑,例如身份验证、日志记录、性能监控等。在 Spring 框架中,拦截器被广泛应用于处理这些方面的需求。
刷新时间在所有都拦截的拦截器进行
防止用户一直访问不需要用户信息的界面导致过期
使用UserDto 作用 减少tomcat内存使用 并且不发送敏感信息
3.Redis存储
@Resourceprivate StringRedisTemplate stringRedisTemplate;
stringRedisTemplate.opsForValue().
set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
缓存的作用:
缓存更新策略:
1.内存淘汰 不用自己维护 利用Redis的内存淘汰机制,当内存不足时自动淘汰部分数据 下次查询时更新缓存 一致性差 没有维护成本
2.超时剔除 给缓存数据添加TTL时间,到期后自动删除缓存,下次查询时更新缓存 一致性一般 维护成本低
3.主动更新 编写业务逻辑 在修改数据库的同时更新缓存 一致性好 维护成本高
关于主动更新:
1.由缓存的调用者 在更新数据库的同时更新缓存
2.缓存与数据库整合为一个服务,由服务来维护一致性 调用者调用该服务,无需关心缓存一致性问题
3,调用者只操作缓存 由其它线程异步将缓存数据持久化到数据库,保证最终一致
主动更新需要考虑别的问题
对于缓存的操作:更新缓存还是删除缓存?
更新缓存的话,每次更新数据库都要更新缓存,无效写操作太多 ❌
删除缓存,更新数据库时让缓存失效,查询时再更新缓存 √
如何保证缓存与数据库的操作同时成功或失败?
单体系统的话,将缓存与数据库操作 放在 一个事务里
分布式系统的话,利用TCC等分布式方案
【扩展 TCC(Try-Confirm-Cancel)是一种分布式事务模型,用于解决分布式系统中的事务一致性问题。它将整个事务过程拆分为三个阶段:尝试(Try)、确认(Confirm)和取消(Cancel)。
- 尝试(Try)阶段: 在这个阶段,业务逻辑会尝试执行事务操作,但并不真正提交。这个阶段通常包括对业务规则的检查、资源的预留等。如果所有的操作都成功,系统会进入确认阶段;否则,将进入取消阶段。
- 确认(Confirm)阶段: 如果尝试阶段成功,系统将执行真正的事务提交,将之前尝试阶段的操作提交到数据库或其他持久性存储中。确认阶段的成功表示整个事务成功完成。
- 取消(Cancel)阶段: 如果在尝试阶段或确认阶段出现错误,或者在全局事务无法提交的情况下,将进入取消阶段。在取消阶段,之前的操作将被回滚,以确保事务的一致性。这个阶段的目标是释放之前尝试阶段所预留的资源,避免产生不一致的状态。】
先操作缓存还是先操作数据库?
如果先删除缓存
正常情况 线程1 删除缓存 更新数据库 线程2 查询缓存 未命中 查询数据库 写入缓存 正常
异常情况:线程1 删除缓存 还没进行更新数据库(操作较慢)
线程2执行 查询缓存未命中 查找数据库 写入缓存 然后线程1 更新数据库 数据不一致 很可能发生
如果查询缓存未命中,显诚意查询数据库, 此时线程2进入 更新了数据库,然后删除缓存
先操作数据库
正常情况: 线程2 更新数据库 然后删除缓存 线程1 进入未命中缓存 查询数据库 写入新缓存
异常情况: 线程1 查询缓存未命中 然后查询数据库 此时线程2 更新了数据库 并且删除了缓存 这时线程1 查询成功 写入了错误的数据缓存
但是总体上说 写数据库更慢 写缓存是很快的 所以先删除缓存 更容易异常 所以选择先操作数据库,后删除缓存更好
缓存一致性的方案: 低一致要求:使用Redis自带的内存淘汰机制
高一致要求:主动更新 并用超时剔除作为兜底方案
读操作:缓存命中直接返回 未命中则查询数据库 写入缓存 设置超时时间
写操作: 先写数据库,然后再删除缓存 保证数据库与缓存操作的原子性
缓存穿透
缓存穿透指的是客户端请求的数据在数据库和缓存都不存在,缓存永远不会生效,请求都打到数据库 给数据库带来巨大的压力
常见的解决方案有两种: 1.缓存空对象 优点:实现简单 维护方便 缺点:额外的内存消耗 可能造成短期的不一致
2.布隆过滤 优点:内存占用少 没有多余的key 缺点:实现复杂 存在误判的可能
其它方案: 增强id复杂度 避免被猜测id规律 做好数据基础格式校验 加强用户权限校验 做好热点参数的限流
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
1.给不同的Key的TTL添加随机值
2.利用Redis集群提高服务的可用性
3.给缓存业务添加降级限流策略
【扩展:降级和限流是在分布式系统中常用的两种策略,用于保护系统免受异常或高负载的影响。它们的目的是在极端情况下保持系统的稳定性和可用性。
- 降级(Degradation): 降级是指在系统遇到异常情况时,有选择性地关闭系统的一些功能,以确保核心功能或关键业务仍然可用。降级是为了保护整体系统的可用性,即使某些功能出现问题,也不至于导致整个系统崩溃。
- 限流(Rate Limiting): 限流是指对系统的请求进行控制,限制其处理速率,以防止过多的请求同时涌入系统。限流可以平滑处理请求流量,防止系统因请求过载而崩溃。常见的限流策略包括令牌桶算法和漏桶算法。 】
4,给业务添加多级缓存
缓存击穿
缓存击穿也叫热点Key问题,就是一个被高并发访问并且缓存重建业务比较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击
常见的解决方案:
1.互斥锁 2.逻辑过期
互斥锁: 互斥锁会等待一个线程写入缓存后读取新的缓存命中返回
逻辑过期:新的线程会去做查数据库新建缓存的过程 这段时间内的查询线程获得的都是旧数据 直接返回不进行等待
比较 互斥锁 优点:没有额外的内存消耗 保证一致性 实现简单 缺点:线程需要等待 性能受影响 可能有死锁风险
逻辑过期 优点:线程无需等待,性能较好 缺点:不保证一致性 有额外内存消耗 实现复杂
优惠券秒杀
全局唯一ID 要求:唯一性 递增性 安全性 高可用 高性能
使用数据库自增会出现一些问题: id规律太明显 受单表数据量的限制
使用Redis可以做到单线程自增
策略:每天一个key 方便统计订单量 构造:时间戳+计数器
秒杀超卖问腿:
典型的多线程安全问题 常见方案就是加锁
悲观锁:认为线程安全问题一定会发生,因此在操作数据前先获得锁、确保线程串行执行 例如Synchronized Lock等都属于悲观锁 简单粗暴 性能一般
乐观锁:认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改 如果没有修改则认为是安全的 自己才更新数据 如果已经被其它线程修改了说明发生了安全问题此时可以重试或者异常 性能好 但是存在成功率低的问题
悲观锁性能太差 使用乐观锁
乐观锁关键是判断之前的数据是否有修改过常见两种:CAS法 和版本号法
比较相同有问题:失败率太高 同时进入 都失败了就结束了
可以改成>0
也可以考虑使用分段锁,分段锁住资源提高成功率
实现一人一单:悲观锁 查询用户是否下过订单
分布式锁
满足分布式系统或集群模式下多进程可见并且互斥的锁
常见方法有3种: 1.Mysql 利用mysql本身的互斥锁机制 高可用性比较好 性能一般 安全性方面 端口连接自动释放锁
2.Redis 使用setnx这样的互斥命令 高可用性好 高性能好 安全性方面利用锁超时时间 到期释放
3.zookeeper 利用节点的唯一性和有序性实现互斥 高可用性好 高性能性一般 安全性 临时节点 断开连接自动释放
基于Redis的分布式锁
两个基本方法:
获取锁:互斥 保证只能由一个线程获取锁
SETNX lock thread1 #添加锁 setnx保证互斥性
EXPIRE lock 10 #添加锁过期时间 避免服务宕机引起死锁
释放锁: 手动释放 超时释放
DEL key #删除 释放锁
业务阻塞 超时误删
由于业务阻塞 结果到了超时时间业务还没结束 被误删锁 导致其它线程进入
解决方法:使用线程标识 判断是不是自己的锁 不是自己的锁不能删除 解决误删情况
Lua脚本
可以在一个脚本编写多条Redis命令 保证多条Redis命令的原子性
java调用Lua脚本:
举例:
加载资源 的类 声明初始化 加载脚本文件
基于Redis的分布式锁优化
基于setnx的分布式锁还存在一下问题:
- 不可重入 同一个线程无法多次获取同一把锁
- 不可重试 获取锁只尝试一次就返回false 没有重试机制
- 超时释放 锁超时释放可以避免死锁 但是业务执行耗时长 也可能导致锁释放 存在安全隐患
- 主从一致性 如果Redis提供了主从集群 主从同步存在延迟 当主宕机时 如果从并同步主中的所数据 则会出现锁实现
Redisson 在Redis基础上实现的Java驻内存数据网络 不仅提供了一系列的分布式的Java常用对象 还提供了许多分布式服务 包括各种分布式锁的实现
可重入锁:使用一个哈希值来记录线程标识和重入次数
获得一个锁被获取的线程数=0才能被释放 被访问+1访问结束-1 利用watchdog延续锁时间 利用信号量控制锁重试等待
WatchDog
看门狗会在获取锁的时候启动,定期向分布式存储发生心跳或者更新锁的信息 表明客户端仍然活跃并且持有该锁 这样做的目的:
1.防止锁过期: 如果客户端在持有锁的过程中由于某种原因无法及时更新锁的信息 超时时间到达后,系统会认为锁已过期,并采取相应措施,如自动释放锁
2.确保锁的续约:通过定期的心跳 看门狗实现了锁的自动续约 如果客户端在锁的超时时间内能定期更新所信息,就可以一直持有锁 不会被系统自动释放 这种机制可以有效应对客户端崩溃 网络故障等情况 保障分布式
Redis主从一致性问题:
全是主节点: 使用联锁
多个独立的Redis节点 必须在所有节点都获取重入锁,才算获取锁成功
缺点:运维成本高 实现复杂
秒杀优化
使用阻塞队列 存放秒杀成功的用户id
缺点:阻塞队列满了 (内存)无法创建订单 ;如果宕机或者服务重启 数据丢失 (阻塞队列存在内存) 数据非持久化
秒杀思路:
1.先利用Redis完成库存余量 一人一单判断,完成抢单业务
2.再将下单业务放入阻塞队列,利用独立线程异步下单
消息队列:
3个角色 消息队列:存储和管理消息 也被称为消息代理
生产者:发生消息到消息队列
消费者:从消息队列获取消息并处理消息
【扩展 Redis 是持久化的:
Redis是一个基于内存的键值存储系统,通常用于缓存和数据存储。Redis提供了持久化的机制,以确保在Redis服务器重启时数据不会丢失。有两种主要的持久化方式:
- RDB(Redis DataBase)持久化:
RDB 是将 Redis 在某个时间点的数据保存到磁盘上的快照(snapshot)。可以定期创建这样的快照,也可以在满足一定条件时自动创建。RDB 文件是一个二进制文件,保存了某个时刻的数据库快照。
RDB持久化适用于备份数据、灾难恢复等场景。但要注意,如果Redis意外关闭,最后一次生成的RDB文件可能包含最后一次快照之后的所有更改,因此在这种情况下可能会丢失一部分数据。
- AOF(Append Only File)持久化:
AOF 持久化记录了对数据库的所有写操作,以追加的方式保存到文件中。通过重放这个文件,可以恢复出之前的数据库状态。
AOF持久化适用于更高的数据保证要求,因为它记录了每一次写操作,但相对于RDB,AOF文件通常会更大】
Redis 实现消息队列的三种方式:
1.List结构: 基于List模拟消息队列
2.PubSub:基本的点对点消息模型
3.Stream:比较完善的消息队列模型
List的消息队列: 优点 利用Redis存储 不受限于JVM内存上限 基于Redis的持久化机制 数据安全性有保证 可以满足消息的有序性
缺点:无法避免消息丢失 只支持单消费者
PubSub消息队列:
SUBSCRIBE channel[channel]
PUBLISH channel msg PSUBSCRIBE[pattren]
优点:采用发布订阅模型 支持多生产者 多消费者
缺点: 不支持数据持久化 无法避免消息丢失 消息堆积有上限 超出时数据丢失
基于Stream
XREAD命令特点:
消息可回溯 一个消息可以被多个消费者读取 可以堵塞读取 有消息漏读的风险
解决消息漏读问题--消费者组
将对各消费者划分到一个组中 监听同一个队列 特点:
- 消息分流 队列中的消息会分流给组内的不同消费者 而不是重复消费 加快了消息处理的速度
- 消息标示 消费者组会维护一个标示 记录最后一个被处理的消息 哪怕消费者宕机重启 还会从标示之后读取消息 确保每一个消息都被消费
- 消息确认: 消费者获取消息后,消息处于pending状态,并存入一个pending-list,当处理完后需要通过XACK来确认消息,标记消息已经被处理 才会从pending-list 移除
命令:
消费者组特点: 消息可回溯 可以多消费者抢夺消息 加快消费速度 可以阻塞读取 没有消息漏读风险 有消息确认机制,保证消息至少被消费一次
总结:
关注推送:
Feed流: 通过无限下拉获取新的信息
两种常见模式:
1.TimeLine:不做内容筛选 简单的按照内容发布时间排序 常用于好友或关注 例如朋友圈
优点:信息全面 不会丢失 实现简单 缺点:信息噪音多 用户不一定感兴趣 内容获取率低
2,智能排序:利用智能算法屏蔽掉用户不感兴趣的内容 推送用户感兴趣的信息来吸引用户
优点:用户粘度高 容易沉迷 缺点:如果算法不精准 容易适得其反
两种模式: 拉模式和推模式 也叫读扩散和写扩散
拉模式:把关注的用户发送的消息都拉到自己的收件箱 很少使用
推模式:用户发送消息就推送到自己所有的粉丝 适合没有大V 用户量少
推拉结合 对于活跃粉丝采用推模式 对于偶尔上线查看的普通粉丝采用拉模式 适合有大V用户
Feed流的滚动分页:
Redis GEO 数据结构 地理位置
签到 连续签到记录
减少内存使用 采用位图存储 BitMap
UV统计 HyperLog
HyperLog 多种算法综合的概率算法 对于大数据量来说误差是可以接受的