redis


置顶
idea层次结构怎么用
@PostMapping等同于 @RequestMapping(value = "/路径", method = RequestMethod.POST),但更简洁
session实现会话跟踪的原理
客户端首次访问服务器:
服务器检测到请求没有 Session ID(如 JSESSIONID),会创建一个新的 Session。
服务器生成一个 唯一 Session ID,并存储在内存(或持久化存储如 Redis)中。
服务器通过 响应头(Set-Cookie) 将 Session ID 返回给客户端(浏览器)。
客户端后续请求:
浏览器自动在请求头(Cookie)中携带 Session ID。
服务器解析 Session ID,找到对应的 Session 数据,实现状态跟踪。
构造函数实现注入
@TableField(exist = false)告诉 MyBatis-Plus 忽略该字段,不会参与 SQL 操作(插入、更新、查询等)。
适用于实体类中存在但数据库表中没有对应字段的属性
mp用法 voucherorderserviceimpl
voucherserviceimpl
数据类型
string,hash,list,set,sortedset
通用命令
KEYS a*
DEL
EXISTS
EXPIRE 设置有效期
TTL -1永久有效 -2过期
String类型
三种格式:字符串,int,float
key的层级格式 A:B:C 避免id重复
hash类型
value是一个无序字典,类似于java中的HashMap结构 value是哈希表
k-v的v又是一个k-v列表(里面的key叫field或者hash-key)
List类型
与java中的LinkedList类似,可以看作双向链表结构
有序,元素可以重复,插入删除快,查改速度慢
使用场景:朋友圈评论
LPOP,RPOP都是移除并返回,LRANGE只是返回元素
用list结构模拟栈,队列,阻塞队列
右进右出(同侧)
右进左出(不同侧)
BLPOP+不同侧
Set类型
Redis的Set结构与Java中的HashSet类似,可以看做是一个value为null的HashMap
无序,元素不可重复,查找快,支持交集并集等功能
SortedSet类型
功能上类似于treeset,但是底层数据结构不一样
可排序,元素不重复,查询速度快
连接不上时候记得关闭防火墙
java客户端

