用户在服务器web前端增加、修改、删除了数据后会导致客户端的数据与服务器端的数据不一致。为了能够使客户端和服务器端的数据一致,客户端需要同步服务器端的这些操作。主要的步骤为:”服务器端修改数据”、“客户端同步服务器端的数据”、“客户端同步完数据后,反馈给服务器””和“”服务器收到客户端的反馈信息,标记客户端已经同步该数据,不用再同步了”。

1、    服务器端修改数据。
以本产品修改商品为例,服务器端修改数据后,在数据库插入一条同步表记录:

// 更新同步对象的DB表
		int objSequence = getSequence();
		List<List<BaseModel>> list = updateSyncDBObject(useSYSTEM, posID, req, bmFromDB.getID(), objSequence, SyncCache.SYNC_Type_C, ec, dbName);
	getSequence方法返回同步顺序sequence:
	@Override
	protected int getSequence() {
		return aiSequenceSyncBlockForCommodityAndBarcodes.incrementAndGet();
	}

		/** 不断自增,标记同步块在客户端同步的顺序。<br />
	 * 客户端收到N个同步块后,先排序,再逐个将这些块写入客户端的DB。<br />
	 * 多线程安全问题:本变量不需要加锁,因为每个ACTION都是互斥运行的。<br />
	 * 缺陷:理论上存在过大溢出的bug。实际上在产品的迭代过程中,不可能会有机会溢出 */
	protected static AtomicInteger aiSequenceSyncBlockForCommodityAndBarcodes = new AtomicInteger();

updateSyncDBObject方法,vscCreate对象设置了三个属性:

syncData_ID:修改的商品的ID

syncType:同步类型,有C、U、D三种类型,代表创建、修改和删除了数据。

objSequence:序列号,设置同步的顺序。一个商品可能会被修改了多次,所以要设置同步的顺序,值越小,代表越早修改,就应先让客户端同步。

/** 更新DB中的同步对象
	 * 
	 * @param validPosID
	 *            必须为有效的POS的ID或BaseAction.INVALID_POS_ID
	 * @param objSequence
	 *            POS端收到N个同步块后,同步此块的顺序 */
	protected List<List<BaseModel>> updateSyncDBObject(Boolean useSYSTEM, int validPosID, HttpServletRequest req, int iBaseModelID, int objSequence, String syncType, ErrorInfo ec, String dbName) {
		logger.info("更新DB中的同步对象");

		BaseSyncCache vscCreate = getSyncCacheModel();
		vscCreate.setSyncData_ID(iBaseModelID);
		vscCreate.setSyncType(syncType);
		vscCreate.setSyncSequence(objSequence);
		vscCreate.setPosID(validPosID);
		//
		DataSourceContextHolder.setDbName(dbName);
		List<List<BaseModel>> list = getSyncCacheBO().createObjectEx((useSYSTEM ? BaseBO.SYSTEM : getStaffFromSession(req.getSession()).getID()), BaseBO.INVALID_CASE_ID, vscCreate);
		ec.setErrorCode(getSyncCacheBO().getLastErrorCode());
		ec.setErrorMessage(getSyncCacheBO().getLastErrorMessage());
		if (getSyncCacheBO().getLastErrorCode() != EnumErrorCode.EC_NoError) {
			logger.info("创建DB中的同步对象失败,错误码=" + ec.getErrorCode() + ",错误信息=" + ec.getErrorMessage());
			return null;
		} else {
			logger.info("同步对象的DB表插入的数据为:" + list);
		}

		return list;
	}

调用存储过程,主要sql语句如下(查询是否有之前的’U’型未同步,如果有则删除同步主从表,插入最新的‘U’型同步,同步最新的修改操作即可):

