票据存储方案:内存、Redis、数据库与传输方式

票据存哪儿?怎么传?这俩问题看似简单,但选错了,后面全是坑。

我见过不少项目,一开始图省事全放内存,结果一重启用户全得重新登录。也见过把票据塞进 Cookie 结果超 4KB 直接丢数据的。今天咱们就把四种存储方案和两种传输方式掰开揉碎讲清楚。

一、四种存储方案的取舍

先看一张对比表,心里有个全局印象:

方案 读写速度 持久性 容量 典型场景
内存存储 极快(纳秒级) 无(进程重启即丢) 受限于 JVM 堆 单机开发、原型验证
Redis 缓存 快(微秒级) 可配置(RDB/AOF) 取决于内存大小 分布式会话、高并发
数据库持久化 慢(毫秒级) 强(磁盘存储) 几乎无限 审计日志、长周期票据
Cookie/Header 无存储开销 客户端持有 4KB(Cookie 限制) 无状态服务、JWT

二、内存存储:最快但最脆弱

说白了就是把票据放进程里的 HashMap 或 ConcurrentHashMap。我刚开始写项目时就这么干,简单粗暴。

优点:零网络开销,读写速度碾压一切外部存储。

缺点:进程重启、Full GC、甚至代码热更新都能让所有票据瞬间蒸发。

举个例子:

// 内存存储票据(仅适用于单机开发)
public class InMemoryTicketStore {
    private final ConcurrentHashMap<String, Ticket> store = new ConcurrentHashMap<>();

    public void save(String sessionId, Ticket ticket) {
        store.put(sessionId, ticket);
    }

    public Ticket get(String sessionId) {
        return store.get(sessionId);
    }
}

嗯,这里要注意——千万别在生产环境这么干。我曾经有个同事,上线前忘了切 Redis,结果半夜发布重启,全公司两千人同时掉线。那场面,啧啧。

三、Redis 缓存:生产环境的首选

Redis 为什么香?因为它把「快」和「持久」平衡得最好。

我个人习惯用 Redis 存票据时,会做三件事:

  • 设置 TTL:票据过期时间精确到秒,避免僵尸会话堆积
  • 开启 RDB 持久化:至少每 5 分钟落盘一次,重启不丢太多数据
  • 使用 Hash 结构:一个会话 ID 对应一个 Hash,字段放票据属性
// Redis 存储票据(推荐生产使用)
redisTemplate.opsForHash().put("session:" + sessionId, "ticket", ticketJson);
redisTemplate.expire("session:" + sessionId, 30, TimeUnit.MINUTES);

小技巧:Redis 的 Key 设计建议加前缀(如 session:),方便后续用 SCAN 命令批量清理过期会话。

你想想看,如果 Redis 挂了怎么办?我建议做 Redis 哨兵或集群,至少保证主从切换时票据不丢。当然,如果业务允许短暂掉线,单机 Redis + 持久化也够用。

四、数据库持久化:最后的保险

数据库存票据,速度确实慢,但胜在「稳」。适合那些需要长期追溯的场景,比如金融系统的操作审计、7 天前的会话恢复。

表结构大概长这样:

CREATE TABLE session_ticket (
    session_id   VARCHAR(64) PRIMARY KEY,
    ticket_data  TEXT NOT NULL,
    created_at   DATETIME NOT NULL,
    expires_at   DATETIME NOT NULL,
    INDEX idx_expires (expires_at)
);

注意加 expires_at 索引,不然清理过期数据时全表扫描,能把数据库拖死。我见过一个项目,没加索引,每天凌晨跑清理任务时 CPU 直接飙到 100%。

避坑指南:数据库存储票据时,ticket_data 字段建议用 TEXT 或 JSON 类型,别用 VARCHAR(255)。JWT 动辄几百字节,加上其他属性很容易超长。

五、Cookie vs Header 传输

存好了怎么传?两种主流方式,各有各的脾气。

5.1 Cookie 传输

浏览器自动带,开发者省事。但限制也很明显:

  • 大小限制:单个 Cookie 不超过 4KB,整个域名下所有 Cookie 加起来不超过 20KB
  • 安全风险:容易被 CSRF 攻击,必须设置 HttpOnly 和 SameSite
  • 跨域问题:默认不跨域,需要额外配置 Domain 属性
// 设置 Cookie(安全配置)
response.addCookie(new Cookie("SESSION_ID", sessionId) {{
    setHttpOnly(true);
    setSecure(true);
    setPath("/");
    setMaxAge(1800); // 30 分钟
}});

5.2 Header 传输

最常用的是 Authorization: Bearer <token>。优点很明显:

  • 无大小限制(理论上,但别太夸张)
  • 跨域友好:前端通过 fetch 或 axios 自由设置
  • 更安全:不会自动附加,需要显式携带
// 前端携带 Header
fetch('/api/data', {
    headers: {
        'Authorization': 'Bearer ' + token
    }
});

我的建议:新项目优先用 Header 传输。Cookie 虽然方便,但 CSRF、跨域、大小限制这些坑,踩一个就够你折腾半天。

六、整体架构图

下面这张图展示了票据从生成到验证的完整流转路径:

票据存储与传输架构图 用户客户端 应用服务器 内存存储 Redis 缓存 数据库 Cookie 传输 Header 传输 核心原则 生产环境:Redis 为主 + 数据库兜底 传输方式:优先 Header,Cookie 需加强安全配置 登录请求 写入 写入/读取 持久化 Set-Cookie Authorization

七、选型建议

最后给个实在的结论:

  • 开发环境:内存存储 + Cookie,怎么简单怎么来
  • 生产环境:Redis 缓存 + Header 传输,数据库做冷备
  • 金融/审计场景:数据库持久化必须上,Redis 做热缓存加速
  • 微服务架构:统一走 Redis,所有服务共享票据状态

个人经验:我一般会做两层存储——Redis 扛 99% 的读写,数据库异步落盘做灾备。这样既保证了性能,又不怕 Redis 宕机丢数据。

票据存储没有银弹,关键看你的业务对「快」和「稳」的容忍度。选对了,后面会话恢复、分布式扩展都顺风顺水;选错了,光排查问题就能耗掉你半天。