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消息队列实现异步秒杀

问题:

  1. 大量秒杀单场景下容易超出阻塞队列的内存上限;
  2. 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:都是前端负责的

posted @ 2025-05-19 21:21  huhulahu  阅读(9)  评论(0)    收藏  举报