面试题解一

88 阅读49分钟

001.jpg

1. IOC&AOP概念?IOC好处?IOC和DI的关系?AOP原理?

1.1_IOC(控制反转)概念

IOC(Inversion of Control)  是一种设计原则,将对象的创建、依赖管理和生命周期控制从应用程序代码中剥离,交给容器(如 Spring 框架)统一管理。传统开发中,对象直接控制依赖的创建(如 new 关键字),而 IOC 通过“反转”这一控制权,由容器负责对象的组装和依赖注入。

1.2_IOC的好处

①解耦(降低耦合度)与模块化
对象无需自行管理依赖,而是通过容器注入,减少类之间的直接依赖,实现“面向接口编程”。

// 传统方式(高耦合)
class ServiceA {
    private ServiceB serviceB = new ServiceB(); // 直接依赖具体实现
}

// IOC 方式(低耦合)
class ServiceA {
    private ServiceB serviceB; // 通过容器注入接口,无需关心实现
}

②提高可维护性
依赖关系集中配置(如 XML 或注解),修改依赖时无需改动业务代码。
③增强可测试性
依赖可替换为 Mock 对象,便于单元测试。

@Test
void testServiceA() {
    ServiceB mockB = mock(ServiceB.class);
    ServiceA serviceA = new ServiceA(mockB); // 注入 Mock 对象
}

④统一生命周期管理
容器管理对象的单例、原型等作用域,避免资源泄漏。

1.3_IOC 和 DI 的关系
  • IOC 是设计原则:强调控制权的反转,将对象创建和依赖管理的责任交给容器。
  • DI(依赖注入)是 IOC 的实现方式:通过构造函数、Setter 方法或接口注入依赖。
  • 关系总结:IOC 是目标,DI 是手段。其他实现方式还包括“服务定位器模式”,但 DI 是主流。
1.4_AOP(面向切面编程)原理

AOP 的核心:将横切关注点(如日志、事务、权限)从业务逻辑中分离,通过动态代理或字节码增强实现功能织入。

实现原理

动态代理(JDK / CGLIB)

  • JDK 动态代理:基于接口,运行时生成代理类(要求目标类实现接口)。
  • CGLIB 代理:通过继承目标类生成子类代理(适用于无接口的类)。
// JDK 动态代理示例
public class LogProxy implements InvocationHandler {
    private Object target;
    public Object createProxy(Object target) {
        this.target = target;
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), 
            target.getClass().getInterfaces(), this);
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Log before method");
        return method.invoke(target, args);
    }
}

织入(Weaving)时机

  • 编译时织入:在编译阶段修改字节码(如 AspectJ)。
  • 运行时织入:在运行时动态生成代理对象(如 Spring AOP)。

核心概念

  • 切面(Aspect) :横切关注点的模块化(如日志切面)。
  • 连接点(Join Point) :可插入切面的方法执行点(如方法调用)。
  • 切点(Pointcut) :定义哪些连接点会被拦截(通过表达式匹配)。
  • 通知(Advice) :在切点执行的逻辑(如 @Before@Around)。

2. MySQL事务的隔离级别?

MySQL事务的隔离级别详解

一、事务隔离级别的定义

事务隔离级别定义了多个并发事务之间的可见性和影响范围,用于解决数据库并发操作中的一致性问题。SQL标准定义了四种隔离级别,从低到高依次为:

  1. 读未提交(Read Uncommitted)
  2. 读已提交(Read Committed)
  3. 可重复读(Repeatable Read)
  4. 串行化(Serializable)

隔离级别越高,数据一致性越强,但并发性能越低。


二、四种隔离级别解决的问题

隔离级别脏读(Dirty Read)不可重复读(Non-Repeatable Read)幻读(Phantom Read)
读未提交(RU)❌ 可能发生❌ 可能发生❌ 可能发生
读已提交(RC)✅ 避免❌ 可能发生❌ 可能发生
可重复读(RR)✅ 避免✅ 避免❌ 可能发生(部分避免)
串行化(Serializable)✅ 避免✅ 避免✅ 避免
  • 脏读:一个事务读取到其他事务未提交的数据。
  • 不可重复读:同一事务中多次读取同一数据,结果不一致(因其他事务修改了数据)。
  • 幻读:同一事务中多次范围查询,结果集数量不一致(因其他事务插入或删除了数据)。

三、MySQL的默认隔离级别

  • InnoDB存储引擎的默认隔离级别是 可重复读(Repeatable Read)
  • 特殊机制:InnoDB通过 多版本并发控制(MVCC)  和 间隙锁(Gap Lock) ,在 可重复读 级别下也能 避免大部分幻读

四、各隔离级别的实现原理

  1. 读未提交(RU)

    • 原理:直接读取最新数据(包括其他事务未提交的数据)。
    • 问题:所有并发问题均可能发生。
  2. 读已提交(RC)

    • 原理:每次读取时生成一个快照,仅读取已提交的数据。
    • 问题:同一事务多次查询可能看到其他事务已提交的修改(不可重复读)。
  3. 可重复读(RR)

    • 原理:事务开始时生成一个全局快照,后续读取均基于此快照。

    • 幻读的解决

      • MVCC:通过版本链保证读取历史版本数据。
      • 间隙锁:锁定范围条件(如 WHERE id > 100),阻止其他事务插入范围内数据。
  4. 串行化(Serializable)

    • 原理:所有事务串行执行,通过加锁(读锁+写锁)彻底避免并发问题。
    • 代价:性能极低,仅用于严格要求一致性的场景。

五、如何设置与查看隔离级别

  1. 查看当前隔离级别

    -- MySQL 5.7及之前
    SELECT @@tx_isolation;
    
    -- MySQL 8.0及之后
    SELECT @@transaction_isolation;
    
  2. 设置会话/全局隔离级别

    -- 设置会话级隔离级别(仅当前连接有效)
    SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
    
    -- 设置全局隔离级别(需重启生效)
    SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
    

六、使用建议

  1. 默认推荐:使用 可重复读(RR),在保证一致性的同时避免大部分幻读。
  2. 高并发场景:可降级为 读已提交(RC),减少锁竞争(如电商系统)。
  3. 严格一致性场景:使用 串行化(如金融交易系统)。

七、示例演示

-- 事务A(更新数据)
START TRANSACTION;
UPDATE users SET balance = balance - 100 WHERE id = 1;

-- 事务B(不同隔离级别的表现)
-- 若隔离级别为 RU:事务B会读取到事务A未提交的余额变化(脏读)。
-- 若隔离级别为 RC:事务B需等待事务A提交后才能看到变化(避免脏读)。
-- 若隔离级别为 RR:事务B在事务A提交前后读取的余额一致(可重复读)。

总结

  • MySQL默认隔离级别为 可重复读,通过MVCC和间隙锁平衡一致性与性能。
  • 选择隔离级别需权衡:一致性要求 vs 并发性能。
  • 实际开发中:优先使用默认的 RR,必要时调整为 RC 或 Serializable

3. 脏读、不可重复读、幻读?不回滚会导致脏读问题吗?只事务中修改一次会导致不可重复读吗?幻读具体场景?

一、核心概念与区别

  1. 脏读(Dirty Read)

    • 定义:一个事务读取了另一个事务尚未提交的数据。
    • 示例
      • 事务A修改了数据X(未提交),事务B读取了X的新值。
      • 若事务A最终回滚,事务B读取到的X值就是无效的“脏数据”。
    • 关键点:脏读关注的是读取未提交的数据,与事务最终是否回滚无关。
      • 即使事务A提交了,只要事务B在事务A提交前读取了未提交的数据,仍属于脏读。
  2. 不可重复读(Non-Repeatable Read)

    • 定义:同一事务中多次读取同一数据,结果不一致(因其他事务修改了该数据并提交)。
    • 示例
      • 事务A第一次读取X=100,事务B修改X=200并提交。
      • 事务A再次读取X=200,两次结果不一致。
  3. 幻读(Phantom Read)

    • 定义:同一事务中多次范围查询,结果集数量不一致(因其他事务插入或删除了数据)。
    • 示例
      • 事务A查询年龄>30的用户(返回5条),事务B插入一条年龄>30的用户并提交。
      • 事务A再次查询,返回6条,结果集出现“幻影行”。

