基于Segment-Mybatis的:分布式系统中主键自增拦截器的逻辑分析与实现

分布式系统中主键自增拦截器的逻辑分析与实现

在分布式系统中,为数据库记录生成唯一的主键ID是一项核心任务。传统的数据库自增ID在多节点环境下容易出现冲突或性能问题,因此需要借助分布式ID生成方案。本文将详细分析一个基于MyBatis的拦截器实现,通过拦截插入操作自动为实体字段分配分布式ID。我们会从背景知识入手,逐步拆解代码逻辑,并在关键部分(如 Plugin.wrap)提供深入说明。

背景知识:分布式ID与拦截器的必要性

1. 分布式ID的挑战

在单体数据库中,AUTO_INCREMENT 可以为每条记录生成唯一的递增ID。但在分布式系统中,多个数据库节点或服务同时操作时,这种方式可能导致ID重复。为解决这个问题,业界提出了分布式ID生成方案,例如美团的Leaf或Twitter的Snowflake。这些方案通常通过独立的服务生成全局唯一ID,并支持高并发。

2. 拦截器的价值

手动为每个实体设置分布式ID会增加开发工作量,尤其是在大规模系统中。我们希望通过拦截器,在数据库操作(如插入)执行前自动识别需要生成ID的字段并赋值。MyBatis作为一个流行的持久层框架,提供了拦截器机制,允许我们在SQL执行流程中插入自定义逻辑。

3. 技术假设

  • 框架:使用MyBatis处理数据库操作,Spring管理依赖注入。
  • ID生成:通过远程服务(如Feign客户端)获取分布式ID。
  • 注解 :自定义注解(如 @DistributedId)标记需要自动生成ID的字段。

拦截器逻辑:从整体到细节

让我们逐步分析拦截器的实现逻辑,代码基于Java和MyBatis。

1. 定义拦截器范围

拦截器需要作用于MyBatis的Executor类的update方法,因为插入操作属于更新操作的一种。我们通过@Intercepts注解指定:

java 复制代码
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
  • Executor:MyBatis的核心执行器,负责SQL的执行。
  • update :包括INSERTUPDATEDELETE等操作。
  • args :方法参数,包括MappedStatement(SQL映射)和Object(传入参数)。

每次执行update方法时,拦截器都会被触发。

2. 筛选插入操作

拦截器首先判断SQL类型,只处理INSERT操作:

java 复制代码
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
if (SqlCommandType.INSERT != sqlCommandType) {
    return invocation.proceed();
}
  • SqlCommandType :MyBatis枚举,表示SQL类型(如INSERTUPDATE)。
  • invocation.proceed():跳过拦截器逻辑,继续执行原始方法。

这一步避免了对非插入操作的干扰,提高效率。

3. 获取实体对象

插入操作的参数中包含待插入的数据,我们需要提取实体对象:

java 复制代码
Object parameter = invocation.getArgs()[1];
Object dbObject = findDbObject(parameter);
if (dbObject == null) {
    return invocation.proceed();
}
  • invocation.getArgs()[1]update方法的第二个参数,通常是实体或参数集合。
  • findDbObject:自定义方法,从参数中解析出实体。
findDbObject 方法详解

参数可能是单一实体或包含实体的Map:

java 复制代码
protected Object findDbObject(Object parameterObj) {
    if (parameterObj instanceof BaseEntity) {
        return (BaseEntity) parameterObj;
    } else if (parameterObj instanceof Map) {
        for (Object val : ((Map<?, ?>) parameterObj).values()) {
            if (val instanceof BaseEntity) {
                return (BaseEntity) val;
            }
        }
    }
    return null;
}
  • 单一实体 :如果参数是BaseEntity(假设的基类),直接返回。
  • Map参数:遍历Map的值,找到第一个符合条件的实体。
  • 空值处理 :找不到实体时返回null,跳过后续逻辑。

4. 处理单条与批量插入

