01cookie、session、token的区别,以及使用场景 (为什么要用cookie、session、token,什么时候用)
一、基本概念
1. Cookie
- 定义:由服务器发送给客户端(浏览器)的一小段数据,浏览器会自动保存并在后续请求中自动携带。
- 存储位置:客户端(浏览器)。
- 生命周期:可设置过期时间(持久 Cookie)或仅在当前会话有效(会话 Cookie)。
- 安全性:可通过
HttpOnly、Secure、SameSite等属性增强安全。
2. Session
- 定义:服务器端保存的用户会话数据,通常通过一个唯一 ID(Session ID)与客户端关联。
- 存储位置:服务器(如内存、Redis、数据库等)。
- 关联方式:Session ID 一般通过 Cookie 传递(也可通过 URL 参数)。
- 生命周期:由服务器控制,可设置超时时间。
3. Token(通常指 JWT)
- 定义:一种自包含的令牌(如 JSON Web Token),包含用户信息和签名,无需服务器存储状态。
- 存储位置:客户端(可存在 Cookie、localStorage、sessionStorage 等)。
- 结构:Header + Payload + Signature,Payload 可包含用户 ID、角色、过期时间等。
- 无状态:服务器无需保存 Token,只需验证签名即可。
二、核心区别对比
| 特性 | Cookie | Session | Token (JWT) |
|---|---|---|---|
| 存储位置 | 客户端 | 服务器 | 客户端 |
| 是否需要服务器存储 | 否 | 是 | 否(无状态) |
| 自动携带 | 是(同域下自动随请求发送) | 依赖 Cookie 或 URL | 否(需手动添加到请求头) |
| 安全性 | 中(易受 CSRF 攻击) | 高(数据在服务端) | 高(但需防 XSS) |
| 跨域支持 | 困难(受同源策略限制) |
困难 | 容易(适合前后端分离/跨域) |
| 扩展性 | 差(依赖服务器状态) | 差(需共享 Session) | 好(天然支持分布式) |
| 生命周期管理 | 可设过期时间 | 服务端控制 | Token 内置过期时间 |
同源:
协议、域名、端口完全相同,即非跨域
三、为什么需要它们?
Web 协议 HTTP 是无状态的,每次请求都是独立的。为了实现“记住用户登录状态”、“个性化内容”等功能,就需要某种机制来维持状态或识别用户身份。
- Cookie:解决客户端如何携带标识的问题。
- Session:解决服务器如何记住用户状态的问题。
- Token:解决在无状态架构(如 RESTful API、微服务)中如何安全认证的问题。
四、使用场景建议
使用 Cookie + Session 的场景:
- 传统 Web 应用(如 PHP、Java Web、Django 等服务端渲染应用)
- 用户登录后跳转页面,保持登录状态
- 对安全性要求高,且不涉及跨域
- 例如:电商后台、企业管理系统
优点:简单、成熟、自动管理;
缺点:难以扩展到分布式系统,跨域麻烦。
使用 Token(如 JWT)的场景:
- 前后端分离架构(React/Vue + RESTful API)
- 移动端 App / 小程序 与后端通信
- 微服务架构,需要无状态认证
- 跨域或第三方集成(如 OAuth2)
jti(JWT ID)是 JWT(JSON Web Token)中的一个可选标准声明字段,用于唯一标识一个具体的 Token 实例。它的核心作用是:让每个签发的Token 都有一个“身份证号”,便于后续追踪、吊销或审计。可以把jti储存在Redis中,作为黑名单
优点:无状态、易扩展、跨平台;
缺点:Token 一旦签发无法主动失效(除非引入黑名单),Payload 不宜过大。
⚠️ 注意事项
- CSRF(跨站请求伪造):Cookie + Session 易受攻击,需用
SameSite=Strict/Lax或 CSRF Token 防护。 - XSS(跨站脚本):若 Token 存在 localStorage,可能被 JS 窃取;建议存入
HttpOnly Cookie并配合SameSite。 - 敏感信息:不要在 Cookie 或 Token Payload 中存放密码等敏感数据。
02.Redis能干点啥
Redis 的核心作用(按使用场景分类)
1. 缓存(Cache)—— 最常见用途
- 目的:减轻数据库压力,提升系统响应速度。
- 原理:将热点数据(如用户信息、商品详情、文章内容)存入 Redis,后续请求直接读缓存,避免查数据库。
- 典型场景:
- Web 页面缓存
- API 接口结果缓存
- 数据库查询结果缓存(如 MyBatis + Redis)
- 优势:读写性能极高(10万+ QPS),延迟微秒级。
💡 示例:用户访问商品页 → 先查 Redis → 有则返回,无则查 DB 并写入 Redis。
2. 分布式会话存储(Session Store)
- 问题:传统 Session 存在单机内存中,无法用于负载均衡下的多台服务器。
- 解决方案:将 Session 数据(如用户登录状态)统一存到 Redis。
- 效果:任意后端节点都能读取用户会话,实现无状态服务 + 水平扩展。
✅ 常用于:Java(Spring Session + Redis)、Node.js、PHP 等 Web 应用。
3. 分布式锁(Distributed Lock)
- 目的:在分布式系统中,保证多个进程/服务对共享资源的互斥访问。
- 实现方式:利用
SET key value NX EX命令(原子操作)。 - 典型场景:
- 秒杀库存扣减
- 防止重复提交订单
- 定时任务只在一个节点执行
⚠️ 注意:需处理锁过期、续期、误删等问题(可结合 Redlock 或 Lua 脚本)。
4. 消息队列(Message Queue)—— 轻量级
- 数据结构:使用
List(LPUSH + BRPOP)或Stream(Redis 5.0+)。 - 适用场景:
- 异步任务处理(如发邮件、日志收集)
- 解耦生产者和消费者
- 优点:简单、快速、无需引入 Kafka/RabbitMQ
- 缺点:功能不如专业 MQ(无 ACK、持久化弱、无路由)
✅ 适合中小型项目或临时任务队列。
5. 计数器 & 限流(Rate Limiting)
- 数据结构:
String(INCR)或ZSet(滑动窗口) - 典型应用:
- 接口调用次数统计
- 用户登录失败次数限制
- 短信发送频率控制
- 分布式 ID 生成(INCR)
💡 示例:
INCR user:123:login_attempts+EXPIRE实现 5 分钟内最多试错 5 次。
6. 排行榜 & 实时统计(Leaderboard)
- 数据结构:
Sorted Set(ZSet) - 原理:成员(member)+ 分数(score),自动排序。
- 场景:
- 游戏积分榜
- 热门文章 Top 10
- 实时销售额排名
✅ 支持高效插入、更新、范围查询(如
ZREVRANGE)。
7. 发布/订阅(Pub/Sub)—— 实时通信
- 模式:消息广播机制
- 命令:
PUBLISH channel msg/SUBSCRIBE channel - 应用场景:
- 聊天室消息推送
- 系统通知广播
- 微服务间简单事件通知
⚠️ 缺点:消息不持久(订阅者离线会丢消息),不适合可靠消息场景。
8. 位图(Bitmap)与布隆过滤器(Bloom Filter)
- Bitmap:用 bit 位存储状态,节省空间
→ 如:用户签到(SETBIT user:123:sign 20251110 1) - Bloom Filter(需 RedisBloom 模块):
→ 高效判断“某元素可能存在”(如防止缓存穿透)
9. 地理位置(GEO)
- 命令:
GEOADD、GEORADIUS - 场景:附近的人、门店搜索
- 底层:基于 Sorted Set + GeoHash
10. 作为 JWT 黑名单/白名单存储(认证控制)
- 存储被吊销的 Token ID(
jti) - 控制用户登出、权限变更等实时生效
03.hashmap,hashtable,concurrenthashmap的区别
| 特性 | HashMap |
Hashtable |
ConcurrentHashMap |
|---|---|---|---|
| 线程安全 | ❌ 不安全 | ✅ 安全(全表锁) | ✅ 安全(分段锁 / CAS + synchronized) |
| null 键/值 | ✅ 允许一个 null key,多个 null value |
❌ 不允许 null key 或 value |
❌ 不允许 null key 或 value |
| 底层结构 | 数组 + 链表 + 红黑树(JDK 8+) | 数组 + 链表 | 数组 + 链表 + 红黑树(JDK 8+) |
| 锁粒度 | 无锁 | 方法级 synchronized(整表锁) | 分段锁(JDK 7)→ CAS + synchronized(JDK 8+,锁单个桶) |
| 性能(并发) | 最高(无锁) | 最低(高并发下严重阻塞) | 高(并发读不加锁,写只锁部分) |
| 迭代器 | fail-fast | fail-fast | 弱一致性(不会抛 ConcurrentModificationException) |
| 继承关系 | AbstractMap |
Dictionary(已废弃) |
AbstractMap |
04rabbitmq消息有序性
保证同一业务实体(如订单ID、用户ID)的消息处理顺序,不同实体之间可并行处理。
🔑 核心思想:分区(Partitioning) + 单线程消费
| 原则 | 说明 |
|---|---|
| 1. 同一业务 ID 的消息必须进入同一个队列 | 通过 hash(业务ID) % N 路由到固定队列 |
| 2. 每个队列只由一个消费者(单线程)处理 | 避免并发打乱顺序 |
| 3. RabbitMQ 队列本身是 FIFO | 入队顺序 = 出队顺序,天然保序 |
🛠️ 实现步骤
预创建多个队列(如
order.queue.0~order.queue.3)生产者按业务 ID 路由:
1
2int index = Math.abs(orderId.hashCode()) % queueCount;
channel.basicPublish("", "order.queue." + index, ..., message);每个队列绑定一个单线程消费者(禁止多线程并发消费同一队列)
消费者关闭 prefetch,手动 ACK:
1
2channel.basicQos(1); // 每次只取1条
// 处理成功后再 basicAck
⚠️ 关键前提(缺一不可)
| 环节 | 要求 |
|---|---|
| 生产者 | 同一业务 ID 的消息必须按序发送(避免并发发同一流消息) |
| Broker | 队列 FIFO 特性保证入队顺序 |
| 消费者 | 单线程处理 + 不 requeue 到队首(失败应进死信队列) |
05.什么时候用rabbitmq
1. 应用解耦(Decoupling)
- 问题:模块 A 直接调用模块 B,B 挂了 → A 也失败。
- RabbitMQ 方案:A 发消息到队列,B 异步消费。
- 例子:
- 用户注册后,发邮件、发短信、写日志 → 各自独立消费
- 订单创建后,通知库存、物流、积分系统
🎯 价值:系统间松耦合,故障隔离。
2. 异步处理(Asynchronous Processing)
- 问题:用户点击“下单”,要等 2 秒(因为同步调支付、发短信…),体验差。
- RabbitMQ 方案:下单成功后发消息,后台慢慢处理。
- 效果:接口响应从 2s → 50ms。
🎯 价值:提升用户体验,降低请求链路延迟。
3. 流量削峰(Traffic Shaping / Peak Shaving)
- 问题:秒杀活动瞬间 10 万请求,数据库扛不住。
- RabbitMQ 方案:请求先入队列,消费者按能力匀速处理(比如每秒 1000 单)。
- 注意:需配合限流 + 排队提示(“正在排队中…”)
🎯 价值:保护后端系统不被冲垮。
4. 最终一致性(Eventual Consistency)
- 问题:分布式事务太重(如 Seata),但又需要数据一致。
- RabbitMQ 方案:通过“事件驱动”实现补偿。
- 例:扣款成功 → 发
PaymentSuccessEvent→ 库存服务消费并减库存 - 失败则重试 or 进死信队列人工干预
- 例:扣款成功 → 发
🎯 价值:轻量级实现跨服务数据同步。
5. 广播通知(Pub/Sub)
- 问题:一个事件要通知多个系统(如“用户注销”要清空缓存、踢掉 WebSocket、记录审计日志)。
- RabbitMQ 方案:用 Exchange + 多个 Queue 绑定,实现一对多广播。
🎯 价值:避免硬编码通知列表。
❌ 二、不适合使用 RbbitMQ 的场景
| 场景 | 原因 | 更佳选择 |
|---|---|---|
| 需要强顺序(全局有序) | RabbitMQ 不天然支持 | Kafka / RocketMQ |
| 需要消息回溯(重复消费历史消息) | 队列消费后即删除 | Kafka(持久化日志) |
| 超大消息(> 10MB) | 内存压力大,性能差 | 文件存储 + 消息传 URL |
| 低延迟要求(< 10ms) | 网络 + Broker 开销 | 直接 RPC / gRPC |
| 简单定时任务 | 杀鸡用牛刀 | Quartz / XXL-JOB |
| 高吞吐日志收集(每秒百万条) | 性能不如专用系统 | Kafka / Fluentd |
06.秒杀实现
🎯 核心目标
- 高性能:扛住瞬时高并发(万级 QPS)
- 一致性:不能超卖(卖出去的 ≤ 实际库存)
- 可靠性:即使系统崩溃,数据最终一致
阶段 1️⃣:库存预热 —— 从数据库加载到 Redis
⏰ 时机
- 秒杀开始前(比如提前 5 分钟、1 小时,或商品上架时)
- 不能在秒杀请求到来时才查 DB(会压垮数据库)
💡 做什么
将商品的可售库存写入 Redis,作为秒杀的“令牌池”。
✅ 示例
1 | -- 数据库商品表 |
1 | # 写入 Redis(Key 设计很重要!) |
📌 关键点:
- Redis 中的库存 = 本次秒杀可售数量(可能是总库存,也可能是单独配置的秒杀库存)
- 通常用 String 类型(方便
DECR原子操作)
阶段 2️⃣:秒杀扣减 —— 在 Redis 中原子操作
🚀 用户点击“抢购”时
后端只做一件事:原子扣减 Redis 库存
1 | // Java + Lettuce / Jedis 示例 |
⚠️ 为什么安全?
DECR是 Redis 单线程原子操作,天然防并发超卖- 即使 10 万人同时请求,Redis 也能保证最多 100 人成功
📌 注意:
有些系统会用GET + SET或 Lua 脚本做更复杂的校验(如每人限购),但核心仍是原子性。
阶段 3️⃣:异步落库 —— 订单创建 & 数据库库存扣减
📦 关键思想:“先抢资格,后建订单”
当 Redis 扣减成功后:
立即返回用户:“抢购成功,请支付”
同时发送一条消息到 RabbitMQ/Kafka,内容如:
1
2
3
4
5{
"userId": 123,
"productId": 1001,
"orderId": "O20251110123456"
}
🔄 后台消费者处理消息(异步)
1 | // 消费者收到消息 |
❓ 你可能会问:数据库库存还要扣吗?
这取决于你的库存模型:
| 方案 | 说明 | 是否需要扣 DB 库存 |
|---|---|---|
| ✅ 纯 Redis 库存 | 秒杀库存独立于 DB,DB 只存“总库存” | ❌ 不需要(但需对账) |
| 🔄 Redis + DB 双写 | Redis 是缓存,DB 是权威 | ✅ 需要(最终一致) |
| 🧩 分离库存 | DB 存“总库存”,Redis 存“秒杀专用库存” | ❌ 秒杀库存扣完即可,DB 总库存定期同步 |
💡 主流做法:
秒杀用独立库存(Redis),和普通销售库存分开。
秒杀结束后,再把“已售数量”同步到 DB 做汇总。
🔐 如何保证“不超卖 + 不少卖”?
| 风险 | 解决方案 |
|---|---|
| Redis 扣了,但订单创建失败 | 消息队列重试 + 幂等处理(订单 ID 唯一) |
| 重复消费导致多扣库存 | 消费者做幂等(如用订单 ID 去重) |
| Redis 崩溃丢失库存 | 持久化(AOF)+ 高可用(哨兵/集群) |
| DB 和 Redis 不一致 | 定时对账任务(补偿) |
🛡️ 终极保障:
即使中间环节出错,对账系统会在事后修复数据(比如发现 Redis 卖了 100,DB 只扣了 98 → 补扣 2)。
🌰 完整流程示例(以 100 件商品为例)
- 预热:
SET stock:1001 100 - 秒杀开始:
- 用户 A 请求 →
DECR→ 返回 99 ≥ 0 → 成功 - 用户 B 请求 →
DECR→ 返回 98 ≥ 0 → 成功 - …
- 第 100 个用户 →
DECR→ 返回 0 ≥ 0 → 成功 - 第 101 个用户 →
DECR→ 返回 -1 < 0 → 失败
- 用户 A 请求 →
- 异步:100 条消息进入 MQ,订单服务慢慢创建 100 个订单
- 结束:DB 中该商品“秒杀已售”字段更新为 100
✅ 总结:秒杀库存流转三原则
| 阶段 | 原则 |
|---|---|
| 预热 | 提前加载,绝不现场查 DB |
| 扣减 | Redis 原子操作,保证不超卖 |
| 落库 | 异步处理,通过 MQ 解耦 + 幂等保证最终一致 |
07.spring的事务传递性
方法 A 调用方法 B,A 有事务,B 也有
@Transactional,
那 B 是加入 A 的事务?还是开启新事务?还是不支持事务?
答案由 @Transactional(propagation = ...) 中的 传播行为(Propagation) 决定。
✅ Spring 提供的 7 种事务传播行为
| 传播行为 | 说明 | 类比理解 |
|---|---|---|
REQUIRED(默认) |
如果当前存在事务,则加入;否则新建一个事务 | “有群就进,没群就建” |
SUPPORTS |
如果当前存在事务,则加入;否则以非事务方式执行 | “有群就聊,没群就算了” |
MANDATORY |
必须在已有事务中执行,否则抛异常 | “必须在群里说话,否则禁言!” |
REQUIRES_NEW |
无论当前是否有事务,都挂起当前事务(如果有),创建新事务 | “我要单独开个小会,原会议暂停” |
NOT_SUPPORTED |
以非事务方式执行,如果当前有事务,则挂起它 | “我现在不想开会,请暂停群聊” |
NEVER |
以非事务方式执行,如果当前有事务,则抛异常 | “禁止在群里说话!” |
NESTED |
如果当前存在事务,则在嵌套事务内执行;否则行为同 REQUIRED |
“在大会议里开个子议题,可单独回滚” |
Java 方法参数传递机制
Java 中所有的参数传递都是 值传递(Pass-by-Value),无论是基本类型还是引用类型。
- 对于 基本类型(如
int、boolean),传递的是实际值的副本。方法内部对参数的修改不会影响原始变量。 - 对于 引用类型(如对象、数组),传递的是 对象引用的副本(即堆中对象地址的拷贝)。这个副本和原引用指向同一个对象(储存在堆中),因此通过它修改对象的属性,会影响原对象;但如果在方法内让引用指向一个新对象(比如
obj = new Xxx()),则不会影响原来的引用。
本质上,Java 没有‘按引用传递’。之所以对象属性能被修改,是因为多个引用共享同一个堆对象,而不是因为传参机制不同。”
在JVM中,Java 方法调用时,实参的“值”被复制到被调用方法的局部变量表中。
- 基本类型的“值” = 数据本身(如
int=5) - 引用类型的“值” = 对象在堆中的地址(即 Oop 指针,例如
0x000000076ab23450)
JVM 不会传递变量本身,也不会传递对象本身,只传递“值”的副本。