分布式系统中主键自增拦截器的逻辑分析与实现
在分布式系统中,为数据库记录生成唯一的主键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
:包括INSERT
、UPDATE
、DELETE
等操作。args
:方法参数,包括MappedStatement
(SQL映射)和Object
(传入参数)。
每次执行update
方法时,拦截器都会被触发。
2. 筛选插入操作
拦截器首先判断SQL类型,只处理INSERT
操作:
java
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
if (SqlCommandType.INSERT != sqlCommandType) {
return invocation.proceed();
}
SqlCommandType
:MyBatis枚举,表示SQL类型(如INSERT
、UPDATE
)。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包含
insert
或save
,直接处理单个实体。 - 批量插入 :ID包含
insertBatch
或saveBatch
,从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("系统异常");
}
}
}
逐步解析:
- 反射获取字段 :
getDeclaredFields
获取实体类的所有字段。 - 筛选字段 :
- 类型必须是
Long
(分布式ID通常为长整型)。 - 必须有
@DistributedId
注解。 - 字段值必须为空(避免覆盖已有值)。
- 类型必须是
- 调用远程服务 :通过
idGeneratorClient.getDistributedId
获取ID,注解值作为业务标识。 - 赋值或异常 :
- 成功时用
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提供的工具方法,用于生成代理对象,将拦截器逻辑绑定到目标对象。 - 实现原理 :
- 动态代理 :使用JDK动态代理(基于接口)创建一个
Executor
的代理对象。 - InvocationHandler :
Plugin
类实现了InvocationHandler
,在代理对象的方法调用时插入拦截器逻辑。 - 拦截流程 :
- 调用代理对象的
update
方法时,触发Plugin.invoke
。 invoke
方法检查方法签名是否匹配@Signature
,若匹配则调用拦截器的intercept
方法。- 否则直接执行原始方法。
- 调用代理对象的
- 动态代理 :使用JDK动态代理(基于接口)创建一个
- 参数 :
o
:目标对象(这里是Executor
实例)。this
:当前拦截器实例。
- 返回值 :如果是
Executor
,返回代理对象;否则返回原始对象。
这一机制确保拦截器只作用于指定的目标和方法,既灵活又高效。
完整流程回顾
- 拦截 :捕获
Executor.update
调用。 - 筛选 :只处理
INSERT
操作。 - 解析:从参数中提取实体。
- 区分:处理单条或批量插入。
- 生成ID:为标记字段赋值分布式ID。
- 执行:完成赋值后继续插入操作。
脱敏后的完整代码
以下是去除了项目标识的通用实现:
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生成和拦截器设计有更深刻的认识!