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

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

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

订单同步接口
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;
	}
}
 

你可能感兴趣的:(同步)