數據數據遷移其實主要就是垂直拆分和分庫分表 垂直拆分和分庫分表過程中主要數據庫的操作就是雙寫和查詢 我們會有開關來控制狀態的轉換,公司業務里orm主要是用mybatis
本文主要目的是為了減少代碼的侵入性和遷移過程中數據庫讀寫代碼的可復用性,實際項目里單個表涉及的查詢多達幾十個,并且涉及到幾十個文件的修改,為了減少遷移過程中對業務代碼的修改,我文章下面會給一個樣例(畢竟不能帶上公司業務代碼)。我目前在酷家樂工作中有遇到以下兩個之前處理起來代碼比較繁瑣的地方
1、一個是大量的業務表從老的數據進行遷移(這里可能容易遇到自增主鍵切換寫順序一致性問題),
2、還有一個問題就是部分大表的擴容(實際上相當于垂直拆分 然后分庫) 本質上都是一回事情
進行遷移的話會需要采用dts或者數據庫日志binlog同步存量數據的過程,這里根據自己公司的技術棧來選擇 存量數據寫入數據庫的時候帶上主鍵
INSERT INTO tablename(field1,field2, field3, ...)
VALUES(value1, value2, value3, ...)
ON DUPLICATE KEY UPDATE
field1=value1,field2=value2, field3=value3, ...;
寫增量數據
1、通常做法是進行雙寫 寫入老表的時候同時寫入新表,如果需要的話可以加上手動事務管理,畢竟是跨庫,不過實際應用場景中寫入失敗的情況很少,根據實際情況來決定。
2、雙寫邏輯會麻煩點的地方就是插入順序切換時候的自增主鍵一致性問題。
自增主鍵插入的問題
(1) 老表如果是單表自增的,新表是單表自增的話
找流量低的時候切換讀寫順序,如果業務需要高度一致性,加分布式鎖,需要額外的開關來決定是否走有鎖的邏輯,順序切換以后,關閉那個控制是否走鎖的邏輯的開關(如果條件允許 的話,各個服務器之間其實通過本地rocksDb寫磁盤 實現一個分布式一致性算法比如于raft 也是可以的 實際上各個服務自己的集群就是一個小型的raft集群了) 其實這里也可以方法 (2)去解決,就是需要個過渡表來管理主鍵自增
(2) 老表如果是單表自增的,新表是分表的話
老表自增id 增加一個大區間比如原來是 id = 10^7 我們直接增加到 id = 2 * 10 ^ 7 新表設置自增id為 10^7 + delta( delta > 0 && delta < 10 ^ 4) 這個范圍自己控制一下就好, 切換插入順序以后主鍵不會沖突,也不會阻塞依賴業務方修改sql為rpc 遷移完成以后 新表主鍵改成 3 * 10 ^ 7, 這里只是大致數量, 實際區間大小由業務來決定
at
然后就是代碼邏輯冗余問題了
這里其實稍微涉及到一點mybatis的架構,通常業務里面我們的mybatis的mapper對象本質上是MapperProxy這個類,套了層jdk動態代理而已,對于需要垂直拆分數據遷移表相關的mapper,我這邊直接自己實現了一個代理,繞過MapperProxy,直接通過 SqlSession 去執行,但是我還是會實現一個 mapper類的代理對象去替換掉業務代碼里面用到的mapper對象,從而實現基本無侵入性, 遷移完了以后代碼還是需要改一下包名啥的
改造如下 我這里以一個UserMapper為例
@Primary
@Bean
public UserMapper delegateUserMapper(DbHandler dbHandler) {
return (UserMapper) Proxy.newProxyInstance(UserMapper.class.getClassLoader(),
new Class<?>[]{ UserMapper.class }, dbHandler);
}
MapperHandler
public class MapperHandler {
private final MapperMethod.SqlCommand command;
private final MapperMethod.MethodSignature methodSignature;
public MapperHandler(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new MapperMethod.SqlCommand(config,
mapperInterface, method);
this.methodSignature = new MapperMethod.MethodSignature(config,
mapperInterface, method);
}
public MapperMethod.MethodSignature getMethodSignature() {
return methodSignature;
}
public Object execute(SqlSession sqlSession, Object[] args) {
return execute(sqlSession, args, methodSignature.convertArgsToSqlCommandParam(args));
}
public Object execute(SqlSession sqlSession, Object[] args, Object param) {
Object result;
switch (command.getType()) {
case INSERT: {
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (methodSignature.returnsVoid() && methodSignature.hasResultHandler()) {
executeWithResultHandler(sqlSession, args, param);
result = null;
} else if (methodSignature.returnsMany()) {
result = executeForMany(sqlSession, args, param);
} else if (methodSignature.returnsMap()) {
result = executeForMap(sqlSession, args, param);
} else if (methodSignature.returnsCursor()) {
result = executeForCursor(sqlSession, args, param);
} else {
result = sqlSession.selectOne(command.getName(), param);
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && methodSignature.getReturnType().isPrimitive() &&
!methodSignature.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" +
methodSignature.getReturnType() + ").");
}
return result;
}
private Object rowCountResult(int rowCount) {
final Object result;
if (methodSignature.returnsVoid()) {
result = null;
} else if (Integer.class.equals(methodSignature.getReturnType()) || Integer.TYPE.equals(
methodSignature.getReturnType())) {
result = rowCount;
} else if (Long.class.equals(methodSignature.getReturnType()) || Long.TYPE.equals(
methodSignature.getReturnType())) {
result = (long) rowCount;
} else if (Boolean.class.equals(methodSignature.getReturnType()) || Boolean.TYPE.equals(
methodSignature.getReturnType())) {
result = rowCount > 0;
} else {
throw new BindingException(
"Mapper method '" + command.getName() + "' has an unsupported return type: " +
methodSignature.getReturnType());
}
return result;
}
private void executeWithResultHandler(SqlSession sqlSession, Object[] args, Object param) {
MappedStatement ms = sqlSession.getConfiguration().getMappedStatement(command.getName());
if (void.class.equals(ms.getResultMaps().get(0).getType())) {
throw new BindingException("method " + command.getName()
+ " needs either a @ResultMap annotation, a @ResultType annotation,"
+
" or a resultType attribute in XML so a ResultHandler can be used as a parameter.");
}
if (methodSignature.hasRowBounds()) {
RowBounds rowBounds = methodSignature.extractRowBounds(args);
sqlSession.select(command.getName(), param, rowBounds,
methodSignature.extractResultHandler(args));
} else {
sqlSession.select(command.getName(), param, methodSignature.extractResultHandler(args));
}
}
private <E> Object executeForMany(SqlSession sqlSession, Object[] args, Object param) {
List<E> result;
if (methodSignature.hasRowBounds()) {
RowBounds rowBounds = methodSignature.extractRowBounds(args);
result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.<E>selectList(command.getName(), param);
}
// issue #510 Collections & arrays support
if (!methodSignature.getReturnType().isAssignableFrom(result.getClass())) {
if (methodSignature.getReturnType().isArray()) {
return convertToArray(result);
} else {
return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
}
}
return result;
}
private <T> Cursor<T> executeForCursor(SqlSession sqlSession, Object[] args, Object param) {
Cursor<T> result;
if (methodSignature.hasRowBounds()) {
RowBounds rowBounds = methodSignature.extractRowBounds(args);
result = sqlSession.<T>selectCursor(command.getName(), param, rowBounds);
} else {
result = sqlSession.<T>selectCursor(command.getName(), param);
}
return result;
}
private <E> Object convertToDeclaredCollection(Configuration config, List<E> list) {
Object collection = config.getObjectFactory().create(methodSignature.getReturnType());
MetaObject metaObject = config.newMetaObject(collection);
metaObject.addAll(list);
return collection;
}
@SuppressWarnings("unchecked")
private <E> Object convertToArray(List<E> list) {
Class<?> arrayComponentType = methodSignature.getReturnType().getComponentType();
Object array = Array.newInstance(arrayComponentType, list.size());
if (arrayComponentType.isPrimitive()) {
for (int i = 0; i < list.size(); i++) {
Array.set(array, i, list.get(i));
}
return array;
} else {
return list.toArray((E[]) array);
}
}
private <K, V> Map<K, V> executeForMap(SqlSession sqlSession, Object[] args, Object param) {
Map<K, V> result;
if (methodSignature.hasRowBounds()) {
RowBounds rowBounds = methodSignature.extractRowBounds(args);
result = sqlSession.<K, V>selectMap(command.getName(), param,
methodSignature.getMapKey(),
rowBounds);
} else {
result = sqlSession.<K, V>selectMap(command.getName(), param,
methodSignature.getMapKey());
}
return result;
}
}
DbHandler 整體架構如下
@Service
public class DbHandler implements InvocationHandler, BeanPostProcessor {
private SqlSession mSrcSqlSession;
private SqlSession mDestSqlSession;
private ConcurrentHashMap<Method, MapperHandler> mMethodCache = new ConcurrentHashMap<>();
@Autowired
public void setProperties(
@Qualifier("srcSqlSessionFactory") SqlSessionFactory srcSqlSessionFactory,
@Qualifier("destSqlSessionFactory") SqlSessionFactory destSqlSessionFactory) {
}
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args)
throws Throwable {
/**
* 這里處理sql處理
* 正常情況下不會有delete 這里進行異常判斷 根據業務場景進行處理
*/
return null;
}
/**
* 這里如果是單表遷移 可以考慮整體加分布式鎖,或者把主鍵自增的任務交給一個中間表
* 移交完成以后 再由中間表移交給新表
* @param args
* @param oldHandler
* @param newHandler
* @return
*/
private Object doInsert(Object[] args, MapperHandler oldHandler, MapperHandler newHandler) {
}
private Object doUpdate(Object[] args, MapperHandler oldHandler, MapperHandler newHandler) {
}
private Object doSelect(Object[] args, MapperHandler oldHandler, MapperHandler newHandler) {
if (readNew()) {
return newHandler.execute(mDestSqlSession, args);
}
return oldHandler.execute(mSrcSqlSession, args);
}
/**
* 開關是否讀新表
* @return
*/
private boolean readNew() {
/**
* TODO
*/
return false;
}
/**
* 開關寫舊表
* @return
*/
private boolean writeOld() {
/**
* TODO
*/
return true;
}
/**
* 開關寫新表
* @return
*/
private boolean writeNew() {
/**
* TODO
*/
return true;
}
/**
* 開關先插入新表
* @return
*/
private boolean insertNewFirst() {
/**
* TODO
*/
return false;
}
private boolean partitionDb() {
return false;
}
private int getSequenceId() {
return 10000;
}
private String getStatementId(Method method) {
return method.getDeclaringClass().getName() + "." + method.getName();
}
private MappedStatement getMappedStatement(Method method, String statementId,
}
private MapperHandler cachedMapperMethod(Method method, Class<?> clazz,
Configuration configuration) {
}
}
很多細節我這里暫時就先略去了,大家可以自己思考下怎么寫,畢竟這個比較偏向業務,我這里 就是直接獲取 sqlSession, 然后我們可以借助 MapperSignature這個內部類來完成mybatis的接下來的工作
下面這里給出一個 invoke和insert方法的簡要實現 , 細節大家看下注釋我用的是java8
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args)
throws Throwable {
/**
* {@link Object#hashCode()} {@link #equals(Object)} 這些方法不做處理
* interface default實現不做處理
*/
if (Object.class.equals(method.getDeclaringClass()) || method.isDefault()) {
return method.invoke(this, args);
}
Configuration configuration = mSrcSqlSession.getConfiguration();
String statementId = getStatementId(method);
MappedStatement mappedStatement = getMappedStatement(method, statementId, configuration);
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
MapperHandler oldMapperHandler = cachedMapperMethod(method, OldUserMapper.class,
mSrcSqlSession.getConfiguration());
MapperHandler newMapperHandler = cachedMapperMethod(method, NewUserMapper.class,
mDestSqlSession.getConfiguration());
if (SqlCommandType.INSERT.equals(sqlCommandType)) {
return doInsert(args, oldMapperHandler, newMapperHandler);
} else if (SqlCommandType.UPDATE.equals(sqlCommandType)) {
return doUpdate(args, oldMapperHandler, newMapperHandler);
} else if (SqlCommandType.SELECT.equals(sqlCommandType)) {
return doSelect(args, oldMapperHandler, newMapperHandler);
}
/**
* 正常情況下不會有delete 這里進行異常判斷 根據業務場景進行處理
*/
return null;
}
/**
* 這里如果是單表遷移 可以考慮整體加分布式鎖,或者把主鍵自增的任務交給一個中間表
* 移交完成以后 再由中間表移交給新表
* @param args
* @param oldHandler
* @param newHandler
* @return
*/
private Object doInsert(Object[] args, MapperHandler oldHandler, MapperHandler newHandler) {
Object newRet, oldRet;
boolean enableOld = writeOld();
boolean enableNew = writeNew();
if (enableOld && enableNew) {
if (insertNewFirst()) {
/**
* 如果是分庫分表 我們從sequence里取出id
*/
if (partitionDb()) {
if (args[0] instanceof User) {
int id = getSequenceId();
((User) args[0]).setUserId(id);
}
}
newRet = newHandler.execute(mDestSqlSession, args);
oldRet = oldHandler.execute(mSrcSqlSession, args);
/**
* 這里可以做比較
*/
return newRet;
} else 「
oldRet = oldHandler.execute(mSrcSqlSession, args);
newRet = newHandler.execute(mDestSqlSession, args);
/**
* 這里可以做比較
*/
return oldRet;
}
} else if (enableNew) {
return newHandler.execute(mDestSqlSession, args);
}
return oldHandler.execute(mSrcSqlSession, args);
}
mybatis xml插入的時候 判斷下userId是否為null 是null就自增否則直接插入
<insert id="addUser" parameterType="com.qunhe.instdeco.partition.data.User" useGeneratedKeys="true">
INSERT INTO user
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="user.userId != null">
user_id,
</if>
<if test="user.name != null">
username,
</if>
<if test="user.age != null">
age,
</if>
</trim>
VALUES
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="user.userId != null">
#{user.userId},
</if>
<if test="user.name != null">
#{user.name},
</if>
<if test="user.age != null">
#{user.age},
</if>
</trim>
</insert>
這樣一來的話 原有的業務代碼里面基本不需要我們自行修改代碼,我這里其實還省略了很多的細節,酷家樂業務里面查詢的時候如果是分庫分表的話,對于分表鍵批量查詢其實多的時候可以采用 ElasticSearch來查,這里就需要判斷 分表鍵的參數的數量 需要 methodSignature去把 Object[] args轉換為 ParamMap 其實就是一個hashMap大家自己去看下mybatis這部分源碼就知道了.