jedis
引入依赖--创建jedis对象--使用jedis--释放资源
线程不安全+性能损耗,推荐使用连接池代替直连方式
SpringDataRedis
Spring Data 是 Spring 框架的一个子项目,旨在简化与各种数据存储技术(如关系型数据库、NoSQL数据库、图数据库等)的集成和操作
提供了redistemplate工具类,封装各种对redis的操作
redistemplate可以接收任意object作为之写入redis,只不过写入前会把object序列化成字节形式
默认使用的是jdk序列化器,有问题:可读性差,占用空间大
(它使用 Java 原生 ObjectOutputStream。
存储成字节码(不可读),有类名版本等强依赖,易出兼容问题。
Redis 客户端(如 RedisInsight、命令行)中看不到人类可读内容)
因此自定义序列化方式
key用string,StringRedisSerializer
value用json,GenericJackson2JsonRedisSerializer,使用 Jackson(流行的 JSON 库)将对象序列化成 JSON 格式的字符串,同时支持反序列化
理解序列化:将java对象转换为字节数组,便于存储
以上白雪,还有一种使用StringRedisTemplate
写入redis,手动把对象序列化为json
读取redis,手动把读取到的json反序列化为对象,需要传递类的字节码
JdkSerializationRedisSerializer Java 原生序列化,带类信息 ❌乱码 不推荐,兼容性差
StringRedisSerializer 只用于字符串,适合 key ✅可读 用于 key/hashKey
GenericJackson2JsonRedisSerializer 转成 JSON 字符串,支持各种类型 ✅可读 通用推荐
实战篇
短信登录
项目导入
基于session实现登录
流程图
;
发送验证码:
用户在提交手机号后,会校验手机号是否合法,如果不合法,则要求用户重新输入手机号。
如果手机号合法,后台此时生成对应的验证码,同时将验证码保存到session中,然后再通过短信的方式将验证码发送给用户。
短信验证码登录、注册:
用户将验证码和手机号进行输入,后台从session中拿到当前验证码,然后和用户输入的验证码进行校验,如果不一致,则无法通过校验;如果一致,则后台根据手机号查询用户,如果用户不存在,则为用户创建账号信息,保存到数据库,无论是否存在,都会将用户信息保存到session中,创建session时tomcat会自动生成JsessionId写到用户浏览器的Cookie里,方便后续获得当前登录信息。
校验登录状态:
用户在请求时候,会从cookie中携带着JsessionId到后台,后台通过JsessionId从session中拿到用户信息,也就是说登录凭证就是JsessionId。如果没有session信息,则进行拦截;如果有session信息,则将用户信息保存到threadLocal中,并且放行。
不理解怎样保证1,2步骤对同一个session的操作
同一Session的保证:依赖 JSESSIONID Cookie 的自动传递和服务器的 Session 存储机制。
开发者只需:直接操作 HttpSession 对象,无需关心 ID 传递细节。
同一个用户只要 session 没过期 set 与 get 操作访问的是同一个 HttpSession 对象
浏览器在登录请求 /user/login 时,会自动带上之前服务器给它的 Cookie(Cookie: JSESSIONID=ABC123),服务器就用这个 JSESSIONID 找到你上次的 Session,自然就能拿出之前存在 session 中的验证码了
集群session共享问题
多台tomcat不能共享session存储空间
redis可以替代的原因:共享、内存、键值
基于Redis实现共享session登录
注意这个存到redis中,是两种类型的数据
1.选择String类型存验证码即可,key是手机号,value:验证码
2.选择Hash存储用户信息,因为每个字段独立,比较好去CRUD,内存占用少,key用token即可(key使用token而不使用手机号,防止信息泄露的风险)
与session的方式相比,由于无法使用自动化的cookie,因此生成的token需要手动返还给客户端
注意有效期的设置,每次拦截判断后,有效期需要更新(用户30分钟后失去登录状态)
LoginIntercepter不是bean,不能spring注入其他bean,MvcConfig注入stringRedisTemplate,然后传给LoginInterceptor
这个地方不太清楚--springboot,bean注入的三种方式--通过构造函数方式注入
注:有个long转换成string的报错
这种redis的方式比session更好理解一点,主要不太清楚session的原理
redis代替session需要考虑的问题:合适的数据结构,合适的key,合适的存储粒度
登录拦截器的优化
如果一直在访问一些不需要拦截的页面,token有效期就不刷新,时间一到过期了,因此拆分拦截器,第一个拦截器拦截所有行为并刷新token有效期,但是不进行拦截,等于监视所有行为,起到记录的作用
商户查询缓存
什么是缓存,添加redis缓存,缓存更新策略
缓存的应用场景丰富


缓存更新策略
三种更新策略:

主动更新策略:

策略1 cache aside需要考虑的问题

选择:先删除数据库,再删除缓存,方案2出现线程安全的可能性更低

一个示例实现:店铺更新
缓存穿透
解决方案:缓存null值,布隆过滤
示例:给查询商铺解决缓存穿透问题(缓存null值),增强id的复杂性
缓存雪崩
同一时段大量的缓存key同时失效或者redis服务宕机,大量请求到达数据库

解决方案:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
缓存击穿
缓存击穿问题,也叫 热点 Key 问题;就是一个被 高并发访问 并且 缓存中业务较复杂的 Key 突然失效,大量的请求在极短的时间内一起请求这个 Key 并且都未命中,无数的请求访问在瞬间打到数据库上,给数据库带来巨大的冲击。

两种解决缓存击穿的策略:互斥锁,逻辑过期

