用户在服务器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插入的数据如下:
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:
判断是否所有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;