问题类型定义触发原因示例场景
脏读(Dirty Read)事务A读取了事务B 未提交 的数据。事务B未提交时,事务A读取中间状态事务B修改数据X=200(未提交),事务A读取X=200,之后事务B回滚,X恢复为原值。
不可重复读(Non-Repeatable Read)事务A多次读取同一数据,结果不一致(因事务B 修改并提交 了该数据)。数据被其他事务修改且提交事务A第一次读取X=100,事务B修改X=200并提交,事务A第二次读取X=200。
幻读(Phantom Read)事务A多次范围查询,结果集数量不一致(因事务B 插入/删除并提交 了数据)。数据被其他事务新增或删除且提交事务A查询年龄>30的用户(返回5条),事务B插入一个年龄>30的用户并提交,事务A再次查询返回6条。

二、关键问题解析

1. 不回滚是否会导致脏读问题?
  • 结论:脏读的发生与事务是否回滚无关,关键在于是否读取了 未提交的数据
    • 即使事务B最终提交,只要事务A在事务B提交前读取了未提交的数据,仍属于脏读。
    • 示例
      -- 事务B修改数据(未提交)
      START TRANSACTION;
      UPDATE users SET balance = 200 WHERE id = 1;
      
      -- 事务A(隔离级别为读未提交)读取未提交数据
      SELECT balance FROM users WHERE id = 1;  -- 读到200(脏读)
      
      -- 事务B提交
      COMMIT;
      
      • 事务A的读取操作在事务B提交前完成,即使事务B最终提交,事务A的读取仍为脏读。
2. 仅一次修改是否会导致不可重复读?
  • 结论:是的,只要其他事务对数据进行了 修改并提交,就会导致不可重复读。
    • 示例
      -- 事务A第一次读取
      SELECT balance FROM users WHERE id = 1;  -- 结果100
      
      -- 事务B修改并提交
      UPDATE users SET balance = 200 WHERE id = 1;
      COMMIT;
      
      -- 事务A第二次读取
      SELECT balance FROM users WHERE id = 1;  -- 结果200(不可重复读)
      
3. 幻读的具体场景
  • 场景示例
    -- 事务A第一次查询年龄>30的用户
    SELECT * FROM users WHERE age > 30;  -- 返回5条记录
    
    -- 事务B插入新用户并提交
    INSERT INTO users (name, age) VALUES ('John', 35);
    COMMIT;
    
    -- 事务A第二次查询年龄>30的用户
    SELECT * FROM users WHERE age > 30;  -- 返回6条记录(幻读)
    
  • 本质原因:其他事务插入或删除了符合当前事务查询条件的数据,导致结果集数量变化。

三、隔离级别与问题的解决

隔离级别脏读不可重复读幻读
读未提交(RU)❌ 允许❌ 允许❌ 允许
读已提交(RC)✅ 避免❌ 允许❌ 允许
可重复读(RR)✅ 避免✅ 避免⚠️ 部分避免(InnoDB避免)
串行化(Serializable)✅ 避免✅ 避免✅ 避免
  • MySQL的特殊性
    • 默认隔离级别为 可重复读(RR),通过 MVCC(多版本并发控制)间隙锁(Gap Lock) 避免了大部分幻读。
    • 间隙锁:锁定范围条件(如 WHERE age > 30),阻止其他事务插入范围内数据。

四、脏读的实际影响

  • 若事务A回滚:事务B基于脏数据所做的操作将导致数据不一致(例如扣款错误)。
  • 若事务A提交:事务B读取的数据最终是正确的,但破坏了事务隔离性(允许读取中间状态)。
  • 总结
    • 脏读的本质问题是 暴露了事务的中间状态,无论事务A是否回滚,均违反了隔离性要求。

五、如何避免这些问题?

  1. 选择合适的隔离级别

    • 使用 读已提交(RC)可重复读(RR) 隔离级别,避免脏读。
    • MySQL默认隔离级别为 可重复读(RR),通过MVCC和间隙锁避免大部分幻读。
  2. 代码设计优化

    • 在事务中尽量减少长时间操作,降低并发冲突概率。
    • 对关键数据使用悲观锁(如 SELECT ... FOR UPDATE)。

六、总结

  • 脏读:读取未提交数据(与事务是否回滚无关)。
  • 不可重复读:同一数据多次读取结果不一致(其他事务修改数据)。
  • 幻读:范围查询结果集数量变化(其他事务插入/删除数据)。
  • 不回滚的影响:即使事务A提交,事务B的脏读依然存在(违反隔离性)。

实际开发建议

  • 默认使用 可重复读(RR) 隔离级别(MySQL默认)。
  • 高并发场景可降级为 读已提交(RC),平衡性能与一致性。

4. Innodb引擎的默认隔离级别?怎么实现的?MVCC机制?MVCC一条记录有几个版本?

一、InnoDB的默认隔离级别

  • 默认隔离级别可重复读(Repeatable Read, RR)
    • 这是MySQL的默认配置,与SQL标准的默认隔离级别(通常是读已提交)不同。
    • 特点
      • 避免脏读、不可重复读,并通过 间隙锁(Gap Lock)MVCC 机制 部分解决幻读
      • 在大多数场景下,InnoDB的RR级别可以避免幻读,但在极端高并发操作中仍可能发生(如事务中混合使用快照读和当前读)。

二、可重复读的实现原理

InnoDB通过以下两种核心机制实现可重复读隔离级别:

1. MVCC(多版本并发控制)
  • 核心思想:为每条记录维护多个版本,使事务在读取时基于一致性视图(Consistent Read View)访问数据。
  • 实现细节
    • 隐藏字段
      每条记录包含两个隐藏字段:
      • DB_TRX_ID:记录最后一次修改该数据的事务ID。
      • DB_ROLL_PTR:指向该记录上一个版本的指针(通过undo日志构建版本链)。
    • Read View
      • 事务在第一次查询时生成Read View,包含当前活跃事务ID列表。
      • 根据Read View判断记录的哪个版本对当前事务可见(通过比较事务ID与活跃事务状态)。
2. 间隙锁(Gap Lock)
  • 作用:锁定索引记录的范围间隙,防止其他事务插入数据导致幻读。
  • 示例
    -- 事务A执行范围查询(age > 30)
    SELECT * FROM users WHERE age > 30 FOR UPDATE;  -- 间隙锁锁定age>30的范围
    
    -- 事务B尝试插入age=35的数据时会被阻塞
    INSERT INTO users (name, age) VALUES ('John', 35);
    

三、MVCC机制详解

1. 数据版本链
  • 每条记录的版本链:通过DB_ROLL_PTR指针链接到undo日志中的旧版本数据。
  • 示例
    • 事务T1修改数据X=100 → X=200,生成新版本记录(事务ID=T1)。
    • 事务T2再次修改X=200 → X=300,生成新版本记录(事务ID=T2)。
    • 记录的版本链为:X=300 ← X=200 ← X=100。
2. 事务可见性判断
  • 规则
    • 如果记录的DB_TRX_ID小于当前事务ID,且该事务已提交 → 可见。
    • 如果记录的DB_TRX_ID属于活跃事务列表 → 不可见,需沿版本链查找更早的版本。
