基本要求
- 数据同步要及时可靠,方案要有通用性;
- 进行双向同步;
- 不同的数据源,无法直接访问。
基本思路
- 使用Mybatis拦截器,去获取Insert/Update/Delete语句,并组装sql参数形成完整sql;
- 字符串含有单引’,使用\'替换,sql才能执行成功;
- 由于要进行双向同步,可以考虑使用雪花算法来保证记录ID的唯一性,由于数据源不能直接访问,考虑使用中转服务来进行数据传递;
- Insert语句,重置ID字段;
- 为了保证数据同步接口的并发数据和数据的可靠性,使用消息队列来进行消峰。
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/2498e1338d214360aabfbb34f1c3f277.png)
关键代码
- SQL拦截器
@Intercepts(@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}))
@Component
@Slf4j
public class SqlInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
if (invocation.getArgs().length > 1) {
Object parameter = invocation.getArgs()[1];
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
String tableName = SqlUtil.getTableName(boundSql.getSql());
log.info("tableName = {}", tableName);
String sql = SqlUtil.getSql(mappedStatement, parameter);
log.info("sql = {}", sql);
}
return invocation.proceed();
}
}
- SQL提取方法
public class SqlUtil {
/**
* 获取sql语句
*/
public static String getSql(MappedStatement mappedStatement, Object parameterObject) {
Configuration configuration = mappedStatement.getConfiguration();
BoundSql boundSql = mappedStatement.getBoundSql(parameterObject);
// 获取参数
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
// sql语句中多个空格都用一个空格代替
String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
if (!CollectionUtils.isEmpty(parameterMappings) && parameterObject != null) {
// 获取类型处理器注册器,类型处理器的功能是进行java类型和数据库类型的转换
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
// 如果根据parameterObject.getClass()可以找到对应的类型,则替换
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
sql = sql.replaceFirst("\\?",
Matcher.quoteReplacement(formatValue(parameterObject)));
} else {
// MetaObject主要是封装了originalObject对象,提供了get和set的方法用于获取和设置originalObject的属性值,主要支持对JavaBean、Collection、Map三种类型对象的操作
MetaObject metaObject = configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
String propertyName = parameterMapping.getProperty();
if (metaObject.hasGetter(propertyName)) {
Object obj = metaObject.getValue(propertyName);
sql = sql.replaceFirst("\\?",
Matcher.quoteReplacement(formatValue(obj)));
} else if (boundSql.hasAdditionalParameter(propertyName)) {
// 该分支是动态sql
Object obj = boundSql.getAdditionalParameter(propertyName);
sql = sql.replaceFirst("\\?",
Matcher.quoteReplacement(formatValue(obj)));
} else {
sql = sql.replaceFirst("\\?", "缺失");
}
}
}
}
return sql;
}
/**
* 获取表名
*/
public static String getTableName(String sql) {
// 正则表达式匹配 INSERT 语句中的表名
Pattern pattern = Pattern.compile("\\bINTO\\s+(\\w+)");
// 忽略大小写
Matcher matcher = pattern.matcher(sql.toUpperCase());
// 提取匹配的表名
if (matcher.find()) {
return matcher.group(1);
}
// 正则表达式匹配 UPDATE 语句中的表名
pattern = Pattern.compile("\\bUPDATE\\s+(\\w+)");
matcher = pattern.matcher(sql.toUpperCase());
// 提取匹配的表名
if (matcher.find()) {
return matcher.group(1);
}
// 正则表达式匹配 DELETE 语句中的表名
pattern = Pattern.compile("\\bFROM\\s+(\\w+)");
matcher = pattern.matcher(sql.toUpperCase());
// 提取匹配的表名
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
/**
* 将参数值格式化为 SQL 字符串格式
*/
private static String formatValue(Object value) {
// 字符串类型,在值两端加单引号
if(value instanceof String) {
String data = ((String) value).trim();
// 如果字符串包含"'", 替换为 "\\\\'"
if(data.contains("'")) {
data = data.replace("'", "\\'");
}
return "'" + data + "'";
}
// 日期时间类型,使用 SimpleDateFormat 格式化
else if (value instanceof Date) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return "'" + dateFormat.format((Date) value) + "'";
}
// 布尔类型,直接返回字符串格式
else if (value instanceof Boolean) {
return ((boolean) value) ? "true" : "false";
}
// 其他类型,直接使用 toString() 方法
else {
return value.toString();
}
}
}
- SQL数据消费
public interface SqlMapper {
/**
* 插入
* @param sql
*/
void executeNativeInsertSql(@Param("sql") String sql);
/**
* 更新
* @param sql
*/
void executeNativeUpdateSql(@Param("sql") String sql);
/**
* 删除
* @param sql
*/
void executeNativeDeleteSql(@Param("sql") String sql);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ocean.angel.tool.mapper.SqlMapper">
<insert id="executeNativeInsertSql">
${sql}
</insert>
<update id="executeNativeUpdateSql">
${sql}
</update>
<delete id="executeNativeDeleteSql">
${sql}
</delete>
</mapper>
运行结果
- Insert
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/7bffb7f1991f4ccca4e8c9d5dcdc4dd2.png)
- Update
3. Delete
4. batchInsert
tableName = T_USER_INFO
sql = insert into t_user_info ( username, pwd, mobile, id_card, nick, create_time ) values ( 'Jaime.yu', '123456', '15755141000', '341202*********3538', 'Jaim\'e\'', '2024-02-04 17:32:28' ) ; insert into t_user_info ( username, pwd, mobile, id_card, nick, create_time ) values ( 'Jaime.yu', '00000', '00000000000', '341202*********3533', 'Jaime', '2024-02-04 17:32:28' )
- batchUpdate
tableName = T_USER_INFO
sql = update t_user_info SET username = 'Jaime.yu', pwd = '123456', mobile = '15755141000', id_card = '341202*********3538', nick = 'Jaim\'e\'00', update_time = '2024-02-04 17:34:00' where id = 25 ; update t_user_info SET username = 'Jaime.yu', pwd = '00000', mobile = '00000000000', id_card = '341202*********3533', nick = 'Jaime00', update_time = '2024-02-04 17:34:00' where id = 26