基于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生成和拦截器设计有更深刻的认识!

相关推荐
炒空心菜菜8 分钟前
MapReduce 实现 WordCount
java·开发语言·ide·后端·spark·eclipse·mapreduce
wowocpp2 小时前
spring boot Controller 和 RestController 的区别
java·spring boot·后端
后青春期的诗go2 小时前
基于Rust语言的Rocket框架和Sqlx库开发WebAPI项目记录(二)
开发语言·后端·rust·rocket框架
freellf2 小时前
go语言学习进阶
后端·学习·golang
全栈派森4 小时前
云存储最佳实践
后端·python·程序人生·flask
CircleMouse5 小时前
基于 RedisTemplate 的分页缓存设计
java·开发语言·后端·spring·缓存
獨枭6 小时前
使用 163 邮箱实现 Spring Boot 邮箱验证码登录
java·spring boot·后端
维基框架6 小时前
Spring Boot 封装 MinIO 工具
java·spring boot·后端
秋野酱6 小时前
基于javaweb的SpringBoot酒店管理系统设计与实现(源码+文档+部署讲解)
java·spring boot·后端
☞无能盖世♛逞何英雄☜6 小时前
Flask框架搭建
后端·python·flask