根据SQL语句的ID区分插入类型:

java 复制代码
if (mappedStatement.getId().contains("insert") || mappedStatement.getId().contains("save")) {
    generatedKey(dbObject);
} else if (mappedStatement.getId().contains("insertBatch") || mappedStatement.getId().contains("saveBatch")) {
    if (parameter instanceof HashMap) {
        Object list = ((Map) parameter).get("list");
        if (list instanceof ArrayList) {
            for (Object o : (ArrayList) list) {
                generatedKey(o);
            }
        }
    }
}
  • mappedStatement.getId():SQL语句的唯一标识,通常包含方法名。
  • 单条插入 :ID包含insertsave,直接处理单个实体。
  • 批量插入 :ID包含insertBatchsaveBatch,从Map中提取list并遍历处理。

5. 生成分布式ID

generatedKey 方法为实体字段赋值分布式ID:

java 复制代码
private void generatedKey(Object parameter) throws Throwable {
    Field[] fieldList = parameter.getClass().getDeclaredFields();
    for (Field field : fieldList) {
        if (!field.getType().isAssignableFrom(Long.class)) {
            continue;
        }
        DistributedId annotation = field.getAnnotation(DistributedId.class);
        if (annotation == null) {
            continue;
        }
        field.setAccessible(true);
        if (field.get(parameter) != null) {
            continue;
        }
        Response<Long> response = idGeneratorClient.getDistributedId(annotation.value());
        if (response.isSuccess()) {
            field.set(parameter, response.getData());
        } else {
            logger.error("无法获取分布式ID");
            throw new RuntimeException("系统异常");
        }
    }
}
逐步解析:
  1. 反射获取字段getDeclaredFields 获取实体类的所有字段。
  2. 筛选字段
    • 类型必须是Long(分布式ID通常为长整型)。
    • 必须有@DistributedId注解。
    • 字段值必须为空(避免覆盖已有值)。
  3. 调用远程服务 :通过idGeneratorClient.getDistributedId获取ID,注解值作为业务标识。
  4. 赋值或异常
    • 成功时用field.set赋值。
    • 失败时记录日志并抛出异常。

6. 绑定到MyBatis:Plugin.wrap 的细节

拦截器通过plugin方法集成到MyBatis:

java 复制代码
@Override
public Object plugin(Object o) {
    if (o instanceof Executor) {
        return Plugin.wrap(o, this);
    } else {
        return o;
    }
}
Plugin.wrap 深入分析
  • 作用Plugin.wrap 是MyBatis提供的工具方法,用于生成代理对象,将拦截器逻辑绑定到目标对象。
  • 实现原理
    1. 动态代理 :使用JDK动态代理(基于接口)创建一个Executor的代理对象。
    2. InvocationHandlerPlugin 类实现了InvocationHandler,在代理对象的方法调用时插入拦截器逻辑。
    3. 拦截流程
      • 调用代理对象的update方法时,触发Plugin.invoke
      • invoke 方法检查方法签名是否匹配@Signature,若匹配则调用拦截器的intercept方法。
      • 否则直接执行原始方法。
  • 参数
    • o:目标对象(这里是Executor实例)。
    • this:当前拦截器实例。
  • 返回值 :如果是Executor,返回代理对象;否则返回原始对象。

这一机制确保拦截器只作用于指定的目标和方法,既灵活又高效。


完整流程回顾

  1. 拦截 :捕获Executor.update调用。
  2. 筛选 :只处理INSERT操作。
  3. 解析:从参数中提取实体。
  4. 区分:处理单条或批量插入。
  5. 生成ID:为标记字段赋值分布式ID。
  6. 执行:完成赋值后继续插入操作。

脱敏后的完整代码

以下是去除了项目标识的通用实现:

java 复制代码
package com.example.common.database.interceptor;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