3. 示例(事务隔离性验证)
-- 事务A(事务ID=100)
START TRANSACTION;
SELECT * FROM users WHERE id=1;  -- 生成Read View,假设此时活跃事务为空

-- 事务B(事务ID=101)修改数据并提交
UPDATE users SET balance=200 WHERE id=1;
COMMIT;

-- 事务A再次查询id=1,仍读取到旧值(Read View一致性保证)
SELECT * FROM users WHERE id=1;  -- 结果不变

四、MVCC中一条记录的版本数量

  • 版本数量:一条记录理论上可以存在 多个版本,具体数量取决于:
    1. 活跃事务的持续时间:已提交事务的旧版本可能被保留,直到没有事务需要访问它们。
    2. undo日志清理机制:InnoDB的后台purge线程会定期清理不再需要的旧版本数据。
  • 典型场景
    • 高并发系统中,多个事务频繁修改同一条数据 → 版本链较长。
    • 长事务的存在会阻止旧版本数据的清理 → 版本链保留时间更长。

五、总结

  • 默认隔离级别:InnoDB使用 可重复读(RR),通过MVCC和间隙锁实现高并发下的数据一致性。
  • MVCC核心
    • 版本链 + Read View → 实现非阻塞一致性读。
    • 每条记录可能有多个版本,由undo日志维护。
  • 注意事项
    • 长事务可能导致版本链过长,影响存储效率。
    • 混合使用快照读(SELECT)和当前读(SELECT ... FOR UPDATE)时仍需注意幻读风险。

示例代码(版本链与Read View)

-- 事务A(ID=100)读取数据
START TRANSACTION;
-- 生成Read View,活跃事务列表为空
SELECT * FROM users WHERE id=1;  -- 读取版本链中最新的可见版本

-- 事务B(ID=101)修改数据并提交
UPDATE users SET balance=200 WHERE id=1;
COMMIT;

-- 事务A再次读取,仍看到旧版本(事务B已提交,但事务A的Read View不包含事务B)
SELECT * FROM users WHERE id=1;  -- 结果不变

通过理解InnoDB的隔离级别和MVCC机制,可以更好地设计高并发场景下的数据库操作,平衡一致性与性能。

5. 讲讲索引?联合索引听过吗?场景(a=1&b>2&c=2)和(a=1||b=2||c=3)索引失效问题?追问用or一定会索引失效吗?怎么看sql是不是走索引了?

一、索引的核心概念

索引的本质:数据库中的“目录”,通过特定数据结构(如B+树)加速数据检索,减少全表扫描。
常见索引类型

  • 单列索引:基于单个字段(如 INDEX(a))。
  • 联合索引(复合索引):基于多个字段组合(如 INDEX(a, b, c)),遵循 最左前缀原则

二、联合索引的工作原理

1. 最左前缀原则
  • 规则:查询条件必须包含联合索引的 最左连续字段,才能命中索引。
  • 示例(假设联合索引为 (a, b, c)):
    • WHERE a=1 → 使用索引。
    • WHERE a=1 AND b=2 → 使用索引。
    • WHERE b=2 → 无法使用索引(未从最左字段开始)。
    • WHERE a=1 AND c=3 → 仅部分使用索引(仅用到字段 ac 无法命中索引)。
2. 联合索引的排序与过滤
  • 排序规则:索引按字段顺序(a → b → c)排序,字段值相同时按下一字段排序。
  • 范围查询中断:若某个字段使用范围查询(如 ><),其后的字段无法使用索引过滤。
    -- 联合索引 (a, b, c)
    WHERE a=1 AND b>2 AND c=3;  
    -- 仅用到 a 和 b 的索引(b 是范围查询,c 无法走索引)
    

三、索引失效场景分析

1. 场景一:WHERE a=1 AND b>2 AND c=2
  • 索引使用情况
    • 若存在联合索引 (a, b, c)
      • a=1 精确匹配 → 使用索引。
      • b>2 范围查询 → 使用索引,但后续字段 c 无法再走索引。
      • 最终效果:索引仅覆盖到 abc 需回表过滤。
    • 优化建议:调整索引顺序为 (a, c, b),利用索引覆盖更多条件。
2. 场景二:WHERE a=1 OR b=2 OR c=3
  • 索引失效原因
    • OR 运算符的特性:要求每个条件独立满足,无法利用联合索引的最左前缀。
    • 可能的索引合并
      • 若存在单列索引 abc,且MySQL开启 index_merge,可能触发 索引合并(Index Merge)
      • 但效率较低:合并多个索引的代价可能高于全表扫描。
    • 常见结果:全表扫描(若未命中单列索引)。
追问:使用 OR 一定会导致索引失效吗?
  • 不一定:若每个 OR 条件均有独立的单列索引,且MySQL启用索引合并,可能走索引。
  • 示例
    -- 单列索引:INDEX(a), INDEX(b), INDEX(c)
    WHERE a=1 OR b=2 OR c=3;  
    -- 可能触发 index_merge_union,合并三个索引(但性能未必优于全表扫描)。
    

四、如何判断 SQL 是否走索引?

1. 使用 EXPLAIN 命令

执行 EXPLAIN SELECT ...,关注以下字段:

  • type:访问类型(性能从高到低):
    • const(唯一索引精确匹配)、ref(普通索引)、range(范围查询)、index(全索引扫描)、ALL(全表扫描)。
  • key:实际使用的索引。
  • rows:预估扫描的行数。
  • Extra:额外信息(如 Using index 表示覆盖索引)。
2. 示例分析
-- 联合索引 (a, b, c)
EXPLAIN SELECT * FROM table WHERE a=1 AND b>2 AND c=3;
  • 结果分析
    • type=range(范围查询)。
    • key=index_a_b_c(使用联合索引)。
    • rows=100(扫描约100行)。
    • Extra=Using index condition(索引条件下推,部分过滤在存储引擎层完成)。

五、索引使用的最佳实践

  1. 优先使用联合索引:根据查询条件设计合理的字段顺序。
  2. 避免过度索引:索引占用存储空间,增删改操作需维护索引,影响性能。
  3. 覆盖索引优化:尽量让索引包含查询所需字段(避免回表)。
  4. 长字段索引前缀:对长文本字段使用前缀索引(如 INDEX(name(10)))。

总结

  • 联合索引需遵循最左前缀原则,范围查询会中断后续字段的索引使用。
  • OR 条件通常导致索引失效,但索引合并可能提供有限优化。
  • EXPLAIN 命令是验证索引使用情况的核心工具。
  • 设计索引时需结合业务场景,平衡查询性能与维护成本。

6. redis的常用数据结构?zset和set有啥区别?做排行榜用什么?

一、Redis 常用数据结构

数据结构特点典型应用场景
String(字符串)存储文本、数字或二进制数据,支持原子操作(如INCR/DECR)。计数器、缓存、分布式锁
List(列表)双向链表,支持头部/尾部插入、阻塞操作。消息队列、最新动态列表
Hash(哈希)存储字段-值映射,适合表示对象。用户信息、商品详情
Set(集合)无序且元素唯一,支持集合运算(交集、并集、差集)。标签系统、共同好友
ZSet(有序集合)元素唯一且关联分数(Score),按分数排序,支持范围查询。排行榜、延迟队列、带权重的任务调度
其他扩展结构Bitmap(位图)、HyperLogLog(基数统计)、Geospatial(地理空间)、Stream(流)特定场景如UV统计、地理位置服务等。

二、ZSet(Sorted Set)与 Set 的核心区别

