1. Entity
2. DTO
- UserDTO
- LoginFormDTO
- Result
1. UserHolder
2. RedisConstants
1. 接口
2. 实现类
1. token 刷新拦截器
2. 登录校验
- 缓存就是数据交换的缓冲区(称作Cache [ kæʃ ] ),是存贮数据的临时地方,一般读写性能较高。
- 业务场景:
- 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
- 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存
- Cache Aside Pattern(最优解)
- 由缓存的调用者,在更新数据库的同时更新缓存
- Read/Write Through Pattern
- 缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题。
- Write Behind Caching Pattern
- 调用者只操作缓存,由其它线程异步的将缓存数据持久化到数据库,保证最终一致。
1. Cache Aside Pattern
- 删除缓存还是更新缓存?
- 更新缓存:每次更新数据库都更新缓存,无效写操作较多
- 删除缓存:更新数据库时让缓存失效,查询时再更新缓存(√)
- 如何保证缓存与数据库的操作的同时成功或失败?
- 单体系统,将缓存与数据库操作放在一个事务
- 分布式系统,利用TCC等分布式事务方案
- 先操作缓存还是先操作数据库?
- 先删除缓存,再操作数据库
- 先操作数据库,再删除缓存(√)
2. 最佳实践方案
- 低一致性需求:使用Redis自带的内存淘汰机制
- 高一致性需求:主动更新,并以超时剔除作为兜底方案
- 读操作:
- 缓存命中则直接返回
- 缓存未命中则查询数据库,并写入缓存,设定超时时间
- 写操作:
- 先写数据库,然后再删除缓存
- 要确保数据库与缓存操作的原子性
- 读操作:
3. 主动更新策略
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
1. 缓存空对象
1.1 解决方案
1.2 优缺点
- 优点:实现简单,维护方便
- 缺点:
- 额外的内存消耗
- 可能造成短期的不一致
1.3 业务流程
1.4 代码实现
2. 布隆过滤
2.1 解决方案
2.2 优缺点
- 优点:内存占用较少,没有多余key
- 缺点:
- 实现复杂
- 存在误判可能
3. 其他解决方案
- 增强id的复杂度,避免被猜测id规律
- 做好数据的基础格式校验
- 加强用户权限校验
- 做好热点参数的限流
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
- 给不同的 Key 的 TTL 添加随机值
- 利用 Redis 集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
1. 互斥锁
1.1 解决方案
1.2 业务流程
1.3 代码实现
2. 逻辑过期
2.1 解决方案
2.2 业务流程
2.3 创建RedisData
2.4 代码实现
1. RedisData
2. RedisCacheUtils
3. 使用工具
1. 面临问题
当用户抢购时,就会生成订单并保存到 数据库,而订单表如果使用数据库自增ID就存在一些问题:
- id的规律性太明显
- 受单表数据量的限制
2. ID生成器特性
- 唯一性
- 高可用
- 高性能
- 递增性
- 安全性
3. ID生成器组成
ID的组成部分:
- 符号位:1bit,永远为0
- 时间戳:31bit,以秒为单位,可以使用69年
- 序列号:32bit,秒内的计数器,支持每秒产生2^32个不同 ID
4. 代码实现
5. 生成策略
- UUID
- Redis自增
- snowflake算法
- 数据库自增
1. 业务流程
2. 代码实现
1. 超卖原因
2. 问题解决
3. 乐观锁
3.1 版本号法
3.2 CAS法
- 代码实现
1. 业务流程
2. 代码实现
3. 并发安全问题
集群模式下,synchronized 锁失效:
1. 分布式锁原理
2. 分布式锁特点
**分布式锁:**满足分布式系统或集群模式下多进程可见并且互斥的锁。
- 多进程可见
- 互斥
- 高可用
- 高性能(高并发)
- 安全性(死锁问题)
- 可重入性
- 公平锁
- …
3. 分布式锁实现
1. 实现方法
实现分布式锁时需要实现的两个基本方法:
- 获取锁
- 互斥:确保只能有一个线程获取锁
- 非阻塞:尝试一次,成功返回true,失败返回false
- 释放锁
- 手动释放
- 超时释放:获取锁时添加一个超时时间
2. 业务流程
3. 代码实现
3.1 创建锁对象
3.2 使用锁对象
1. 阻塞情况一
1.1 产生原因
- 线程1 获取锁,由于某种原因导致业务阻塞
- 业务阻塞时间过长,导致线程1超时释放锁
- 此时线程2 获取锁,执行业务
- 线程2 执行任务过程中,线程1 继续,完成业务
- 线程1 完成业务后 释放锁,此时释放的锁为 线程2 的锁
1.2 解决方案
- 在获取锁时存入线程标识(可以用UUID表示)
- 在释放锁时先获取锁中的线程标识,判断是否与当前线程标识一致
1.3 业务流程
1.4 代码实现
2. 阻塞情况二
2.1 产生原因
- 线程1 完成 锁标识后,释放锁之前,发生阻塞,导致超时释放锁
- 此时 线程2 获取锁,执行业务
- 线程2 执行任务过程中,线程1 继续并释放锁
- 此时释放的锁为 线程2 的锁
2.2 解决方案
基于Lua脚本实现分布式锁的释放锁逻辑,确保多条命令执行时的原子性。
- 执行redis命令
- 调用脚本
2.3 业务流程
- 获取锁中的线程标识
- 判断是否与指定的标识(当前线程标识)一致
- 如果一致则释放锁(删除)
- 如果不一致则什么都不做
2.4 代码实现
- unlock.lua
- 释放锁
3. 实现思路
- 利用 set nx ex 获取锁,并设置过期时间,保存线程标示
- 释放锁时先判断线程标示是否与自己一致,一致则删除锁
- 基于Lua脚本实现分布式锁的释放锁逻辑,确保原子性
4. 特性
- 利用 set nx 满足互斥性
- 利用 set ex 保证故障时锁依然能释放,避免死锁,提高安全性
- 利用 Redis 集群保证高可用和高并发特性
1. 分布式锁存在问题
- 不可重入:同一个线程无法多次获取同一把锁
- 不可重试:获取锁只尝试一次就返回false,没有重试机制
- 超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患
- 主从一致性:如果Redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现
2. Redisson 介绍
Redisson 是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
- 分布式锁(Lock)和同步器(Synchronizer)
- 可重入锁(Reentrant Lock)
- 公平锁(Fair Lock)
- 联锁(MultiLock)
- 红锁(RedLock)
- 读写锁(ReadWriteLock)
- 信号量(Semaphore)
- 可过期性信号量(PermitExpirableSemaphore)
- 闭锁(CountDownLatch)
3. Redisson入门
3.1 引入依赖
3.2 配置客户端
3.3 使用分布式锁
4. 可重入锁原理
4.1 业务流程
4.2 获取锁
4.3 释放锁
4.4 代码实现
5. 分布式锁原理
- 可重入:利用hash结构记录线程id和重入次数
- 可重试:利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制
- 超时续约:利用watchDog,每隔一段时间(releaseTime / 3),重置超时时间
6. 主从一致性问题
- java 应用 获取锁之后,主节点进行主从同步之前发生宕机
- java程序获取的锁失效
- 解决办法:联合锁—只有所有节点都拿到锁才成功
1. 不可重入Redis分布式锁
- 原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标示
- 缺陷:不可重入、无法重试、锁超时失效
2. 可重入的Redis分布式锁
- 原理:利用hash结构,记录线程标示和重入次数;利用watchDog延续锁时间;利用信号量控制锁重试等待
- 缺陷:redis宕机引起锁失效问题
3. Redisson的multiLock
- 原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功
- 缺陷:运维成本高、实现复杂
1. 原始业务流程
2. 优化业务流程
3. 秒杀业务流程
1. 新增优惠券并保存Redis
2. Lua脚本判断是否抢购成功
3. 执行Lua脚本判断抢购结果
4. 开启线程任务
消息队列(Message Queue),字面意思就是存放消息的队列。最简单的消息队列模型包括3个角色:
- 消息队列:存储和管理消息,也被称为消息代理(Message Broker)
- 生产者:发送消息到消息队列
- 消费者:从消息队列获取消息并处理消息
Redis提供了三种不同的方式来实现消息队列:
- list结构:基于List结构模拟消息队列
- PubSub:基本的点对点消息模型
- Stream:比较完善的消息队列模型
- 队列是入口和出口不在一边,因此我们可以利用:LPUSH 结合 RPOP、或者 RPUSH 结合 LPOP来实现。
- 不过要注意的是,当队列中没有消息时 RPOP 或 LPOP 操作会返回 null,并不像 JVM 的阻塞队列那样会阻塞并等待消息。因此这里应该使用 BRPOP 或者 BLPOP 来实现阻塞效果。
1. 优点
- 利用Redis存储,不受限于JVM内存上限
- 基于Redis的持久化机制,数据安全性有保证
- 可以满足消息有序性
2. 缺点
- 无法避免消息丢失
- 只支持单消费者
**PubSub(发布订阅)**是Redis2.0版本引入的消息传递模型。顾名思义,消费者可以订阅一个或多个channel,生产者向对应channel发送消息后,所有订阅者都能收到相关消息。
- SUBSCRIBE channel [channel] :订阅一个或多个频道
- PUBLISH channel msg :向一个频道发送消息
- PSUBSCRIBE pattern[pattern] :订阅与pattern格式匹配的所有频道
1. 优点
- 采用发布订阅模型,支持多生产、多消费
2. 缺点
- 不支持数据持久化
- 无法避免消息丢失
- 消息堆积有上限,超出时数据丢失
1. 发送消息
2. 读取第一个消息
3. 读取最新消息
注意
当我们指定起始 ID 为 $ 时,代表读取最新的消息,如果我们处理一条消息的过程中,又有超过1条以上的消息到达队列,则下次获取时也只能获取到最新的一条,会出现漏读消息的问题。
4. xread命令特点
- 消息可回溯
- 一个消息可以被多个消费者读取
- 可以阻塞读取
- 有消息漏读的风险
消费者组(Consumer Group):将多个消费者划分到一个组中,监听同一个队列。具备下列特点:
- 消息分流:队列中的消息会分流给组内的不同消费者,而不是重复消费,从而加快消息处理的速度
- 消息标示:消费者组会维护一个标示,记录最后一个被处理的消息,哪怕消费者宕机重启,还会从标示之后读取消息,确保每一个消息都会被消费
- 消息确认:消费者获取消息后,消息处于pending状态,并存入一个pending-list。当处理完成后需要通过XACK来确认消息,标记消息为已处理,才会从pending-list移除
1. 创建消费者组
- key:队列名称
- groupName:消费者组名称
- ID:起始ID标示,$代表队列中最后一个消息,0则代表队列中第一个消息
- MKSTREAM:队列不存在时自动创建队列
2. 其他常见命令
3. 从消费者组读取消息
- group:消费组名称
- consumer:消费者名称,如果消费者不存在,会自动创建一个消费者
- count:本次查询的最大数量
- BLOCK milliseconds:当没有消息时最长等待时间
- NOACK:无需手动ACK,获取到消息后自动确认
- STREAMS key:指定队列名称
- ID:获取消息的起始ID:
- “>”:从下一个未消费的消息开始
- 其它:根据指定id从pending-list中获取已消费但未确认的消息,例如0,是从pending-list中的第一个消息开始
4. 消费者监听消息
5. xreadgroup命令特点
- 消息可回溯
- 可以多消费者争抢消息,加快消费速度
- 可以阻塞读取
- 没有消息漏读的风险
- 有消息确认机制,保证消息至少被消费一次
可以利用多消费者加快处理
可以利用消费者组提高消费速度,减少堆积
1. 创建消息队列
2. 编写Lua脚本
- 在认定有抢购资格后,直接向 stream.orders 中添加消息,内容包含 voucherId、userId、orderId
3. 执行Lua脚本
4. 开启一个线程任务
5. 尝试获取消息
6. 完成下单
1. 前端接口
2. POJO
- tb_blog:探店笔记表,包含笔记中的标题、文字、图片等
- tb_blog_comments:其他用户对探店笔记的评价
3. 保存图片
4. 保存笔记
5. 获取探店笔记
- 同一个用户只能点赞一次,再次点击则取消点赞
- 如果当前用户已经点赞,则点赞按钮高亮显示(前端已实现,判断字段Blog类的isLike属性)
1. 添加标识
- 给 Blog 类中添加一个isLike字段,标识是否被当前用户点赞
2. 完成点赞功能
- 利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞过则点赞数-1
- 修改根据id查询Blog的业务,判断当前登录用户是否点赞过,赋值给isLike字段
- 修改分页查询Blog业务,判断当前登录用户是否点赞过,赋值给isLike字段
- 按照点赞时间先后排序,返回Top5的用户
1. 改造点赞功能
2. 获取点赞用户
3. 排序打印结果
1. 前端接口
2. 数据表
3. POJO
4. Controller
5. Service
1. 改造关注接口
- 利用 Redis 中恰当的数据结构,实现共同关注功能。在博主个人页面展示出当前用户与博主的共同好友。
2. 完成共同关注
- Controller
- Service
1. Feed流
Feed流产品有两种常见模式:
- Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈
- 优点:信息全面,不会有缺失。并且实现也相对简单
- 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
- 智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户
- 优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷
- 缺点:如果算法不精准,可能起到反作用
2. Timeline模式
该模式的实现方案有三种:
- 拉模式:也叫做读扩散
- 推模式:也叫做写扩散
- 推拉结合:也叫做读写混合,兼具推和拉两种模式的优点
3. 方案对比
4. 关注推送功能
- 修改新增探店笔记的业务,在保存blog到数据库的同时,推送到粉丝的收件箱
- 收件箱满足可以根据时间戳排序,必须用Redis的数据结构实现
- 查询收件箱数据时,可以实现分页查询
4.1 Controller
4.2 Service
5. Feed流的分页问题
5.1 问题描述
- Feed流中的数据会不断更新,所以数据的角标也在变化,因此不能采用传统的分页模式。
5.2 滚动分页
6. 滚动分页查询
- 参数:
- max:当前时间戳(上一次查询的最小时间戳)
- min:0
- offset:0(上一次查询结果中,与最小值一样的元素个数)
- count:3(单次查询条数)
6.1 POJO
6.2 Controller
6.3 Service
- 在首页中点击某个频道,即可看到频道下的商户:
我们按月来统计用户签到信息,签到记录为1,未签到则记录为0。
- 把每一个bit位对应当月的每一天,形成了映射关系。用0和1标示业务状态,这种思路就称为 位图(BitMap)。
- **Redis **中是利用 string 类型数据结构实现 BitMap,因此最大上限是512M,转换为bit则是232个bit位。
- [SETBIT]:向指定位置(offset)存入一 个 0 或 1
- [GETBIT]:获取指定位置(offset)的bit值
- [BITCOUNT]:统计BitMap中值为1的bit位的数量
- [BITFIELD]:操作(查询、修改、自增)BitMap中bit数组中的指定位置(offset)的值
- [BITFIELD_RO]:获取BitMap中bit数组,并以十进制形式返回
- [BITOP]:将多个BitMap的结果做位运算(与 、或、异或)
- [BITPOS]:查找 bit 数组中指定范围内第一个0或1出现的位置
- 发送请求
- 测试结果
- UV:全称Unique Visitor,也叫独立访客量,是指通过互联网访问、浏览这个网页的自然人。1天内同一个用户多次访问该网站,只记录1次。
- PV:全称Page View,也叫页面访问量或点击量,用户每访问网站的一个页面,记录1次PV,用户多次打开页面,则记录多次PV。往往用来衡量网站的流量。
UV统计在服务端做会比较麻烦,因为要判断该用户是否已经统计过了,需要将统计过的用户信息保存。但是如果每个访问的用户都保存到Redis中,数据量会非常恐怖。
Hyperloglog(HLL)是从Loglog算法派生的概率算法,用于确定非常大的集合的基数,而不需要存储其所有值。相关算法原理大家可以参考:https://juejin.cn/post/6844903785744056333#heading-0
Redis中的HLL是基于string结构实现的,单个HLL的内存永远小于16kb,内存占用低的令人发指!作为代价,其测量结果是概率性的,有小于0.81%的误差。不过对于UV统计来说,这完全可以忽略。