| 解决方案 | 优点 | 缺点 |
|---|---|---|
| 互斥锁 | 没有额外的内存消耗;保证一致性;实现简单 | 线程需要等待,性能受影响;可能有死锁风险 |
| 逻辑过期 | 线程无需等待,性能较好 | 有额外内存消耗;不保证一致性;实现复杂 |
| 搪塞不搪塞的区别 | ||
| 一致性vs可用性 |
基于互斥锁

过程:redis中查找,缓存命中直接返回,未命中去查找数据库重建缓存--先拿锁,拿到锁就进行查找数据库重建缓存,没拿到说明此时有别的线程在执行,休眠一段时间后再次请求,setnx来实现lock的功能。
基于逻辑过期
逻辑过期就是永不过期
创建个expire字段,同样redis中查找,正常都能命中,如果没命中返回空,命中之后判断是否过期,未过期的话直接返回,过期的话需要查找重建--先拿锁,没拿到的话就返回当前值,拿到的话新建一个线程进行数据库查询和缓存重建
为什么新建线程:不新建的话主线程在堵着

二次检查
封装工具类
涉及知识泛型,函数式编程,stringredistemplate的注入
优惠卷秒杀
redis实现全局id生成器
全局ID生成器:在分布式系统下生成全局唯一ID的工具,满足 唯一性,高可用,高性能,递增性(有利于数据库创建索引),安全性
数据库自增ID的劣势:id规律性太明显;收到单表数据量的限制

ID的组成部分:符号位:1bit,永远为0
时间戳:31bit,以秒为单位,可以使用69年
序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID
示例:实现全局id生成器
全局唯一ID生成策略:
UUID,Redis自增,snowflake雪花算法,数据库自增(建立一张表关联)
Redis自增ID策略:
每天一个key,方便统计订单量
ID构造是 时间戳+计数器
示例--添加优惠卷 调用接口
示例--实现秒杀下单 完成代码
库存超卖问题分析
高并发的秒杀场景下出现多卖问题
多线程并发安全问题
jmeter的使用:需要带上author字段,这个字段具体是干啥的也忘了
超卖问题产生的原因

超卖问题的解决方式

悲观锁实现简单,但是简单的串行操作会导致效率较差
乐观锁:版本号法,CAS法
乐观锁的关键是修改之前判断数据有没有被修改,相当于数据库做了一次新的查询,数据库的update操作加了一个锁
示例--CAS法
乐观锁的弊端:
100个线程同时查到,有一个线程先更改了数据,剩下的99个都会更改失败,从而只卖出一份
改进:
不再查是否相等,而是直接查数据库中的字段值是否大于0
voucher.getStock() Java 对象 voucher 的 stock 属性值,这个值是内存中的值,可能不是数据库当前的真实值(存在并发问题)
stock 数据库表中的 stock 字段值

一人一单
要实现一个用户只能下一单

根据用户id和优惠卷id到数据库的订单表中进行查询
示例--一人一单实现
问题:类似于超卖问题,并发地都查到没有订单,都新增了订单
解决方式:加悲观锁,乐观锁用于更新数据因此无法使用
加锁synchronized应该对用户id加
释放锁的时机:先获取锁,再事务提交之后才去释放锁??
事务是否失效的问题:spring事务失效的几种可能性,代理对象vs真对象??
集群下的并发安全问题
实现了一个小集群,两个节点的集群(service--运行服务配置--springboot)
项目的结构??反向代理,轮询
集群模式下有多个jvm的存在,每个jvm都会有自己的锁,获取一个线程

分布式锁
synchronized只能保证单个jvm内部的线程之间互斥,不能保证集群下的多个jvm互斥

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁

实现思路

不再用synchronized了,而是用自实现的锁来实现
锁误删问题--释放锁之前判断锁标识是否是自己
使用uuid表示,因为不同jvm的线程id可能会重复(如果线程1和线程2的线程id相同则判断失误),类似于uuid实现全局唯一ID
ID_PREFIX (UUID)是每个 JVM 实例唯一的前缀