对比维度ZSet(有序集合)Set(集合)
元素顺序元素按分数(Score)排序,支持范围查询(如TOP N)。无序,元素存储顺序不确定。
元素关联值每个元素必须关联一个分数(Score)。无关联值,仅存储元素本身。
操作复杂度插入/删除:O(log N);范围查询:O(log N + M)。插入/删除:O(1);集合运算:O(N)。
典型命令ZADDZRANGEZRANKZSCORESADDSINTERSMEMBERS
应用场景排行榜、优先级队列、带权重的数据排序。标签系统、去重、集合运算(如共同关注)。

三、排行榜实现:为何选择 ZSet?

需求分析
排行榜需要根据用户的分数动态排序,并支持以下操作:

  1. 快速更新用户分数(如用户得分变化)。
  2. 获取前N名用户及其分数。
  3. 查询某个用户的排名。

ZSet 实现方案

  • 添加/更新分数
    ZADD leaderboard 1000 "user1"  # 添加用户user1,分数1000
    ZADD leaderboard 2000 "user2"  # 添加用户user2,分数2000
    
  • 获取TOP 10
    ZREVRANGE leaderboard 0 9 WITHSCORES  # 按分数降序返回前10名及分数
    
  • 查询用户排名
    ZREVRANK leaderboard "user1"  # 返回user1的排名(从0开始)
    ZSCORE leaderboard "user1"    # 返回user1的分数
    

优势总结

  • 高效排序:基于跳跃表(Skip List)实现,插入和范围查询时间复杂度为 O(log N)。
  • 灵活操作:支持按分数范围、排名范围查询,以及动态更新分数。

四、ZSet 内部实现原理

  1. 数据结构
    • 跳跃表(Skip List):用于快速范围查询和排序。
    • 哈希表(Hash Table):存储成员到分数的映射,确保 O(1) 时间获取分数。
  2. 内存优化
    • 当元素数量较少或成员较小时,使用压缩列表(Ziplist)存储以节省内存。

总结

  • ZSet 适用于需要排序和范围查询的场景(如排行榜)。
  • Set 适用于无需排序且需保证元素唯一的场景(如标签系统)。
  • 选择依据:是否需要按权重排序?是否需要快速获取排名?是则选ZSet,否则选Set。

示例对比

# Set 示例:存储用户标签
SADD user:1:tags "java" "redis" "python"

# ZSet 示例:游戏排行榜
ZADD game_leaderboard 5000 "playerA" 3000 "playerB"

7. reids的持久化方式?AOF和RDB的优缺点?如何解决AOF文件太大的问题?除了文件大小还有别的区别吗?

一、Redis的持久化方式

Redis提供两种持久化机制,用于将内存数据持久化到磁盘,防止服务重启导致数据丢失:

  1. RDB(Redis Database)

    • 原理:在指定时间间隔生成内存数据的快照(Snapshot),保存为二进制文件(默认dump.rdb)。
    • 触发方式
      • 手动触发:SAVE(阻塞主线程)或BGSAVE(后台子进程生成)。
      • 自动触发:配置save <seconds> <changes>规则(如save 900 1表示900秒内至少1次修改触发)。
  2. AOF(Append-Only File)

    • 原理:记录所有写操作命令(以文本协议格式追加到文件),重启时重放命令恢复数据。
    • 同步策略
      • appendfsync always:每次写操作都同步到磁盘(数据最安全,性能最低)。
      • appendfsync everysec:每秒同步一次(默认,平衡性能与安全)。
      • appendfsync no:由操作系统决定同步时机(性能最高,数据风险最大)。

二、RDB与AOF的优缺点对比

维度RDBAOF
数据安全性可能丢失最后一次快照后的数据(如宕机时未触发保存)根据同步策略,最多丢失1秒数据(everysec模式)
文件体积小(二进制压缩格式)大(记录所有写命令,需定期重写优化)
恢复速度快(直接加载二进制文件)慢(需逐条执行命令)
性能影响高(BGSAVE时fork子进程可能阻塞主线程,大数据量时内存占用翻倍)低(追加写操作,everysec模式对性能影响小)
可读性二进制文件不可读文本文件可读(便于人工分析)
容灾能力单一文件,易备份AOF文件损坏时可通过redis-check-aof工具修复

三、AOF文件过大的解决方案

  1. AOF重写(Rewrite)

    • 原理:根据当前内存数据生成新的AOF文件,替换旧文件,去除冗余命令(如多次修改同一Key的命令合并为最终状态)。
    • 触发方式
      • 手动触发:执行BGREWRITEAOF命令。
      • 自动触发:配置auto-aof-rewrite-percentageauto-aof-rewrite-min-size(如当AOF文件大小超过100MB且比上次重写后增长100%时触发)。
    • 重写过程
      1. 主进程fork子进程生成新AOF文件。
      2. 重写期间的新写命令会同时写入AOF缓冲区AOF重写缓冲区
      3. 子进程完成重写后,将重写缓冲区数据追加到新AOF文件,原子替换旧文件。
  2. 混合持久化(Redis 4.0+)

    • 原理:AOF重写时,将当前数据以RDB格式写入AOF文件头部,后续增量命令以AOF格式追加。
    • 优势:结合RDB的快速恢复和AOF的增量数据安全。
    • 启用方式:配置aof-use-rdb-preamble yes

四、RDB与AOF的其他核心区别

维度RDBAOF
数据格式二进制压缩格式文本协议格式(兼容Redis通信协议)
故障恢复优先级Redis默认优先加载AOF文件(若存在)若AOF和RDB同时启用,AOF优先
适用场景对数据完整性要求不高,需快速恢复对数据安全性要求高,允许少量数据丢失
版本兼容性不同版本的RDB文件格式可能不兼容AOF文件格式兼容性更好(基于RESP协议)

五、总结

  • RDB:适合冷备快速恢复场景,但存在数据丢失风险。
  • AOF:适合高数据安全场景,但需定期重写优化文件体积。
  • 混合持久化:Redis 4.0+推荐方案,兼顾安全性与恢复效率。
  • 生产建议
    • 同时启用RDB和AOF(appendonly yes),利用各自优势。
    • 配置AOF自动重写规则,避免文件过大。
    • 备份RDB文件到远程存储(如云存储),防止单点故障。

示例配置

# RDB配置
save 900 1       # 15分钟内有至少1个Key变化则触发BGSAVE
save 300 10      # 5分钟内有至少10个Key变化
save 60 10000    # 1分钟内有至少10000个Key变化

# AOF配置
appendonly yes
appendfsync everysec
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-use-rdb-preamble yes     # 启用混合持久化

8. cookie和session有啥区别?

一、核心概念

维度CookieSession
存储位置客户端(浏览器)服务端(如内存、Redis、数据库)
数据安全性较低(数据存储在客户端,可能被篡改或窃取)较高(数据存储在服务端,仅通过Session ID标识客户端)
容量限制单个Cookie ≤4KB,每个域名下Cookie数量有限(通常≤50个)无硬性限制(但受服务端内存或存储资源影响)
生命周期可设置过期时间(持久Cookie)或随浏览器关闭失效(会话Cookie)通常依赖Session ID的有效期,服务端可设置超时时间(如30分钟无操作自动失效)
性能影响每次HTTP请求自动携带Cookie,可能增加网络开销服务端需存储和检索Session数据,高频访问时可能增加服务端负载
跨域支持可通过domainpath属性控制作用域Session ID默认绑定到当前域名,需额外处理跨域场景(如JWT替代方案)

二、工作原理对比

1. Cookie 的工作流程
  1. 服务端设置:通过HTTP响应头 Set-Cookie 发送数据到浏览器。
    HTTP/1.1 200 OK
    Set-Cookie: username=John; Expires=Wed, 21 Oct 2024 07:28:00 GMT; Path=/; Secure
    
  2. 客户端存储:浏览器保存Cookie到本地(内存或磁盘)。
  3. 自动携带:后续请求通过HTTP头 Cookie 自动附加到服务端。
    GET /home HTTP/1.1
    Cookie: username=John; theme=dark
    