@Component
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class GeneratedKeyInterceptor implements Interceptor {

    private static final Logger logger = LoggerFactory.getLogger(GeneratedKeyInterceptor.class);
    private static final String INSERT = "insert";
    private static final String SAVE = "save";
    private static final String BATCH_INSERT = "insertBatch";
    private static final String BATCH_SAVE = "saveBatch";

    @Autowired
    private IdGeneratorClient idGeneratorClient;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
        if (SqlCommandType.INSERT != sqlCommandType) {
            return invocation.proceed();
        }

        Object parameter = invocation.getArgs()[1];
        Object dbObject = findDbObject(parameter);
        if (dbObject == null) {
            return invocation.proceed();
        }

        if (mappedStatement.getId().contains(INSERT) || mappedStatement.getId().contains(SAVE)) {
            generatedKey(dbObject);
        } else if (mappedStatement.getId().contains(BATCH_INSERT) || mappedStatement.getId().contains(BATCH_SAVE)) {
            if (parameter instanceof HashMap) {
                Object list = ((Map) parameter).get("list");
                if (list instanceof ArrayList) {
                    for (Object o : (ArrayList) list) {
                        generatedKey(o);
                    }
                }
            }
        }
        return invocation.proceed();
    }

    protected Object findDbObject(Object parameterObj) {
        if (parameterObj instanceof BaseEntity) {
            return (BaseEntity) parameterObj;
        } else if (parameterObj instanceof Map) {
            for (Object val : ((Map<?, ?>) parameterObj).values()) {
                if (val instanceof BaseEntity) {
                    return (BaseEntity) val;
                }
            }
        }
        return null;
    }

    private void generatedKey(Object parameter) throws Throwable {
        Field[] fieldList = parameter.getClass().getDeclaredFields();
        for (Field field : fieldList) {
            if (!field.getType().isAssignableFrom(Long.class)) {
                continue;
            }
            DistributedId annotation = field.getAnnotation(DistributedId.class);
            if (annotation == null) {
                continue;
            }
            field.setAccessible(true);
            if (field.get(parameter) != null) {
                continue;
            }
            Response<Long> response = idGeneratorClient.getDistributedId(annotation.value());
            if (response.isSuccess()) {
                field.set(parameter, response.getData());
            } else {
                logger.error("无法获取分布式ID");
                throw new RuntimeException("系统异常");
            }
        }
    }

    @Override
    public Object plugin(Object o) {
        if (o instanceof Executor) {
            return Plugin.wrap(o, this);
        } else {
            return o;
        }
    }
}

结语

这个拦截器通过MyBatis的插件机制,实现了分布式ID的自动生成。它巧妙结合反射、注解和远程服务,兼顾了单条和批量插入场景。对 Plugin.wrap 的深入剖析揭示了其动态代理的本质,帮助我们理解拦截器如何无缝嵌入框架。希望这篇博客能让你对分布式ID生成和拦截器设计有更深刻的认识!

相关推荐
Vitalia1 小时前
从零开始学Rust:枚举(enum)与模式匹配核心机制
开发语言·后端·rust
飞飞翼1 小时前
python-flask
后端·python·flask
草捏子2 小时前
最终一致性避坑指南:小白也能看懂的分布式系统生存法则
后端
一个public的class3 小时前
什么是 Java 泛型
java·开发语言·后端
头孢头孢4 小时前
k8s常用总结
运维·后端·k8s
TheITSea4 小时前
后端开发 SpringBoot 工程模板
spring boot·后端
Asthenia04124 小时前
编译原理中的词法分析器:从文本到符号的桥梁
后端
Asthenia04125 小时前
用RocketMQ和MyBatis实现下单-减库存-扣钱的事务一致性
后端
Pasregret5 小时前
04-深入解析 Spring 事务管理原理及源码
java·数据库·后端·spring·oracle
Micro麦可乐5 小时前
最新Spring Security实战教程(七)方法级安全控制@PreAuthorize注解的灵活运用
java·spring boot·后端·spring·intellij-idea·spring security