SELECT F_ID INTO iMasterID FROM T_commoditySyncCache WHERE F_SyncData_ID = iSyncData_ID AND F_SyncType = 'U';
		
		IF (iMasterID IS NOT NULL) THEN 
			
			DELETE FROM T_commoditySyncCacheDispatcher WHERE F_SyncCacheID = iMasterID ;
			DELETE FROM T_commoditySyncCache WHERE F_ID = iMasterID;
	   
		END IF;		
		   	
		INSERT INTO T_commoditySyncCache (F_SyncData_ID, F_SyncSequence, F_SyncType) VALUES (iSyncData_ID, iSyncSequence, cSyncType);
		SELECT F_ID, F_SyncData_ID, F_SyncSequence, F_SyncType,F_CreateDatetime FROM T_commoditySyncCache WHERE F_ID = LAST_INSERT_ID();
		
		IF iPOSID > 0 THEN	-- 网页端创建商品时,iPOSID <= 0
			INSERT INTO T_CommoditySyncCacheDispatcher (F_SyncCacheID, F_POS_ID) VALUES (LAST_INSERT_ID(), iPOSID);
			SELECT F_ID, F_SyncCacheID, F_POS_ID,F_CreateDatetime FROM T_CommoditySyncCacheDispatcher WHERE F_ID = LAST_INSERT_ID();
		END IF;
		
		SET iErrorCode := 0;	
		SET sErrorMsg := '';

在T_commoditySyncCache表中查找是否有F_SyncType = 'U'的记录,叫做U型同步块,U代表update更新,如果不为空,则删除记录,并且删除同步从表T_commoditySyncCacheDispatcher对应的记录。

将F_SyncData_ID、F_SyncSequence、F_SyncType插入到T_commoditySyncCache同步表中。

修改了商品ID为291,在t_commoditySyncCache插入的数据如下:

java同步助手 java同步数据_mysql

2、客户端同步服务器端的数据。

由于服务器端对商品ID为291的数据进行了修改,所以客户端需要同步这个修改。

客户端请求服务器端,查询是否有需要同步的数据:

private void retrieveNCommodity() {
    commoditySQLiteEvent.setEventTypeSQLite(BaseSQLiteEvent.EnumSQLiteEventType.ESET_Commodity_RefreshByServerDataAsync_Done);
    if (!commodityHttpBO.retrieveNAsync(BaseHttpBO.INVALID_CASE_ID, null)) {
        log.error("POS机启动时,同步商品失败!");
        Toast.makeText(this, "POS机启动时,同步商品失败!", Toast.LENGTH_SHORT).show();
    }
}

@Override
protected boolean doRetrieveNAsync(int iUseCaseID, BaseModel bm) {
    log.info("正在执行CommodityHttpBO的retrieveNAsync,bm=" + (bm == null ? null : bm.toString()));

    httpEvent.setEventProcessed(false);

    Request req = new Request.Builder()
            .url(Configuration.HTTP_IP + Commodity.HTTP_Commodity_RETRIEVEN)
            .addHeader(BaseHttpBO.COOKIE, GlobalController.getInstance().getSessionID())
            .build();
    HttpRequestUnit hru = new RetrieveNCommodity();
    hru.setRequest(req);
    hru.setTimeout(TIME_OUT);
    hru.setbPostEventToUI(true);
    httpEvent.setStatus(BaseEvent.EnumEventStatus.EES_Http_ToDo);
    hru.setEvent(httpEvent);
    HttpRequestManager.getCache(HttpRequestManager.EnumDomainType.EDT_Communication).pushHttpRequest(hru);

    log.info("正在请求需要同步的Commodity");

    return true;
}

服务器端查找商品同步表的所有记录:

final List<BaseModel> vsInfoList = SyncCacheManager.getCache(dbName, getSyncCacheType()).readN(false, false);

调用存储过程,sql语句如下:

SELECT F_ID, F_SyncSequence, F_SyncData_ID, F_SyncType FROM t_Commoditysynccache ORDER BY F_ID DESC;
		SET iTotalRecord := found_rows();
		SET iErrorCode := 0;
			SET sErrorMsg := '';

POS收银机已经同步的数据就不需要再同步:

/** 查询posID是否同步了块blockID <br />
	 * 前提条件:所有同存都已经加载在内存中。 <br />
	 * 因为所有同存一开始就全部加载到了内存中,后来也会不断更新,所以这个前提条件是满足的
	 * 
	 * @param blockID
	 *            块的ID
	 * @return */
	public boolean checkIfPosSyncThisBlock(int blockID, int posID) {
		boolean bSync = false;

		System.out.println("checkIfPosSyncThisBlock正在加锁....");
		lock.readLock().lock();

		// TODO:
		// 目前hashtable存储的是syncInfo,无法根据块的ID迅速定位到块及其从表信息。将来重构成去掉syncInfo,将块ID作为hashtable的key。而块内部增加一个成员,其值为从表的指针,通过主表能拿到它全部从表的信息
		for (Iterator<String> it = getCache().keySet().iterator(); it.hasNext();) {
			BaseSyncCache syncCache = (BaseSyncCache) getCache().get(it.next());
			if (syncCache.getID() == blockID) {
				System.out.println(syncCache.getListSlave1());
				if (syncCache.getListSlave1() == null) {
					break;
				}
				for (Object o : syncCache.getListSlave1()) {
					if (((BaseSyncCacheDispatcher) o).getPos_ID() == posID) {
						bSync = true;
						break;
					}
				}
			}
		}

		lock.readLock().unlock();
		System.out.println("checkIfPosSyncThisBlock已经解锁....");

		return bSync;
	}

