欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-03-02 08:32:11
...

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

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

订单同步接口
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的一部分订单,特别是店铺参加聚划算活动时更容易出现。
五、按状态同步订单,这种方式的问题在于订单状态过多,有可能会出现状态遗漏,而且性能低效。

推荐同步方案
同步流程图

淘宝订单同步方案 - 丢单终结者
            
    
    博客分类: TaobaoOpenPlatform  
 

流程图解释
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;
	}
}
 
  • 淘宝订单同步方案 - 丢单终结者
            
    
    博客分类: TaobaoOpenPlatform  
  • 大小: 33.1 KB