2. Session 的工作流程
  1. 创建Session:用户首次访问时,服务端生成唯一Session ID并存储相关数据(如用户信息)。
  2. 传递Session ID:通过Cookie(如JSESSIONID)或URL重写(如;jsessionid=xxx)返回给客户端。
  3. 客户端携带Session ID:后续请求中,客户端发送Session ID供服务端检索对应Session数据。
  4. 服务端验证:服务端根据Session ID查找并验证数据,维持会话状态。

三、安全性对比

风险CookieSession
数据泄露数据明文存储,易被窃取(需加密或HttpOnly)仅传输Session ID,敏感数据在服务端
会话劫持Cookie被盗取后可直接冒充用户Session ID被盗后仍可冒充用户(需结合IP绑定等增强措施)
XSS攻击可通过脚本窃取Cookie(设置HttpOnly防护)Session ID同样需HttpOnly防护
CSRF攻击自动携带Cookie可能触发CSRF(需Token防护)Session本身不直接防御CSRF

四、典型应用场景

场景CookieSession
记住登录状态存储加密后的Token(如remember_me存储用户ID、权限信息等敏感数据
用户偏好设置存储主题、语言等非敏感数据不适用(数据应存在客户端)
购物车临时存储未登录用户的购物项登录用户购物车数据持久化到服务端
分布式会话管理需额外处理(如共享加密密钥)使用集中存储(如Redis)管理Session数据

五、如何选择?

  1. 使用Cookie的场景

    • 需要客户端持久化的小型非敏感数据(如用户主题设置)。
    • 跨域单点登录(SSO)场景(需配合安全传输)。
  2. 使用Session的场景

    • 存储敏感信息(如用户ID、权限角色)。
    • 需要服务端控制会话生命周期的场景(如强制注销)。
  3. 混合使用

    • 典型方案:使用Cookie存储Session ID,实际数据存储在服务端Session中。

六、代码示例

1. Cookie 操作(Node.js)
// 设置Cookie
res.setHeader('Set-Cookie', [
  'username=John; Max-Age=3600; HttpOnly',
  'theme=dark; Path=/'
]);

// 读取Cookie(需解析请求头)
const cookies = req.headers.cookie.split(';').reduce((acc, cookie) => {
  const [key, value] = cookie.trim().split('=');
  acc[key] = value;
  return acc;
}, {});
console.log(cookies.username); // 输出 John
2. Session 操作(Express + express-session)
const session = require('express-session');
app.use(session({
  secret: 'your_secret_key',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: true, maxAge: 3600000 }
}));

// 存储Session数据
req.session.user = { id: 1, name: 'John' };

// 读取Session数据
console.log(req.session.user.name); // 输出 John

总结

  • Cookie:客户端存储,适合非敏感数据,需注意安全防护(HttpOnly、Secure)。
  • Session:服务端存储,适合敏感数据,依赖Session ID传递(通常通过Cookie)。
  • 最佳实践:敏感数据存Session,非敏感数据存Cookie,并始终启用HTTPS和防御CSRF。

9. 微信的扫码登录如何实现?

一、整体流程概述

微信扫码登录基于 OAuth 2.0 授权框架,结合 轮询机制事件推送 实现无密码登录,核心流程分为以下步骤:

  1. 生成登录二维码:业务服务器生成唯一临时凭证(二维码ID),用户浏览器轮询检查登录状态。
  2. 用户扫码并确认:微信App扫描二维码,用户确认授权后,微信服务器通知业务服务器。
  3. 登录态同步:业务服务器验证授权,返回用户信息,完成浏览器端登录跳转。

二、详细步骤拆解

1. 生成登录二维码
  • 步骤

    1. 用户访问业务网站,触发登录请求。
    2. 业务服务器向微信开放平台请求生成 临时二维码,包含唯一标识符(如 qrcode_id)。
    3. 微信服务器返回二维码图片URL,业务网站展示二维码。
    4. 浏览器启动轮询(如每2秒请求一次),检查扫码状态。
  • 技术要点

    • 二维码内容为URL,形如:https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&state=STATE
    • 业务服务器需缓存 qrcode_id 及其状态(待扫描、已扫描、已确认)。
2. 用户扫码并确认授权
  • 步骤

    1. 用户使用微信App扫描二维码,解析出 qrcode_idappid(业务应用ID)。
    2. 微信App向微信服务器发送扫码事件,携带 qrcode_id 和用户身份临时凭证。
    3. 微信服务器验证用户登录态,向业务服务器推送 扫码事件通知
    4. 业务服务器更新 qrcode_id 状态为 已扫描,等待用户确认。
    5. 微信App弹出确认登录界面,用户点击“确认”后,微信服务器通知业务服务器授权完成。
  • 技术要点

    • 用户确认前,业务服务器仅知“已扫码”,需防止未授权登录。
    • 微信服务器与业务服务器通过 HTTPS+签名 保障通信安全。
3. 登录态同步与跳转
  • 步骤

    1. 业务服务器收到授权完成通知后,生成用户唯一标识(如 unionidopenid)。
    2. 创建用户会话(如生成JWT或Session ID),关联用户信息。
    3. 浏览器轮询检测到 qrcode_id 状态变为“已授权”,获取登录凭证(如重定向URL携带Token)。
    4. 浏览器跳转到登录成功页面,完成登录流程。
  • 技术要点

    • 轮询接口返回状态码(如 0-等待扫码1-已扫码待确认2-授权成功3-二维码过期)。
    • 二维码有效期通常为5分钟,超时需刷新重新生成。

三、技术实现关键点

1. 二维码状态管理
  • 存储结构
    {
      "qrcode_id": "123456",
      "status": "pending",  // pending, scanned, confirmed, expired
      "user_info": null,
      "expire_time": 1716200000
    }
    
  • 缓存策略:使用Redis存储,设置TTL(如300秒)。
2. 安全防护机制
  • 防CSRF:二维码ID需绑定IP或设备指纹,防止跨站伪造扫码。
  • 防重放攻击:微信服务器通知需包含时间戳和签名验证。
  • Token加密传输:登录凭证通过HTTPS传输,避免中间人窃取。
3. 微信开放平台交互
  • 授权流程API
    • 生成二维码/cgi-bin/qrcode/create
    • 扫码事件推送:微信服务器回调业务配置的 redirect_uri
    • 获取用户信息:通过 access_token 调用 /sns/userinfo

四、时序图

用户浏览器          业务服务器          微信服务器          微信App
     |                   |                   |               |
     |--- 1.请求登录 --->|                   |               |
     |                   |--- 2.生成二维码 -->|               |
     |<-- 3.返回二维码 --|                   |               |
     |--- 4.轮询状态 --->|                   |               |
     |                   |                   |               |
     |              用户扫描二维码           |               |
     |                   |                   |<-- 5.扫码 ---|
     |                   |<-- 6.扫码事件通知 -|               |
     |                   |--- 7.更新状态 --->|               |
     |                   |                   |               |
     |              用户确认登录             |               |
     |                   |                   |<-- 8.确认 ---|
     |                   |<-- 9.授权完成通知 -|               |
     |                   |---10.生成会话 --->|               |
     |<--11.返回登录成功--|                   |               |

五、常见问题与解决方案

  1. 二维码过期
    • 前端轮询检测到 status=expired 时,自动刷新二维码。
  2. 网络延迟导致状态不同步
    • 使用WebSocket替代轮询,实时推送状态变更。
  3. 跨设备登录风险
    • 增加二次验证(如短信验证码)或设备绑定策略。

总结