根据syncDataID查询出数据库中的商品信息:

BaseModel v = getModel();
				v.setID(syncCache.getSyncData_ID());
				BaseModel bmTmp = null;
				if (!syncCache.getSyncType().equals(SyncCache.SYNC_Type_D)) {
					logger.info("->非D型同步");
					DataSourceContextHolder.setDbName(dbName);
					List<List<BaseModel>> modelR1 = getModelBO().retrieve1ObjectEx(BaseBO.SYSTEM, BaseBO.INVALID_CASE_ID, v); // 系统初始化时需要加载所有同存DB
					
……
					bmTmp = Commodity.fetchCommodityFromResultSet(modelR1);
					logger.info("返回的普存对象成功:" + bmTmp);

将所有需要同步的商品信息保存到list集合,返回给客户端:

bmTmp.setSyncDatetime(new Date());
					bmList.add(bmTmp);

客户端得到要同步的列表,先按sequence对同步数据进行排序:

int maxSyncSequence = 0;
for (int i = 0; i < commodityList.size(); i++) {
    System.out.println(commodityList.get(i).getSyncSequence());
    if (commodityList.get(i).getSyncSequence() > maxSyncSequence) {
        maxSyncSequence = commodityList.get(i).getSyncSequence();
    }
}

//生成一个根据SyncSequence升序排列的List
List<BaseModel> orderCommodityList = new ArrayList<BaseModel>();
for (int i = 0; i < maxSyncSequence; i++) {
    for (int j = 0; j < commodityList.size(); j++) {
        if (i + 1 == commodityList.get(j).getSyncSequence()) {
            orderCommodityList.add(commodityList.get(j));
        }
    }
}

将sqlite数据库表的商品一个一个更新:

else if (BasePresenter.SYNC_Type_U.equals(commodityType)) {//假如返回的string1==U,则修改SQLite中对应的数据
    updateSync(iUseCaseID, bmList.get(i));

    log.info("服务器返回的U型数据成功同步到本地SQLite中!");


bm.setSyncDatetime(new Date(System.currentTimeMillis() + NtpHttpBO.TimeDifference));
bm.setSyncType(BasePresenter.SYNC_Type_U);
dao.getCommodityDao().update((Commodity) bm);

3、客户端同步完数据后,反馈给服务器。

将已经同步好的商品反馈feedback给服务器端,下次就不需要再同步了:

/**
 * 在feedback之前做,判断是否有需要feedback的数据,若有就feedback,若无,则retrieveN下一个model
 *
 * @param event             当前sqliteEvent
 * @param syncIDs
 * @param feedbackDataCase  当前需要进行feedback的model
 * @param retrieveNDataCase 下一下需要RetrieveN的model
 */
private void beforeFeedback(BaseSQLiteEvent event, String syncIDs, String feedbackDataCase, String retrieveNDataCase) {
    if (event.getStatus() == BaseEvent.EnumEventStatus.EES_SQLite_DoneApplyServerData) {
        event.setStatus(BaseEvent.EnumEventStatus.EES_SQLite_ToApplyServerData);
        List<BaseModel> list = (List<BaseModel>) event.getListMasterTable();
        //
        if (list != null) {
            if (list.size() > 0) {
                for (int i = 0; i < list.size(); i++) {
                    syncIDs = syncIDs + "," + list.get(i).getID();
                }
                syncIDs = syncIDs.substring(1, syncIDs.length());
                //
                onFeedback(feedbackDataCase, syncIDs);
            }
        } else {
            onRetrieveN(retrieveNDataCase);
        }
    }
}

case CommodityData:
    feedbackCommodity(ids);
    break;

private void feedbackCommodity(String ids) {
    if (!commodityHttpBO.feedback(ids)) {
        log.error("POS机启动时,feedback Commodity失败!");
    }
}

@Override
public boolean feedback(String feedbackIDs) {
    log.info("正在执行CommodityHttpBO的feedback,feedbackIDs=" + feedbackIDs);

    httpEvent.setEventProcessed(false);

    Request req = new Request.Builder()
            .url(Configuration.HTTP_IP + Commodity.commodityfeedbackURL_FRONT + feedbackIDs + Commodity.commodityfeedbackURL_BEHIND)
            .addHeader(BaseHttpBO.COOKIE, GlobalController.getInstance().getSessionID())
            .build();
    HttpRequestUnit hru = new FeedBack();
    hru.setRequest(req);
    hru.setTimeout(TIME_OUT);
    hru.setbPostEventToUI(true);
    httpEvent.setStatus(BaseEvent.EnumEventStatus.EES_Http_ToDo);
    httpEvent.setHttpBO(this);
    hru.setEvent(httpEvent);
    HttpRequestManager.getCache(HttpRequestManager.EnumDomainType.EDT_Communication).pushHttpRequest(hru);

    log.info("通知服务器改POS机已经同步了commodityID:" + feedbackIDs);

    return true;
}

4、服务器收到客户端的反馈信息,标记客户端已经同步该数据,不用再同步了。

服务器从请求体中获取反馈回来的ID集合ids:

String ids = GetStringFromRequest(req, "sID", String.valueOf(BaseAction.INVALID_ID)).trim();
		String errorCode = GetStringFromRequest(req, "errorCode", String.valueOf(BaseAction.INVALID_ID)).trim();

排序并删除重复值:

Integer[] iArrID = GeneralUtil.toIntArray(ids);
			List<Integer> listObjID = GeneralUtil.sortAndDeleteDuplicated(iArrID);

更新同步从表t_commoditysynccachedispatcher:

if (updateSyncCacheAfterPOSSync(useSYSTEM, posID, listObjID, dbName, req) != EnumErrorCode.EC_NoError) {
					String log = "操作失败:记录POS" + posID + "已经同步了块(对应的对象ID={" + iArrID.toString() + "}),通过更新同存及同存DB";
					logger.info(log);
					param.put(BaseAction.JSON_ERROR_KEY, EnumErrorCode.EC_OtherError);
					param.put(KEY_HTMLTable_Parameter_msg, log);
					return param;
				}

	/** 记录POS已经同步了某些块,通过更新同存及同存DB
	 * 
	 * @param iPosID
	 *            同步了块的POS机
	 * @param iArrID
	 *            同步块对应的对象的ID数组(不是同步块ID)
	 * @return */
	protected EnumErrorCode updateSyncCacheAfterPOSSync(Boolean useSYSTEM, int iPosID, List<Integer> listObjID, String dbName, HttpServletRequest req) {
		List<BaseModel> listSyncCache = SyncCacheManager.getCache(dbName, getSyncCacheType()).readN(false, false);
		logger.info("listObjID为:" + listObjID);
		logger.info("listSyncInfo为:" + listSyncCache);
		for (BaseModel bm : listSyncCache) {
			BaseSyncCache syncCache = (BaseSyncCache) bm;

			for (int iID : listObjID) {
				if (syncCache.getSyncData_ID() == iID) {
					// 更新同存DB。在同存DB中记录此块已经被此POS同步
					BaseSyncCacheDispatcher vscd = getSyncCacheDispatcher();
					vscd.setPos_ID(iPosID);
					vscd.setSyncCacheID(syncCache.getID());
					//
					DataSourceContextHolder.setDbName(dbName);
					vscd = (BaseSyncCacheDispatcher) getSyncCacheDispatcherBO().createObject((useSYSTEM ? BaseBO.SYSTEM : getStaffFromSession(req.getSession()).getID()), BaseBO.INVALID_CASE_ID, vscd);
					if (getSyncCacheDispatcherBO().getLastErrorCode() != EnumErrorCode.EC_NoError) {
						// ...
						logger.info("同存DB更新失败,块ID=" + vscd + ", 对象ID=" + iID + ", POS ID=" + iPosID + "\t错误码:" + getSyncCacheDispatcherBO().getLastErrorCode());
						return getSyncCacheDispatcherBO().getLastErrorCode();
					}
					logger.info("同存DB已经成功更新,对象ID=" + iID + ", POS ID=" + iPosID + "。块信息:" + syncCache);
				}
			}
		}

		return EnumErrorCode.EC_NoError;
	}

调用存储过程,向同步从表中插入一条记录并返回:

IF EXISTS(SELECT 1 FROM t_Commoditysynccachedispatcher WHERE F_SyncCacheID = iSyncCacheID AND F_POS_ID = iPOS_ID) THEN
	  		SELECT F_ID, F_SyncCacheID, F_POS_ID FROM t_Commoditysynccachedispatcher WHERE F_SyncCacheID = iSyncCacheID AND F_POS_ID = iPOS_ID;
			SET iErrorCode := 0;
			SET sErrorMsg := '';
		ELSE
			INSERT INTO t_Commoditysynccachedispatcher (F_SyncCacheID, F_POS_ID) VALUES (iSyncCacheID, iPOS_ID);
			
			SELECT F_ID, F_SyncCacheID, F_POS_ID,F_CreateDatetime FROM t_Commoditysynccachedispatcher WHERE F_ID = LAST_INSERT_ID();
	
			SET iErrorCode := 0;
			SET sErrorMsg := '';
		END IF;

如下图,posID为3的收银机同步好了服务端的一条数据,syncCacheID指向了同步主表的ID:

java同步助手 java同步数据_数据库_02

判断是否所有POS收银机都已经同步完这个操作,如果是,则删除同步表和同步从表,不用再提醒POS收银机同步了:

// 如果所有POS机都已经同步了这个块,那就从同存及同存DB表中都删除这个块的相关信息
				deleteSyncCacheObjectsSinceAllPOSSync(useSYSTEM, iArrID, dbName, req);

	/** 删除同存DB和同存。 <br />
	 * 当所有POS机都已经同步了某个块,则应当删除这个块相关的同存DB及同存。
	 * 
	 * @param iArrID
	 *            块ID的数组 */
	protected void deleteSyncCacheObjectsSinceAllPOSSync(Boolean useSYSTEM, Integer[] iArrID, String dbName, HttpServletRequest req) {
		logger.info("删除同存DB和同存,iArrID=" + iArrID);

		for (int iID : iArrID) {
			BaseSyncCache bsc = getSyncCacheModel();
			bsc.setSyncData_ID(iID);

			DataSourceContextHolder.setDbName(dbName);
			getSyncCacheBO().deleteObject((useSYSTEM ? BaseBO.SYSTEM : getStaffFromSession(req.getSession()).getID()), BaseBO.INVALID_CASE_ID, bsc);
			if (getSyncCacheBO().getLastErrorCode() == EnumErrorCode.EC_NoError) {// 表明所有POS机都已经同步了这个块,应当删除相关的同存
				BaseModel bm = getModel();
				bm.setID(iID);
				SyncCacheManager.getCache(dbName, getSyncCacheType()).delete1(bm);
				System.out.println("所有POS机都已经同步了这个块" + iID + ",删除了相关的同存。删除块" + iID + "相关同存的错误码=" + getSyncCacheBO().getLastErrorCode());
				logger.info("所有POS机都已经同步了这个块" + iID + ",删除了相关的同存。删除块" + iID + "相关同存的错误码=" + getSyncCacheBO().getLastErrorCode());
			} else {
				System.out.println("并非所有POS机都已经同步了这个块ID=" + iID + ",所以并未删除这个块相关的同存DB数据。");
				logger.info("并非所有POS机都已经同步了这个块ID=" + iID + ",所以并未删除这个块相关的同存DB数据。");
			}
		}
	}

调用存储过程:

IF (SELECT COUNT(1) FROM t_pos WHERE F_Status = 0) = (SELECT COUNT(1) FROM T_CommoditySyncCacheDispatcher WHERE F_SyncCacheID 
			IN (SELECT F_ID FROM T_CommoditySyncCache WHERE F_SyncData_ID = iSyncData_ID)) THEN
			
			DELETE FROM T_CommoditySyncCacheDispatcher WHERE F_SyncCacheID IN (SELECT F_ID FROM T_CommoditySyncCache WHERE F_SyncData_ID = iSyncData_ID);
			DELETE FROM T_CommoditySyncCache WHERE F_SyncData_ID =  iSyncData_ID;
			SET iErrorCode := 0;
			SET sErrorMsg := '';
		ELSE 
			SET iErrorCode := 7;
			SET sErrorMsg := 'POS机还没全部同步';
		END IF;