Redis案例

   日期:2024-12-26    作者:linzhiwen688 移动:http://3jjewl.riyuangf.com/mobile/quote/42646.html
 
 
 
 

1. Entity

 

2. DTO

  • UserDTO
 
  • LoginFormDTO
 
  • Result
 
 

1. UserHolder

 

2. RedisConstants

 
 
 
 

1. 接口

 

2. 实现类

 
 
 
 
 

1. token 刷新拦截器

 

2. 登录校验

 
 
 
 
 
  • 缓存就是数据交换的缓冲区(称作Cache [ kæʃ ] ,是存贮数据的临时地方,一般读写性能较高。
 
 
项目内存淘汰超时剔除主动更新说明内存不足时自动淘汰部分数据缓存数据添加TTL时间,到期自动删除修改数据库同时更新缓存。一致性差一般好维护成本无低高
  • 业务场景
    • 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
    • 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存
  • 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. 分布式锁实现

项目MySQLRedisZookeeper互斥利用mysql本身的互斥锁机制利用setnx这样的互斥命令利用节点的唯一性和有序性实现互斥高可用好好好高性能一般好一般安全性断开连接,自动释放锁利用锁超时时间,到期释放临时节点,断开连接自动释放

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命令特点

  • 消息可回溯
  • 可以多消费者争抢消息,加快消费速度
  • 可以阻塞读取
  • 没有消息漏读的风险
  • 有消息确认机制,保证消息至少被消费一次
项目ListPubSubStream消息持久化支持不支持支持阻塞读取支持支持支持消息堆积处理受限于内存空间
可以利用多消费者加快处理受限于消费者缓冲区受限于队列长度
可以利用消费者组提高消费速度,减少堆积消息确认机制不支持不支持支持消息回溯不支持不支持支持

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的用户
ListSetSortedSet排序方式按添加顺序排序无法排序根据score值排序唯一性不唯一唯一唯一查找方式按索引查找或首尾查找根据元素查找根据元素查找

1. 改造点赞功能

2. 获取点赞用户

 

3. 排序打印结果

 
 
 

1. 前端接口

2. 数据表

 

3. POJO

 

4. Controller

 

5. Service

 
 

1. 改造关注接口

  • 利用 Redis 中恰当的数据结构,实现共同关注功能。在博主个人页面展示出当前用户与博主的共同好友。

2. 完成共同关注

  • Controller
 
  • Service
 
 

1. Feed流

Feed流产品有两种常见模式

  • Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈
    • 优点:信息全面,不会有缺失。并且实现也相对简单
    • 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
  • 智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户
    • 优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷
    • 缺点:如果算法不精准,可能起到反作用

2. Timeline模式

该模式的实现方案有三种

  • 拉模式:也叫做读扩散
  • 推模式:也叫做写扩散
  • 推拉结合:也叫做读写混合,兼具推和拉两种模式的优点

3. 方案对比

项目拉模式推模式推拉结合写比例低高中读比例高低中用户读取延迟高低低实现难度复杂简单很复杂使用场景很少使用用户量少、没有大V过千万的用户量,有大V

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统计来说,这完全可以忽略。


 

特别提示:本信息由相关用户自行提供,真实性未证实,仅供参考。请谨慎采用,风险自负。


举报收藏 0评论 0
0相关评论
相关最新动态
推荐最新动态
点击排行
{
网站首页  |  关于我们  |  联系方式  |  使用协议  |  隐私政策  |  版权隐私  |  网站地图  |  排名推广  |  广告服务  |  积分换礼  |  网站留言  |  RSS订阅  |  违规举报  |  鄂ICP备2020018471号