微信扫码登录通过 临时二维码事件驱动 机制,结合OAuth2.0授权,实现安全便捷的无密码登录。核心在于状态机管理、安全通信和实时状态同步,适用于高并发场景,兼顾用户体验与安全性。

10. 讲讲restful的概念?

一、RESTful 的核心概念

REST(Representational State Transfer) 是一种基于 HTTP 协议的软件架构风格,由 Roy Fielding 在 2000 年提出。其核心目标是 通过统一的接口和资源标识,简化分布式系统的交互
RESTful API 是遵循 REST 原则设计的 API,通常用于 Web 服务开发。


二、REST 的六大核心原则

  1. 资源导向(Resource-Based)

    • 资源:系统的一切都是资源(如用户、订单),每个资源有唯一标识(URI)。
    • URI 设计
      • /users(用户集合)
      • /users/123(ID 为 123 的用户)
      • /getUser?id=123(违反资源命名规范)
  2. 统一接口(Uniform Interface)

    • HTTP 方法语义化
      HTTP 方法操作幂等性示例
      GET获取资源GET /users/1
      POST新增资源POST /users
      PUT替换资源PUT /users/1
      PATCH部分更新资源PATCH /users/1
      DELETE删除资源DELETE /users/1
  3. 无状态(Stateless)

    • 每个请求必须包含处理所需的所有信息,服务器不保存客户端状态。
    • 示例
      • ✅ 请求头携带 Token 认证:Authorization: Bearer xxxxx
      • ❌ 依赖服务器 Session 维持登录态(不符合无状态原则)。
  4. 可缓存(Cacheable)

    • 响应需明确标记是否可缓存,减少重复请求。
    • HTTP 缓存控制
      Cache-Control: max-age=3600  # 缓存有效期1小时
      ETag: "abc123"               # 资源版本标识
      
  5. 分层系统(Layered System)

    • 客户端无需知道是否直接连接服务器,中间层(如 CDN、代理)可透明存在。
  6. 按需代码(可选,Code-On-Demand)

    • 服务端可返回可执行代码(如 JavaScript),扩展客户端功能(较少使用)。

三、RESTful API 设计规范

  1. URI 设计规范

    • 使用名词表示资源,避免动词:
      • GET /articles
      • GET /getArticles
    • 层级关系用 / 分隔:
      • GET /users/123/orders(用户 123 的订单列表)
    • 过滤条件使用查询参数:
      • GET /articles?category=tech&sort=date
  2. HTTP 状态码

    状态码含义场景
    200OK成功获取或修改资源
    201Created资源创建成功(配合POST)
    204No Content成功但无返回内容(如DELETE)
    400Bad Request客户端请求错误
    401Unauthorized未认证
    403Forbidden无权限访问
    404Not Found资源不存在
    500Internal Server Error服务器内部错误
  3. 响应数据格式

    • 推荐使用 JSON,保持简洁:
      {
        "id": 1,
        "name": "John",
        "links": [{ "rel": "self", "href": "/users/1" }]  // HATEOAS 可选
      }
      
    • HATEOAS(超媒体驱动):响应中提供资源关联链接(如 selfnext),增强客户端可发现性。

四、RESTful 的优缺点

优点缺点
简单易用,基于 HTTP 标准对复杂业务场景支持有限(如批量操作)
无状态,易扩展过度设计 URI 可能导致冗余
天然支持缓存和 CDN不适合实时性要求高的场景(如聊天)
前后端分离友好严格遵循原则可能增加实现成本

五、RESTful 与 RPC 对比

维度RESTfulRPC(如 gRPC)
协议HTTP/1.1(文本)HTTP/2(二进制,高效)
设计重点资源与状态方法(函数)调用
适用场景通用 CRUD 操作高性能、复杂交互(如微服务内部通信)
数据格式JSON/XMLProtobuf/Thrift(紧凑高效)

六、实际案例

场景:设计一个博客系统的 RESTful API

  1. 获取文章列表

    GET /articles?author=John&page=2 HTTP/1.1
    Accept: application/json
    

    响应

    {
      "data": [
        {"id": 1, "title": "RESTful 设计指南", "author": "John"},
        {"id": 2, "title": "HTTP 缓存详解", "author": "John"}
      ],
      "pagination": {"page": 2, "total": 10}
    }
    
  2. 创建新文章

    POST /articles HTTP/1.1
    Content-Type: application/json
    Authorization: Bearer xyz123
    
    {
      "title": "RESTful 最佳实践",
      "content": "本文详细讲解..."
    }
    

    响应

    HTTP/1.1 201 Created
    Location: /articles/3
    

总结

  • RESTful 核心:资源化设计、无状态通信、标准 HTTP 方法。
  • 最佳实践:简洁的 URI、合理的状态码、JSON 数据格式。
  • 适用场景:适合 CRUD 为主的 Web 服务,需高扩展性和简单性的场景。
  • 扩展思考:在复杂业务中,可结合 GraphQL 或 RPC 弥补 RESTful 的局限性。

11. restful还有什么约定和规范?

RESTful API 的设计遵循一系列约定和规范,确保API的简洁性、一致性和易用性。以下是核心规范及其实践示例:


1. URI 设计规范

  • 使用名词而非动词

    • /users(用户集合)
    • /getUsers(违反资源命名)
  • 层级关系明确

    • /users/{id}/posts(用户的所有文章)
    • /userPosts?id=123(混合路径与查询参数)
  • 复数形式统一

    • /products
    • /product

2. HTTP 方法语义化

HTTP 方法操作幂等性示例
GET获取资源GET /users/1
POST新增资源POST /users
PUT替换资源PUT /users/1
PATCH部分更新资源PATCH /users/1
DELETE删除资源DELETE /users/1

3. 版本控制

  • URI 路径中指定版本
    GET /api/v1/users
    
  • HTTP 头部指定版本
    Accept: application/vnd.myapi.v1+json
    

4. 状态码的正确使用

状态码含义场景示例
200OK成功获取资源
201Created创建新资源成功
204No Content删除资源成功
400Bad Request请求参数错误
401Unauthorized未携带有效 Token
403Forbidden无权访问资源
404Not Found资源不存在
500Internal Server Error服务器内部错误

5. 数据格式与结构

  • JSON 格式统一
    {
      "data": {
        "id": 1,
        "name": "John"
      },
      "links": [
        { "rel": "self", "href": "/users/1" }
      ]
    }
    
  • 分页与过滤
    {
      "data": [...],
      "pagination": {
        "page": 2,
        "total": 100,
        "limit": 20
      }
    }
    
  • 错误响应结构化
    {
      "error": {
        "code": "invalid_input",
        "message": "Email format is invalid.",
        "details": { "field": "email" }
      }
    }
    

6. HATEOAS(超媒体驱动)

在响应中提供资源关联链接,增强客户端导航能力:

{
  "id": 1,
  "name": "John",
  "links": [
    { "rel": "self", "href": "/users/1" },
    { "rel": "posts", "href": "/users/1/posts" }
  ]
}

7. 查询参数规范

  • 过滤、排序、分页
    GET /users?role=admin&sort=-created_at&page=2&limit=20
    
    • role=admin:过滤角色为管理员
    • sort=-created_at:按创建时间降序
    • page=2&limit=20:第二页,每页20条

8. 安全规范

  • HTTPS 加密传输
  • 身份认证:OAuth2、JWT
    Authorization: Bearer <token>
    
  • 输入验证与防攻击:防止 SQL 注入、XSS、CSRF

9. 缓存控制

通过 HTTP 头管理缓存:

Cache-Control: max-age=3600  # 缓存1小时
ETag: "abcd1234"            # 资源版本标识
If-None-Match: "abcd1234"   # 客户端携带ETag验证

10. 无状态性

  • 每个请求必须包含完整上下文,服务器不保存客户端状态。
  • 使用 Token 而非 Session 维持认证状态。

