import java.lang.management.ManagementFactory;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.springframework.util.CollectionUtils;
import com.hengyunsoft.data.sync.client.IIncrementDataSync;
import com.hengyunsoft.data.sync.po.CommonKVQuery;
/**
* 此类主要是实现同步的主体框架,偏向于算法实现,具体的使用什么协议进行通讯,请看 {@link RestTemplateDataSyncImpl}<br/>
*
* @author 我很好[1064319393@qq.com]
* @version 1.0.0
* <br/>这是基于是单线程来执行同步 千万不允许多线程执行 多线程执行太难写了 放弃吧!!!!!
* 这里我们考虑有时间因素、以及mysql mvcc非锁定读的因素。
* 时间因素我们这样解决:1、以主服务器的时间为准。而非本地时间。本地时间快与慢不会影响同步功能
* 2、主服务器可以调整它的时间,可以向前(改小时间)或向后(改大时间)调整,程序都是支持的。
* 假设我们的同步时间是1小时同步一次,假设事物最大持续时间是5分钟(就是没有事物可以超过5分钟而不结束的)。假设当前主服务器时间
* 是: 08:00 , 那么上次同步时间是:06:55,下次同步时间是07:55
* 2.1、向前调整:假设向前调整5小时,那么是 03:00 , 当我们同步的时候,获取主服务器的时间是 X ( 03:00 <= X <= 04:00) 因为我们的同步时间间隔是1小时
* 假设X=03:56 , 那么min(07:55,03:56-01:00-00:05)=02:51 那么我们此次的同步时间将会是:02:51,那么是正确同步时间。
* 下次认为的上次同步时间将会是:02:51。
* 2.2、向后调整:假设向后调整5小时,那么是13:00, 当我们同步的时候,获取主服务器的时间是:Y (13:00 <= Y <= 14:00) 同理。
* 假设Y=13:56,那么min(07:55,13:56-01:00-00:05)=07:55 那么我们此次的同步时间将会是:07:55,那么是正确同步时间。
* 下次认为的上次同步时间将会是:13:56-01:00-00:05 = 12:51 这个时候时间就一下进步了很多了(跟上主服务器的时间步伐)。
*
* 由于我想要完全避免使用本地时间进行计算(因为线上的时间不准确导致时间会被管理员调来调去的)。
*
* 同步时间间隔完全可以通过前一次执行时刻与此次执行时刻计算出来。但是避免使用获取系统时间的方式(危害大)。
* 通过获取虚拟机已经运行时间(毫秒): 有个好处是不随着系统时间的改变而改变。他是记录虚拟机运行了多久
* ManagementFactory.getRuntimeMXBean().getUptime()
*
* <T>是id类型
*/
public abstract class BaseDataSyncImpl implements IIncrementDataSync {
//同步时间间隔 可以稍微大点(比真实在定时任务的执行中的间隔大,但是千万别小于他,等于定时任务执行间隔最好)
private volatile long sync_time_interval_in_milsecond ;
/**
* 上次同步的时候,jvm虚拟机运行时间
*/
private volatile long up_sync_jvm_run_milsecond = -1;
private volatile long this_sync_jvm_run_milsecond ;
//事物处理最长时间 建议同步时间间隔大于此时间
private long tx_time_out_in_milsecond = 5*60*1000;
//下次同步时间
private volatile Long next_sync_time = null;
//本次同步时间
private volatile Long this_sync_time = null;
private int deleteLimit = 1000;
/**
* 当做锁的功能,记录是否在进行同步。只允许一个线程进行同步。
*/
private AtomicBoolean runing = new AtomicBoolean(false);
/**
* 执行总体架构
*/
@Override
public final void startSync() {
//单线程同步
if(!runing.compareAndSet(false, true)) {
//正在同步中,不能够同时进行两次同步,就是加锁操作
return ;
}
sync_time_interval_in_milsecond = getSyncTimeIntervalByJvmruntiem();
try {
//获取同步时间 与主服务器商定同步时间
long nowSyncTime = getSyncTime();
//清楚同步标记 准备开始同步数据,只需要清楚要被同步部分数据的同步标记,
//就是更新时间在nowSyncTime之后的那部分数据
cleanSyncFlag(nowSyncTime);
//开始数据同步
syncDatas(nowSyncTime);
//同步数据仅仅解决更新与插入的问题 这里去解决删除的问题
//有些表不会存在删除操作,这里对那些不需要删除的表直接跳过
if(isNeedDel)
syncDel();
//这个放到最后 怕事物回滚 而时间没有被回滚 导致下次同步时,next_sync_time不正确
updateNextSyncTime();
} catch (Exception e) {
e.printStackTrace();
} finally {
//允许下次的同步 解锁操作
runing.set(false);
}
}
/**
* 计算这次同步与上次同步的时间间隔
* @return
*/
private long getSyncTimeIntervalByJvmruntiem() {
//获取虚拟机已经运行时间(毫秒)
//有个好处是不随着系统时间的改变而改变。他是记录虚拟机运行了多久。
this_sync_jvm_run_milsecond = ManagementFactory.getRuntimeMXBean().getUptime();
if(up_sync_jvm_run_milsecond == -1) {
//启动后的第一次同步,
return this_sync_jvm_run_milsecond ;
}
return this_sync_jvm_run_milsecond - up_sync_jvm_run_milsecond;
}
/**
* 清楚同步标记 准备开始同步数据
* 只需要清楚要被同步部分数据的同步标记,就是更新时间在nowSyncTime之后的那部分数据
* @param nowSyncTime
*/
protected abstract void cleanSyncFlag(long nowSyncTime);
//一定要在最后来更新这个时间
protected void updateNextSyncTime(){
long next_sync_time = this_sync_time + sync_time_interval_in_milsecond;
//先保存下次同步时间
saveDbNext_sync_time(next_sync_time);
//在更新内存时间 怕保存数据库失败而回滚,内存数据与数据库数据不一致
this.next_sync_time = next_sync_time;
up_sync_jvm_run_milsecond = this_sync_jvm_run_milsecond;
}
/**
* 同步删除操作,将在主服务器中删除的数据也在本地进行删除
*/
private void syncDel() {
//1、本地取全部id集合的摘要 MD5,以及记录数 拿去远程比较,相等则啥都不做
CommonKVQuery<String,Integer> abstractAndCount = getLocalIdsAbstractAndCount();
boolean isMach = isMachMasterServer(abstractAndCount);
if(isMach) {
return ;
}
//清楚本地删除标记 准备进入删除
cleanLocalDelFlag();
//2、把本地的数据按照id进行分页拿到远程去对比,没有则拿回来进行删除 。
List<?> ids = null;
List<?> delIds = null;
do {
ids = updateLocalIdsToDeleteFlagAndGet(deleteLimit);
if(CollectionUtils.isEmpty(ids)) {
break ;
}
delIds = getNeedDelIdsFromMasterServer(ids);
if(!CollectionUtils.isEmpty(delIds)) {
deleteLocalByIds(delIds);
}
} while (ids.size() == deleteLimit);
}
/**
* 清楚本地删除标记 准备进入同步删除操作
*/
protected abstract void cleanLocalDelFlag() ;
/**
* 删除本地的数据 通过id集合
* 不需要删除的,则无需实现
* @param delIds
*/
protected abstract void deleteLocalByIds(List<?> delIds);
/**
* 去远处匹配 找出需要删除的id集合
* @param ids
* @return
*/
protected abstract List<?> getNeedDelIdsFromMasterServer(List ids);
/**
* 分页获取本地id集合,并且将返回的id集合都标记为已加入删除查询,下次查询将不再查出。
* 不需要删除的,则无需实现
* @param pageRequest
* @return
*/
protected abstract List<?> updateLocalIdsToDeleteFlagAndGet(int limit);
/**
* 去主服务器匹配摘要及记录数。
* 匹配维度是二维: 1. 数据行数 2.主键摘要
* @param abstractAndCount
* @return
*/
protected abstract boolean isMachMasterServer(CommonKVQuery<String,Integer> abstractAndCount);
//获取本地id集合摘要及记录数量
protected abstract CommonKVQuery<String,Integer> getLocalIdsAbstractAndCount() ;
//同步数据 更新时间在指定的时间之后的数据进行更新
protected abstract void syncDatas(long nowSyncTime) ;
private long getSyncTime(){
final long masterServerTime = getCurMasterServerTime();
if(next_sync_time == null){
//只有在启动的时候,下次同步时间才会为null;
Long dbNext_sync_time = getDbNext_sync_time();
if(dbNext_sync_time != null && dbNext_sync_time > 0) {
//1. 同步过,下次同步时间可以从数据库中取出
next_sync_time = dbNext_sync_time ;
} else {
//2. 从来没有同步过,从数据库中取不出,那么来一次全量同步
this_sync_time = masterServerTime - sync_time_interval_in_milsecond-tx_time_out_in_milsecond;
//返回0表示来一次全量同步;而this_sync_time等于上面的式子,
//是因为在进行下次同步时,让其跟上主服务器的时间
return 0l;
}
}
//min(主服务器时间 - 同步时间间隔(1小时) - 最大事物超时时间(5分钟),上次商定的时间 + 同步时间间隔)
//这里的5分钟我考虑的是最大事物的用时。就是假定所有事物的时间长度不可以超过5分钟。
//因为我们在程序中经常是先设置更新时间,然后插入数据库,然后再做些别的(浪费了一些时间),
//最后提交了事物。那么根据mvcc模式,非锁定读,是读快照。导致更新时间本应该在本次同步中被同步的,而并没有同步到
//(不可见),而下一次的同步时间又大于了这个更新时间。导致会丢失更新。所以每次同步,都多同步5分钟的数据。
//就怕丢下这种间隙中的数据。
this_sync_time = Math.min(next_sync_time ,
masterServerTime-sync_time_interval_in_milsecond-tx_time_out_in_milsecond) ;
final long result = Math.max(0,this_sync_time);
//这里的这一次同步时间取值是 主服务器时间-同步时间间隔-事物最大超时时间
//而舍弃了next_sync_time 这个取值,原因在于让下一次的更新跟上主服务器的时间,不要距离太远
this_sync_time = masterServerTime - sync_time_interval_in_milsecond-tx_time_out_in_milsecond;
//这里 result 不一定等于 this_sync_time
return result;
}
/**
* 每一次同步完成后,都会将下次的同步时间写入到数据库,所以在启动的时候,我们可以去数据库读取下次的同步时间;
* 以此作为基准继续进行增量同步;至于怎么保存是实现说了算了
* @return
*/
protected abstract Long getDbNext_sync_time() ;
/**
* @see #getDbNext_sync_time()
* @return
*/
protected abstract void saveDbNext_sync_time(long next_sync_time) ;
/**
* 获取主服务器的当前时间
* @return
*/
protected abstract long getCurMasterServerTime();
/**
* 表数据是否需要删除操作,不会删除,则可以减少去同步被删的数据
*/
private boolean isNeedDel = false;
public void setTx_time_out_in_milsecond(long tx_time_out_in_milsecond) {
this.tx_time_out_in_milsecond = tx_time_out_in_milsecond;
}
public void setNeedDel(boolean needDel) {
isNeedDel = needDel;
}
}