`
jaychang
  • 浏览: 734811 次
  • 性别: Icon_minigender_1
  • 来自: 嘉兴
社区版块
存档分类
最新评论

淘宝订单同步方案 - 丢单终结者

 
阅读更多

 淘宝订单同步方案 - 丢单终结者

 
订单管理是很多卖家工具的必备功能之一,而订单同步则是订单管理中的数据来源,如何保证订单同步的实时、高效、低碳和不丢单是非常重要的事情。

订单同步接口
1.    taobao.trades.sold.get,根据订单创建时间查询3个月内已卖出的订单。
2.    taobao.trades.sold.increment.get,根据订单修改时间查询1天内的增量订单。
3.    taobao.trade.fullinfo.get,根据订单ID查询订单的详细信息。

丢单原因分析
一、没有检查订单同步接口的返回值是否成功。
二、只使用taobao.trades.sold.get同步订单,此接口是按照订单创建时间查询的,一个订单创建后何时被修改(付款、发货、确认收货)是不确定的,所以采用这种方案无法确定该同步哪个时段内的订单,除非你每次都同步3个月内的订单(严重浪费资源,应该没人会这么做),否则不管选择什么时段同步都有丢单的可能。
三、没有记录每次订单同步成功后的时间点。比如每10分钟增量同步一次订单,如果系统恰好在某个同步时刻出现异常,则这次的同步就有可能被中止。
四、整点误差(时/分/秒)。比如每10分钟增量同步一次订单:第一次同步00:00:00 ~ 00:10:00时段的订单,第二次同步00:10:01 ~ 00:20:00时段的订单。这种方式就有可能丢失00:10:00的一部分订单,特别是店铺参加聚划算活动时更容易出现。
五、按状态同步订单,这种方式的问题在于订单状态过多,有可能会出现状态遗漏,而且性能低效。

推荐同步方案
同步流程图


 

流程图解释
1.    用户第一次登录时使用taobao.trades.sold.get同步3个月内的订单,并把用户登录的时间做为之后增量同步的时间起点。
2.    同时后台启动定时任务进行增量订单同步,根据店铺订单量的不同和客户来访时间,可设置不同的同步频率,每次增量同步完毕后,需要把增量同步的时间点记录下来,以做为下次增量同步的起点。

订单同步技巧
1.    使用taobao.trades.sold.get同步3个月内的订单时,最好把3个月分隔成若干个时段来查询,否则很容易出现超时。由于订单的创建时间不会变化,所以分页时从前翻页还是从后面翻页都无所谓(前提是翻页的过程中不能改变查询时间)。
2.    使用taobao.trades.sold.increment.get增量同步订单时,查询到的订单是按修改时间倒序返回的,所以分页时必须从最后一页开始翻页,否则有可能出现丢单。这是因为如果从第一页开始翻页,则翻页过程中发生变更的订单就会减少订单总数,使翻页出现误差。
3.    使用taobao.trades.sold.increment.get增量同步订单时,可以先通过只查询tid字段得到指定时段的订单总数,然后计算出分页数,后继采用倒序翻页时,设置use_has_next=true可以禁止API接口去统计订单总数,减少每次查询时都做统计的开销,可以大大提高查询性能。
4.    根据订单量的不同,需要采用不同的同步时段。对于日均订单量在1000左右的店铺,如果设置每页查询50条记录,每10分钟同步一次,则每次同步基本上只需要一次分页查询就能完成同步。
5.    时刻记录每次成功同步的时间点(比如存储到数据库中),避免重复劳动。
6.    对于用户量较大,实时性要求较高的应用,最好采用多线程同步的方式。可建立一个固定大小的线程池(需要根据硬件条件和网络状况不同设置不同的线程池大小),为每个用户启动一个线程去同步订单。
7.    由于API调用是有频率限制的,采用多线程同步订单时,有可能需要每次API调用后做一些短暂的停顿,以免调用超频,造成长时间不可访问API。
8.    如果批量订单查询返回的数据不够,需要通过订单详情接口获取时,强烈推荐批量查询订单时,只查询tid字段,然后通过taobao.trade.fullinfo.get查询订单详情。
9.    使用taobao.time.get获取的时间作为当前时间。否则,如果ISV服务器的时间比淘宝服务器的时间快,则有可能提前同步订单导致丢单。
10.    使用taobao.trades.sold.increment.get接口时,设置的查询时间段返回的总记录数最好不要超过2万,否则很容易发生超时。

特别提醒:针对光棍节大促,由于订单量很大,如果使用倒序需要返回total_results,建议大商家抓单时间间隔设置小于5分钟,使每次抓单尽量不要超过2万单,避免数目过多导致的性能和超时问题。


附JAVA示例代码:http://tbtop.googlecode.com/files/TradeSync.java
package com.taobao.top.tool;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.taobao.api.ApiException;
import com.taobao.api.DefaultTaobaoClient;
import com.taobao.api.TaobaoClient;
import com.taobao.api.domain.Trade;
import com.taobao.api.request.TimeGetRequest;
import com.taobao.api.request.TradeFullinfoGetRequest;
import com.taobao.api.request.TradesSoldGetRequest;
import com.taobao.api.request.TradesSoldIncrementGetRequest;
import com.taobao.api.response.TimeGetResponse;
import com.taobao.api.response.TradeFullinfoGetResponse;
import com.taobao.api.response.TradesSoldGetResponse;
import com.taobao.api.response.TradesSoldIncrementGetResponse;
import com.taobao.top.util.TestData;

public class TradeSync {

	private static final Log log = LogFactory.getLog(TradeSync.class);

	private static final String TOP_URL = TestData.ONLINE_SERVER_URL;
	private static final String APP_KEY = TestData.TEST_APP_KEY;
	private static final String APP_SECRET = TestData.TEST_APP_SECRET;
	private static final ExecutorService threadPool = Executors.newFixedThreadPool(12);
	private static final TaobaoClient client = new DefaultTaobaoClient(TOP_URL, APP_KEY, APP_SECRET);

	public static void main(String[] args) throws Exception {
		// 新用户登录后调用此方法
		// getLast3MonthSoldTrades(null);

		// 系统启动后创建此定时任务
		Timer timer = new Timer();
		timer.schedule(new TimerTask() {
			public void run() {
				// 每个卖家启动一个线程去同步增量订单
				final Date end = getTaobaoTime();
				List<UserInfo> users = getUsersFromDB();
				for (final UserInfo user : users) {
					final Date start = user.getLastSyncTime();
					threadPool.submit(new Runnable() {
						public void run() {
							try {
								getIncrementSoldTradesByPeriod(start, end, user.getSessionKey());
								user.setLastSyncTime(end);
								updateUserToDB(user);
							} catch (ApiException e) {
								log.error("同步" + user.getUserId() + "的增量订单失败:" + start + "-" + end, e);
							}
						}
					});
				}
			}
		}, 0, 1 * 60 * 1000L); // 每10分钟增量同步一次

		Thread.sleep(100000);
	}

	private static List<UserInfo> getUsersFromDB() {
		// TODO 从数据库中查询已授权的用户信息
		List<UserInfo> users = new ArrayList<UserInfo>();
		UserInfo user = new UserInfo();
		user.setUserId(123456789L);
		user.setSessionKey("410253676dfef08550cce6f76ac549da2e2a5679429OOd5HfMv88371");
		users.add(user);
		return users;
	}

	private static void updateUserToDB(UserInfo user) {
		// TODO 保存更新后的用户信息到数据库
	}

	/**
	 * 新用户登录后调用:同步三个月内的订单。
	 */
	public static void getLast3MonthSoldTrades(final UserInfo user) {
		Date end = getTaobaoTime();
		Date start = addMonths(end, -3); // 最多只能查询3个月内的订单
		// 切隔时间(公式为:24*每页记录数[推荐50]/日均订单量),如日均订单量为100的店铺,可按每24*50/100=12小时切割一段
		List<Date[]> dateList = splitTimeByHours(start, end, 24);
		for (final Date[] dates : dateList) {
			// 由于3个月的订单数量较大,建议采用多线程的方式同步,但是要注意APP的调用频率
			threadPool.submit(new Runnable() {
				public void run() {
					try {
						getSoldTradesByPeriod(dates[0], dates[1], user.getSessionKey());
					} catch (ApiException e) {
						log.error("同步" + user.getUserId() + "的已卖出订单失败:" + dates[0] + "-" + dates[1], e);
					}
				}
			});
		}
		// 把获取3个月内已卖出订单的结束时间做为下次增量订单同步的开始时间
		user.setLastSyncTime(end);
		updateUserToDB(user);
	}

	private static void getSoldTradesByPeriod(Date start, Date end, String sessionKey) throws ApiException {
		TradesSoldGetRequest req = new TradesSoldGetRequest();
		req.setFields("tid");
		req.setStartCreated(start);
		req.setEndCreated(end);
		req.setPageNo(1L);
		req.setPageSize(50L);

		long pageCount = 0L;
		TradesSoldGetResponse rsp = null;

		do {
			rsp = client.execute(req, sessionKey);
			log.info(rsp.getTotalResults() + "=>>" + req.getPageNo());
			if (rsp.isSuccess()) {
				for (Trade t : rsp.getTrades()) {
					Trade trade = getTradeFullInfo(t.getTid(), sessionKey);
					if (trade != null) {
						// TODO 更新订单数据到本地数据库
					}
				}

				req.setPageNo(req.getPageNo() + 1);
				pageCount = getPageCount(rsp.getTotalResults(), req.getPageSize());
			} else {
				// 错误响应直接重试
			}
		} while (req.getPageNo() <= pageCount);
	}

	/**
	 * 后台线程定时调用:增量同步订单。
	 */
	public static void getIncrementSoldTradesByPeriod(Date start, Date end, String sessionKey) throws ApiException {
		TradesSoldIncrementGetRequest req = new TradesSoldIncrementGetRequest();
		req.setFields("tid");
		req.setStartModified(start);
		req.setEndModified(end);
		req.setPageNo(1L);
		req.setPageSize(50L);
		TradesSoldIncrementGetResponse rsp = client.execute(req, sessionKey);
		if (rsp.isSuccess()) {
			long pageCount = getPageCount(rsp.getTotalResults(), req.getPageSize());
			while (pageCount > 0) {
				req.setPageNo(pageCount);
				req.setUseHasNext(true);
				rsp = client.execute(req, sessionKey);
				if (rsp.isSuccess()) {
					log.info(rsp.getTotalResults() + " >> " + req.getPageNo());
					for (Trade t : rsp.getTrades()) {
						Trade trade = getTradeFullInfo(t.getTid(), sessionKey);
						if (trade != null) {
							// TODO 更新订单数据到本地数据库
						}
					}
					pageCount--;
				} else {
					// 错误响应直接重试
				}
			}
		} else {
			getIncrementSoldTradesByPeriod(start, end, sessionKey);
		}
	}

	private static Trade getTradeFullInfo(Long tid, String sessionKey) throws ApiException {
		TradeFullinfoGetRequest req = new TradeFullinfoGetRequest();
		req.setFields("tid,buyer_nick,seller_nick,status,payment,created");
		req.setTid(tid);
		TradeFullinfoGetResponse rsp = client.execute(req, sessionKey);
		if (rsp.isSuccess()) {
			return rsp.getTrade();
		} else {
			// API服务不可用或超时,则重试
			if ("520".equals(rsp.getErrorCode())) {
				return getTradeFullInfo(tid, sessionKey);
			} else {
				log.error("查询订单详情失败:" + tid);
				return null;
			}
		}
	}

	/**
	 * 获取淘宝服务器时间作为当前时间,避免部分ISV机器时间提前时导致同步漏单现象。
	 */
	private static Date getTaobaoTime() {
		TimeGetRequest req = new TimeGetRequest();
		try {
			TimeGetResponse rsp = client.execute(req);
			if (rsp.isSuccess()) {
				return rsp.getTime();
			}
		} catch (ApiException e) {
		}
		return new Date();
	}

	private static List<Date[]> splitTimeByHours(Date start, Date end, int hours) {
		List<Date[]> dl = new ArrayList<Date[]>();
		while (start.compareTo(end) < 0) {
			Date _end = addHours(start, hours);
			if (_end.compareTo(end) > 0) {
				_end = end;
			}
			Date[] dates = new Date[] { (Date) start.clone(), (Date) _end.clone() };
			dl.add(dates);

			start = _end;
		}
		return dl;
	}

	private static long getPageCount(long totalCount, long pageSize) {
		return (totalCount + pageSize - 1) / pageSize;
	}

	private static Date addMonths(Date date, int amount) {
		Calendar c = Calendar.getInstance();
		c.setTime(date);
		c.add(Calendar.MONTH, amount);
		return c.getTime();
	}

	private static Date addHours(Date date, int amount) {
		Calendar c = Calendar.getInstance();
		c.setTime(date);
		c.add(Calendar.HOUR_OF_DAY, amount);
		return c.getTime();
	}
}

class UserInfo {
	private Long userId; // 用户ID
	private String sessionKey; // 访问授权码
	private Date lastSyncTime; // 上次增量订单同步时间

	public Long getUserId() {
		return this.userId;
	}

	public void setUserId(Long userId) {
		this.userId = userId;
	}

	public String getSessionKey() {
		return this.sessionKey;
	}

	public void setSessionKey(String sessionKey) {
		this.sessionKey = sessionKey;
	}

	public Date getLastSyncTime() {
		return this.lastSyncTime;
	}

	public void setLastSyncTime(Date lastSyncTime) {
		this.lastSyncTime = lastSyncTime;
	}
}
 
  • 大小: 33.1 KB
分享到:
评论
2 楼 jaychang 2017-10-06  
dujianqiao 写道
HI ,能否给一个完整的demo 啊 ?

这个已经过时了哈
1 楼 dujianqiao 2017-08-31  
HI ,能否给一个完整的demo 啊 ?

相关推荐

    终结者2.2本地生成版+终结者完整金牌验证-源码

    【终结者2.2本地生成版】是一款针对软件授权验证技术的工具,它允许开发者在本地环境下生成软件的授权信息,以实现对软件使用的有效控制。这个版本的终结者主要聚焦于提供一种安全且便捷的方式来验证软件的合法性,...

    5破解access任意版本密码:旭氏-access密码终结者.rar

    5破解access任意版本密码:旭氏-access密码终结者.rar

    驱动程序-其它驱动-达尔优凌豹终结者键盘驱动程序.zip

    达尔优凌豹终结者键盘驱动程序是为达尔优品牌下的凌豹终结者系列键盘设计的专业驱动软件。这个驱动程序主要用于优化键盘的性能,提供额外的功能支持,并解决在使用过程中可能出现的兼容性问题。驱动程序在计算机硬件...

    源代码-web木马终结者 v1.0.zip

    源代码-web木马终结者 v1.0.zip

    p局域网管理软件--p2p终结者

    P2P终结者目前可以控制绝大部分流行的P2P软件下载,而且P2P终结者开发人员将持续跟踪最新的P2P下载技术的发展,在发现会对网络正常应用造成较大影响的网络下载技术时,会及时进行软件升级更新。 P2P终结者具有以下...

    银行承兑汇票挂失------终结版集合版.doc

    银行承兑汇票挂失------终结版集合版.doc

    旭氏-Access密码终结者

    Access密码破解,完全免费,速度超快!!

    免费无水印高清录屏软件--(录屏终结者)

    《录屏终结者》作为一款备受赞誉的软件,以其免费、无水印和高清的特点,成为了众多用户的选择。本文将深入探讨这款软件的功能、优势以及如何使用它来实现高效录屏。 首先,让我们来看看《录屏终结者》的核心特点。...

    p2p 终结者 网管

    p2p 终结者 p2p 终结者p2p 终结者 p2p 终结者 p2p 终结者 p2p 终结者p2p 终结者 p2p 终结者 p2p 终结者p2p 终结者 p2p 终结者 p2p 终结者 p2p 终结者p2p 终结者p2p 终结者 p2p 终结者p2p 终结者 p2p 终结者 p2p 终结...

    P2P终结者+反P2P终结者

    标题中的“P2P终结者+反P2P终结者”指的是两个与网络管理相关的软件工具,一个是"P2P终结者",另一个是"反P2P终结者"。这两个工具在IT领域中有着特定的应用场景和功能。 P2P(Peer-to-Peer)终结者是一种网络流量...

    终结者远控免费版

    总的来说,【终结者远控】提供了一套完整的远程管理解决方案,适用于个人和企业用户,尤其适合需要远程协助、系统维护或远程办公的场景。使用时,用户应确保遵循合法和道德的使用原则,尊重他人的隐私权。同时,为了...

    最好用的网管软件--网络终结者

    流量控制!能控制别人的流量!能流量控制流量控制!能控制别人的流量!能流量控制

    RF-GC-ZY-10-F07 保修终结申请.zip

    标题中的“RF-GC-ZY-10-F07 保修终结申请”可能是指一个特定的产品或设备的型号,其中“RF”可能是产品类别或公司的缩写,“GC”可能代表某个系列或功能,“ZY”可能指代“装置”或“应用”,“10”可能是型号的...

    ASP实例开发源码-web木马终结者asp版 v1.0.zip

    ASP实例开发源码—web木马终结者asp版 v1.0.zip ASP实例开发源码—web木马终结者asp版 v1.0.zip ASP实例开发源码—web木马终结者asp版 v1.0.zip

    一种可有效破解ADSL密码的工具(附带注册机)--------ADSL密码终结者

    一款破解ADSL密码的工具 内附注册机。

    反p2p终结者杀死局域网中的p2p终结者

    标题“反p2p终结者杀死局域网中的p2p终结者”指的是一个特定的软件或技术解决方案,用于在局域网环境中对抗P2P(peer-to-peer)终结者软件。P2P终结者是一种网络管理工具,它通常被用来限制或阻止局域网内的P2P文件...

    局域网终结者--v1.0版

    【局域网终结者--v1.0版】是一款针对局域网管理的软件,它在特定场景下可能被用于网络监控或者网络安全测试。这款工具的主要功能是揭示局域网内的设备连接情况,帮助用户了解网络流量的流向,以及可能存在的网络滥用...

    p2p终结者2.07

    所有P2P终结者的版权专属于作者 - Net.Soft Studio。 2. 任何人都可以在30天的评估期内使用这个软件。在30天的评估期限后,如 果你还想继续使用,您必须向软件开发商或供应商进行购买。 3. 对于使用P2P终结者...

    3dmax插件神器-011-重面终结.mse

    3dmax插件神器-011-重面终结.mse

    局域网管理-P2P终结者3.5

    P2P终结者是一款优秀的网络管理软件,它可以让您轻松地、傻瓜化地管理局域网中BT、电驴等大量占用带宽的网络引用,为家庭、企业节省宝贵的有限带宽,从而保障网页浏览,邮件,企业ERP等关键应用。 功能特性 ...

Global site tag (gtag.js) - Google Analytics