11. 幂等性设计

  • PUT 和 DELETE 必须幂等:多次调用效果相同。
  • POST 非幂等:每次调用可能创建新资源。

12. 跨域资源共享(CORS)

配置合适的响应头允许跨域访问:

Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT

13. 文档与可发现性

  • 使用 OpenAPI(Swagger) 生成交互式文档。
  • 提供 /docs/swagger 端点访问API文档。

总结

RESTful API 的规范涵盖 URI 设计、HTTP 方法、状态码、数据格式、安全性等多个方面。遵循这些规范能显著提升 API 的可用性、可维护性和安全性。实际开发中,需根据业务需求灵活调整,同时保持核心原则的一致性。

12. POST和GET有啥区别?POST、GET、PUT、DELETE应用什么场景?

一、核心区别

HTTP方法语义幂等性安全性参数位置数据长度限制缓存典型用途
GET获取资源是 ✅安全 ✅URL查询参数受URL长度限制可缓存 ✅查询数据、过滤、分页
POST创建资源否 ❌不安全 ❌请求体无限制不可缓存 ❌提交表单、上传文件
PUT替换资源是 ✅不安全 ❌请求体无限制不可缓存 ❌全量更新资源
DELETE删除资源是 ✅不安全 ❌URL路径无限制不可缓存 ❌删除指定资源

二、详细对比

1. GET 与 POST 的区别
  • 语义
    • GET:从服务器获取资源(只读操作)。
    • POST:向服务器提交数据(可能修改资源)。
  • 幂等性
    • GET 是幂等的(多次请求结果一致,无副作用)。
    • POST 非幂等(多次提交可能产生不同结果,如重复创建资源)。
  • 参数位置
    • GET:参数通过 URL 传递(如 /users?id=1),明文可见(需 HTTPS 加密)。
    • POST:参数在请求体中,适合传输敏感或大量数据。
  • 安全性
    • GET 不应修改数据,避免被缓存或爬虫意外触发。
    • POST 用于变更操作,相对更安全(仍需 HTTPS)。
2. PUT 与 POST 的区别
  • 语义
    • PUT:替换整个资源(客户端需提供完整数据)。
    • POST:创建新资源(服务器生成资源ID)。
  • 幂等性
    • PUT 是幂等的(多次调用结果一致)。
    • POST 非幂等(多次调用创建多个资源)。
  • 资源定位
    • PUT URL 需指定资源 ID(如 PUT /users/1)。
    • POST URL 指向集合(如 POST /users)。

三、应用场景

1. GET
  • 场景
    • 查询数据(如获取用户列表、搜索商品)。
    • 分页和过滤(如 /users?page=2&role=admin)。
    • 获取静态资源(如图片、CSS 文件)。
  • 示例
    GET /api/users/1 HTTP/1.1
    
2. POST
  • 场景
    • 创建新资源(如提交注册表单、上传文件)。
    • 触发非幂等操作(如支付请求、发送消息)。
    • 无法通过简单URL表达的操作(如复杂查询)。
  • 示例
    POST /api/users HTTP/1.1
    Content-Type: application/json
    
    { "name": "John", "email": "john@example.com" }
    
3. PUT
  • 场景
    • 全量更新资源(如修改用户所有信息)。
    • 客户端明确知道资源 ID 的创建(如指定 ID 上传)。
  • 示例
    PUT /api/users/1 HTTP/1.1
    Content-Type: application/json
    
    { "name": "John", "email": "john_new@example.com" }
    
4. DELETE
  • 场景
    • 删除指定资源(如删除文章、注销用户)。
  • 示例
    DELETE /api/users/1 HTTP/1.1
    

四、幂等性与安全性

  • 幂等性
    • GETPUTDELETE 是幂等的,适合重试和缓存。
    • POST 非幂等,需确保操作仅执行一次(如订单提交)。
  • 安全性
    • 安全方法(GETHEADOPTIONS)不修改资源。
    • 非安全方法(POSTPUTDELETE)需权限验证(如 Token 或 OAuth)。

五、RESTful API 设计示例

# 获取用户列表(过滤+分页)
GET /users?role=admin&page=2

# 创建用户
POST /users
Body: { "name": "Alice" }

# 获取单个用户
GET /users/1

# 全量更新用户
PUT /users/1
Body: { "name": "Alice", "role": "admin" }

# 删除用户
DELETE /users/1

六、总结

  • GET:读操作,幂等,参数可见于URL,适合查询。
  • POST:写操作,非幂等,参数在请求体,适合创建。
  • PUT:写操作,幂等,全量替换资源。
  • DELETE:写操作,幂等,删除资源。

最佳实践

  • 遵循 RESTful 规范,使用语义化的 HTTP 方法和状态码。
  • 敏感操作使用 HTTPS,结合 Token 或 OAuth2 认证。
  • 幂等性设计提升接口容错性(如支付接口需防重复提交)。

13. 幂等性概念?如何实现接口的幂等?

一、幂等性概念

幂等性(Idempotence) 是指 同一操作执行一次或多次对系统状态的影响一致。在分布式系统和API设计中,幂等性用于避免因网络重试、客户端重复提交等导致的重复操作问题。
示例

  • 支付接口:用户点击多次支付按钮,系统应仅扣款一次。
  • 订单创建:网络超时后重试,不应生成多个订单。

二、幂等性实现方案

1. 唯一标识符(Token/ID)
  • 原理:客户端在操作前获取唯一标识(如Token或业务ID),服务端校验该标识是否已处理。
  • 流程
    1. 客户端请求服务端获取唯一Token。
    2. 客户端携带Token发起业务请求。
    3. 服务端检查Token是否已使用:
      • 若未使用,执行业务并标记Token为已使用。
      • 若已使用,拒绝请求并返回幂等错误。
  • 适用场景:表单提交、支付接口。
  • 示例代码(Redis实现)
    // 生成Token(前端获取)
    String token = UUID.randomUUID().toString();
    redis.setex("req_token:" + token, 3600, "1"); // 有效期1小时
    
    // 处理请求(后端校验)
    if (redis.del("req_token:" + token) == 1) {
        processOrder(); // 执行业务
    } else {
        throw new IdempotentException("重复请求");
    }
    
2. 数据库唯一约束
  • 原理:利用数据库唯一索引(如订单号、流水号)阻止重复数据插入。
  • 实现
    CREATE TABLE orders (
        id BIGINT PRIMARY KEY,
        order_no VARCHAR(64) UNIQUE, -- 唯一订单号
        amount DECIMAL
    );
    
  • 适用场景:订单创建、数据入库。
3. 乐观锁(版本号控制)
  • 原理:在数据更新时检查版本号,仅当版本匹配时才执行更新。
  • 实现
    UPDATE products 
    SET stock = stock - 1, version = version + 1 
    WHERE id = 100 AND version = 2; -- 版本号校验
    
  • 适用场景:库存扣减、账户余额更新。
4. 状态机控制
  • 原理:业务状态流转设计为单向(如“已支付”状态不可逆转),拒绝非法状态变更。
  • 示例
    当前状态允许操作目标状态
    待支付支付已支付
    已支付退款已退款
    已退款无操作-
  • 适用场景:订单状态管理、工单流程。
5. 分布式锁
  • 原理:在分布式环境下,通过锁机制确保同一时刻仅一个请求能执行业务。
  • 实现(Redis + Lua脚本)
    String lockKey = "order_lock:" + orderId;
    String requestId = UUID.randomUUID().toString();
    // 尝试加锁(SETNX + EXPIRE)
    boolean locked = redis.set(lockKey, requestId, "NX", "EX", 30);
    if (locked) {
        try {
            processOrder();
        } finally {
            // 释放锁(需校验requestId避免误删)
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            redis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        }
    }
    
  • 适用场景:高并发下的资源争用(如秒杀)。