锁的原子性问题--判断操作和释放操作不是一个原子操作, Lua脚本解决分布式锁的原子性问题
Lua脚本的写法
目前为止是一个相对完整的分布式锁

-------回看代码,熟悉mp用法
stringredistemplate注入不是bean的拦截器的过程不理解
几个缓存的封装
还有悲观锁时的注意事项
mp使得在简单场景下可以不显示定义mapper层
redisson
基于setnx实现的分布式锁有以下的问题

redisson的快速入门

redisson怎么解决四类问题的:
不可重入

方法1拿到锁了之后进入方法2,方法2无法拿到锁,因此卡住
解决方法:判断这个锁是不是自己拿的
哈希结构记录获取锁的线程(说是线程,应该也是uuid+id类似的东西,毕竟要实现不同jvm之间的互斥)以及重入的次数,当锁的标识是自己的时候,锁计数加1,实现重入,当释放时候,如果是自己的锁,锁计数-1,判断计数是否是0,如果是0则删除锁
waittime参数,在waittime内都会进行重试
redisson源码分析的部分先跳过
redis秒杀优化

业务流程较长,将资格查验与操作数据库分开,异步执行
step1:基于redis完成秒杀资格判断:需求1和2
step2:基于阻塞队列实现秒杀异步下单:需求3和4

需求3中与线程池有关的都不太懂
整个流程:
变同步下单为异步下单,主线程要做的事就是完成秒杀资格的判断,而耗时较久的数据库写操作开启独立线程执行,进行异步操作

tips:杀完8080端口进程后要重新开启ngnix服务啊
redis消息队列实现异步秒杀
问题:
- 大量秒杀单场景下容易超出阻塞队列的内存上限;
- jvm内存没有持久化机制,服务重启或者宕机时阻塞队列的所有任务都会丢失,或者拿出的任务没有成功执行,该任务也会丢失
![]()
Redis提供了三种不同方式来实现消息队列
1.list结构:模拟消息队列
2.Pubsub:基本的点对点模型
3.Stream :比较完善的消息队列模型
利用redis中的list结构来模拟消息队列 BLPOP,BRPUSH

pubsub实现消息队列

基于stream的消息队列
XADD XREAD

消费组
常用命令:
1.创建消费者组 XGROUP CREATE
2.从消费组读取消息

3.确认消费后的消息
4.查看pendinglist

被消费 ✅ + 被确认(XACK) 不能再被消费(除非你手动去用 XREADGROUP 从老ID读,在这个消费组里它不会再分配给任何消费者,但在stream里还在)
被消费 ✅ + 没有确认 进入 Pending List (PEL),可以被重新分配消费
达人探店
--实现查看店铺笔记
快速创建方法ctrl alt m
lambda表达式变方法引用
--实现点赞功能

主要使用了set结构
这里注意因为之前的拦截器没有分割,只在特定的需要登录验证的页面才保存用户信息到userHolder,因此userholder中可能没有用户信息,会报空指针异常
分割下拦截器就解决了,后续又添加了对threadlocal中有无用户信息的判断
--点赞排行
主要使用了sortedset数据结构,与set相比多了score
zscore实现ismember的功能
mp查询数据库,同时查询多条,in指令,同时控制查询顺序
queryLikesById可以熟悉下beanutil和mp的用法
好友关注
--关注与取关
follow表中维护了关注信息
--共同关注
set集合的intersect的功能
--基于推模式实现关注推送功能
实现feed流的两张模式,三种方案

sortedset VS list
feed流中的数据在不断地变化,数据的角标也在变化,因此不能采用传统分页,会重复读到
根据sortedset实现滚动分页
A:第一次访问时返回的lastid和offset,第二次访问时如何拼接到查询字符串?
A:第一次访问时lastid是什么?当前时间戳
Q:都是前端负责的


浙公网安备 33010602011771号