三、方案对比与选型

方案优点缺点适用场景
Token机制实现简单,适合前后端交互需额外请求获取Token表单提交、支付接口
唯一约束数据库天然支持,可靠性高依赖数据库性能,无法覆盖更新场景订单号、流水号生成
乐观锁轻量级,适合高频更新需设计版本字段,可能冲突重试库存扣减、余额操作
状态机业务逻辑清晰需预先设计状态流转规则订单状态、工单流程
分布式锁强一致性,适合分布式环境实现复杂,需处理锁超时问题秒杀、分布式任务调度

四、幂等性设计注意事项

  1. 客户端配合:前端可通过按钮置灰、防抖(Debounce)减少重复请求。
  2. 超时与重试:合理设置HTTP超时时间,结合退避策略(Exponential Backoff)重试。
  3. 日志与对账:记录唯一标识和操作日志,定期对账修复数据不一致。
  4. 错误码设计:明确返回幂等错误(如HTTP 409 Conflict),告知客户端勿重复提交。

总结

幂等性是保障系统可靠性的核心设计,需结合业务场景选择合适方案:

  • 简单场景:Token机制或唯一约束。
  • 高频更新:乐观锁。
  • 复杂状态流:状态机控制。
  • 分布式高并发:分布式锁。
    核心原则:识别业务中的幂等需求,通过唯一标识、状态控制或锁机制确保操作唯一性。

算法题:给前序遍历和中序遍历还原二叉树

好的,这是一个经典的二叉树重建问题。以下是解题思路和Java代码实现:


解题思路

  1. 前序遍历的第一个节点是当前子树的根节点。
  2. 中序遍历中找到这个根节点:
    • 根节点左边的所有节点属于左子树
    • 根节点右边的所有节点属于右子树
  3. 递归处理左子树和右子树,直到子树为空。

代码实现

// 这是一个简单的二叉树节点类,包含三个成员变量
class TreeNode {
    // -   `val`:存储节点的值。
    int val;
    // -   `left`:指向左子节点的引用。
    TreeNode left;
    // -   `right`:指向右子节点的引用。
    TreeNode right;
    // -   构造函数 `TreeNode(int x)` 用于初始化节点的值。
    TreeNode(int x) { val = x; }
}

public class Solution {
    // 该方法是对外提供的接口,用于构建二叉树。
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        
        // 增加输入参数的边界检查
        if (preorder == null || inorder == null || preorder.length != inorder.length) {
            return null;
        }

        // 用哈希表存储中序遍历的值和索引,加速查找
        // 1.  创建一个 `HashMap` 对象 `inMap`,用于存储中序遍历数组中每个元素及其对应的索引。这样做的目的是为了在后续查找根节点在中序遍历中的位置时,时间复杂度可以从  降低到 。
        Map<Integer, Integer> inMap = new HashMap<>();
        // 2.  遍历中序遍历数组 `inorder`,将每个元素及其索引存入 `inMap` 中。
        for (int i = 0; i < inorder.length; i++) {
            inMap.put(inorder[i], i);
        }
        // 1.  调用 `build` 方法开始递归构建二叉树,并返回构建好的根节点。
        return build(
            preorder, 0, preorder.length - 1,
            inorder, 0, inorder.length - 1,
            inMap
        );
    }

    // 该方法是一个递归方法,用于根据前序遍历和中序遍历的子数组构建二叉树。
    private TreeNode build(
        // -   `preorder`:前序遍历数组。
        // -   `preStart` 和 `preEnd`:前序遍历数组的起始和结束索引。
        // -   `inorder`:中序遍历数组。
        // -   `inStart` 和 `inEnd`:中序遍历数组的起始和结束索引。
        // -   `inMap`:存储中序遍历元素及其索引的哈希表。
        int[] preorder, int preStart, int preEnd,
        int[] inorder, int inStart, int inEnd,
        Map<Integer, Integer> inMap
    ) {
        // 终止条件,说明当前子数组为空,返回 `null`。
        if (preStart > preEnd || inStart > inEnd) return null;

        // 前序的第一个节点是当前根节点
        // 前序遍历的第一个元素 `preorder[preStart]` 就是当前子树的根节点,创建一个新的 `TreeNode` 对象。
        TreeNode root = new TreeNode(preorder[preStart]);
        // 在中序中找到根节点的位置
        // 通过 `inMap` 快速找到根节点在中序遍历数组中的索引 `rootIdx`。
        int rootIdx = inMap.get(root.val);
        
        // 增加对 rootIdx 为 null 的检查,防止输入数组不合法
        if (rootIdx == null) {
            return null;
        }
        
        // 左子树的节点数量
        int leftNum = rootIdx - inStart;

        // 递归构建左子树
        root.left = build(
            preorder, preStart + 1, preStart + leftNum, // 前序左子树范围
            inorder, inStart, rootIdx - 1,            // 中序左子树范围
            inMap
        );

        // 递归构建右子树
        root.right = build(
            preorder, preStart + leftNum + 1, preEnd,  // 前序右子树范围
            inorder, rootIdx + 1, inEnd,              // 中序右子树范围
            inMap
        );

        return root;
    }
}
// 时间复杂度为O(n),其中n是二叉树的节点数量。

关键点说明

  1. 哈希表优化:通过哈希表存储中序的值与索引,将查找根节点的时间复杂度从 O(n) 降到 O(1)。
  2. 索引计算
    • 左子树节点数 = rootIdx - inStart
    • 前序左子树范围:preStart + 1preStart + leftNum
    • 前序右子树范围:preStart + leftNum + 1preEnd
  3. 时间复杂度:O(n)(哈希表优化后)
  4. 空间复杂度:O(n)(哈希表 + 递归栈)

示例验证

输入:

  • 前序 [3,9,20,15,7]
  • 中序 [9,3,15,20,7]

输出:

    3
   / \
  9  20
    /  \
   15   7

可以用 TreeNodeval 属性验证树结构是否符合预期。

算法题:两个链表的相交节点


要找出两个单链表的相交节点,可以使用双指针技巧高效解决。以下是详细步骤和代码实现:

方法思路

  1. 双指针遍历:两个指针 pApB 分别从链表A和B的头节点出发。
  2. 路径补偿:当一个指针遍历到链表末尾时,将其重定向到另一链表的头节点继续遍历。
  3. 相遇条件:若链表相交,两个指针最终会在相交节点相遇(路径长度相同);若不相交,则同时到达末尾(null)。

代码实现

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) return null;

        ListNode pA = headA, pB = headB;
        // 当两个指针相遇或都为null时终止循环
        while (pA != pB) {
            // pA走到末尾后跳转到headB继续,否则继续下一个节点
            pA = (pA == null) ? headB : pA.next;
            // pB同理跳转到headA
            pB = (pB == null) ? headA : pB.next;
        }
        return pA; // 返回相遇节点或null
    }
}

关键点解析

  1. 路径补偿机制

    • pA 遍历路径:链表A → 链表B
    • pB 遍历路径:链表B → 链表A
    • 总路径长度均为 A长度 + B长度,确保相交时同步到达交点。
  2. 时间复杂度:O(m + n),其中m、n为链表长度。

  3. 空间复杂度:O(1),仅使用两个指针。

示例验证

相交情况

  • 链表A:1 → 2 → 3 → 4
  • 链表B:5 → 3 → 4
    两指针路径:
    pA: 1 → 2 → 3 → 4 → 5 → 3
    pB: 5 → 3 → 4 → 1 → 2 → 3
    相交节点:3

不相交情况

  • 链表A:1 → 2 → 3
  • 链表B:4 → 5
    两指针最终同时为null,返回null