在电商平台的数字化运营中,数据采集是核心环节之一。淘宝作为国内头部电商平台,其搜索数据包含了商品价格、销量、评价等关键信息,是电商分析、竞品监控、智能推荐的重要数据源。在微服务架构体系下,将淘宝搜索 API 封装为独立的数据服务,不仅能实现数据采集能力的解耦复用,还能提升系统的可扩展性、容错性和维护性。本文将详细讲解如何基于微服务思想设计并实现这一数据服务,并提供完整的代码示例。
一、微服务设计思路
1. 核心需求分析
封装淘宝搜索 API 的独立数据服务需满足以下核心需求:
提供统一的接口供上层服务调用,屏蔽淘宝 API 的底层细节;
支持参数灵活配置(如搜索关键词、页码、排序方式等);
实现请求限流、异常重试、数据缓存,保障服务稳定性;
输出标准化的数据格式,便于后续处理和分析;
具备独立部署、水平扩展的能力。
2. 技术选型
开发框架:Spring Boot + Spring Cloud(微服务核心,快速构建独立服务);
HTTP 客户端:OkHttp(高效处理 HTTP 请求,适配淘宝 API 的 HTTPS 通信);
数据缓存:Redis(缓存高频搜索关键词的结果,降低 API 调用频率);
序列化:FastJSON2(处理淘宝 API 返回的 JSON 数据,转换为标准化 DTO);
服务治理:Sentinel(实现接口限流、熔断,防止服务雪崩);
构建工具:Maven(依赖管理与打包部署)。
3. 服务架构设计
该数据服务作为微服务集群中的 “数据采集层”,核心模块划分如下:
接口层:对外暴露 RESTful API,接收上层服务的搜索请求;
适配层:封装淘宝 API 的请求参数、签名规则、响应解析逻辑;
缓存层:基于 Redis 实现结果缓存,设置合理的过期时间;
容错层:实现请求重试、限流、熔断,保障服务稳定性;
数据转换层:将淘宝 API 的原始响应转换为标准化的 DTO 对象。
二、淘宝 API 接入准备
1. 开发者资质与 API 申请
首先需注册淘宝开发者账号并获取 ApiKey、ApiSecret 等关键参数。
2. API 调用规则说明
淘宝搜索 API 的核心调用规则:
请求方式:HTTP GET/POST,需按规则生成签名;
核心参数:q(搜索关键词)、page_no(页码)、page_size(每页条数)、sort(排序方式)等;
响应格式:JSON,包含商品 ID、标题、价格、销量、佣金等信息;
调用限制:单应用日调用量、QPS 均有上限,需合理控制请求频率。
三、代码实现
1. 项目依赖配置(pom.xml)
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.0</version> <relativePath/> </parent> <groupId>com.ecommerce</groupId> <artifactId>taobao-search-service</artifactId> <version>1.0.0</version> <name>taobao-search-service</name> <dependencies> <!-- Spring Boot核心依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!-- HTTP客户端 --> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.12.0</version> </dependency> <!-- JSON序列化 --> <dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2</artifactId> <version>2.0.45</version> </dependency> <!-- 服务治理:限流熔断 --> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-core</artifactId> <version>1.8.7</version> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-spring-webmvc-adapter</artifactId> <version>1.8.7</version> </dependency> <!-- 工具类 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.22</version> </dependency> <!-- 测试依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
2. 配置文件(application.yml)
server: port: 8081 servlet: context-path: /taobao-search spring: # Redis配置 redis: host: localhost port: 6379 password: database: 0 timeout: 2000ms # 日志配置 logging: level: com.ecommerce.taobao: INFO org.springframework.web: WARN # 淘宝API配置 taobao: api: url: https://eco.taobao.com/router/rest app-key: 你的AppKey app-secret: 你的AppSecret format: json v: 2.0 sign-method: md5 # 缓存过期时间(秒) cache-expire: 300 # 最大重试次数 max-retry: 3 # 重试间隔(毫秒) retry-interval: 1000 # Sentinel限流配置 sentinel: rules: flow: # 限流阈值:QPS qps-threshold: 10
3. 核心代码实现
(1)DTO 对象:标准化响应格式
package com.ecommerce.taobao.dto;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
* 淘宝搜索结果标准化DTO
*/
@Data
public class TaobaoSearchResponseDTO implements Serializable {
/** 响应状态:success/fail */
private String status;
/** 错误信息 */
private String message;
/** 搜索关键词 */
private String keyword;
/** 页码 */
private Integer pageNo;
/** 每页条数 */
private Integer pageSize;
/** 总条数 */
private Long totalCount;
/** 商品列表 */
private List<ItemDTO> items;
/**
* 商品DTO
*/
@Data
public static class ItemDTO implements Serializable {
/** 商品ID */
private String itemId;
/** 商品标题 */
private String title;
/** 商品价格 */
private BigDecimal price;
/** 商品销量 */
private Long salesCount;
/** 商品图片URL */
private String picUrl;
/** 商品链接 */
private String itemUrl;
/** 店铺名称 */
private String shopName;
}
}(2)配置类:API 参数与 Redis 配置
package com.ecommerce.taobao.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "taobao.api")
public class TaobaoApiConfig {
private String url;
private String appKey;
private String appSecret;
private String format;
private String v;
private String signMethod;
private Integer cacheExpire;
private Integer maxRetry;
private Integer retryInterval;
}(3)核心服务类:API 调用与数据处理
package com.ecommerce.taobao.service;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ecommerce.taobao.config.TaobaoApiConfig;
import com.ecommerce.taobao.dto.TaobaoSearchResponseDTO;
import com.ecommerce.taobao.dto.TaobaoSearchResponseDTO.ItemDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@RequiredArgsConstructor
public class TaobaoSearchService {
private final TaobaoApiConfig taobaoApiConfig;
private final StringRedisTemplate redisTemplate;
private final OkHttpClient okHttpClient = new OkHttpClient();
/**
* 淘宝搜索核心方法
* @param keyword 搜索关键词
* @param pageNo 页码
* @param pageSize 每页条数
* @return 标准化搜索结果
*/
public TaobaoSearchResponseDTO search(String keyword, Integer pageNo, Integer pageSize) {
TaobaoSearchResponseDTO response = new TaobaoSearchResponseDTO();
response.setKeyword(keyword);
response.setPageNo(pageNo);
response.setPageSize(pageSize);
// 1. 构建缓存Key
String cacheKey = "taobao:search:" + SecureUtil.md5(keyword + "_" + pageNo + "_" + pageSize);
// 2. 尝试从缓存获取数据
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
if (cacheValue != null) {
log.info("从缓存获取淘宝搜索结果,关键词:{},页码:{}", keyword, pageNo);
return JSON.parseObject(cacheValue, TaobaoSearchResponseDTO.class);
}
// 3. 缓存未命中,调用淘宝API
try {
String apiResponse = callTaobaoApi(keyword, pageNo, pageSize);
// 4. 解析API响应
TaobaoSearchResponseDTO result = parseApiResponse(apiResponse);
result.setKeyword(keyword);
result.setPageNo(pageNo);
result.setPageSize(pageSize);
result.setStatus("success");
// 5. 将结果存入缓存
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(result),
taobaoApiConfig.getCacheExpire(), TimeUnit.SECONDS);
log.info("调用淘宝API并缓存结果,关键词:{},页码:{}", keyword, pageNo);
return result;
} catch (Exception e) {
log.error("淘宝搜索API调用失败,关键词:{},页码:{}", keyword, pageNo, e);
response.setStatus("fail");
response.setMessage("数据采集失败:" + e.getMessage());
return response;
}
}
/**
* 调用淘宝API
*/
private String callTaobaoApi(String keyword, Integer pageNo, Integer pageSize) throws Exception {
// 构建请求参数
Map<String, String> params = new TreeMap<>();
params.put("method", "taobao.tbk.item.search");
params.put("app_key", taobaoApiConfig.getAppKey());
params.put("format", taobaoApiConfig.getFormat());
params.put("v", taobaoApiConfig.getV());
params.put("sign_method", taobaoApiConfig.getSignMethod());
params.put("timestamp", new Date().toString());
params.put("q", keyword);
params.put("page_no", pageNo.toString());
params.put("page_size", pageSize.toString());
// 生成签名
String sign = generateSign(params);
params.put("sign", sign);
// 构建请求URL
String url = taobaoApiConfig.getUrl() + "?" + HttpUtil.toParams(params);
log.info("淘宝API请求URL:{}", url);
// 发送请求(带重试机制)
int retryCount = 0;
while (retryCount < taobaoApiConfig.getMaxRetry()) {
try (Response response = okHttpClient.newCall(new Request.Builder().url(url).get().build()).execute()) {
if (response.isSuccessful() && response.body() != null) {
return response.body().string();
}
} catch (Exception e) {
retryCount++;
log.warn("淘宝API调用失败,重试次数:{},原因:{}", retryCount, e.getMessage());
Thread.sleep(taobaoApiConfig.getRetryInterval());
}
}
throw new RuntimeException("淘宝API调用重试" + taobaoApiConfig.getMaxRetry() + "次后仍失败");
}
/**
* 生成淘宝API签名
*/
private String generateSign(Map<String, String> params) {
StringBuilder sb = new StringBuilder();
sb.append(taobaoApiConfig.getAppSecret());
for (Map.Entry<String, String> entry : params.entrySet()) {
sb.append(entry.getKey()).append(entry.getValue());
}
sb.append(taobaoApiConfig.getAppSecret());
return SecureUtil.md5(sb.toString()).toUpperCase();
}
/**
* 解析淘宝API响应,转换为标准化DTO
*/
private TaobaoSearchResponseDTO parseApiResponse(String apiResponse) {
TaobaoSearchResponseDTO response = new TaobaoSearchResponseDTO();
JSONObject jsonObject = JSON.parseObject(apiResponse);
JSONObject resultObject = jsonObject.getJSONObject("tbk_item_search_response")
.getJSONObject("results")
.getJSONObject("n_tbk_item");
// 解析总条数
Long totalCount = jsonObject.getJSONObject("tbk_item_search_response")
.getJSONObject("results")
.getLong("total_results");
response.setTotalCount(totalCount);
// 解析商品列表
List<ItemDTO> itemList = new ArrayList<>();
if (resultObject != null) {
// 兼容单商品和多商品场景
if (resultObject.containsKey("item_id")) {
ItemDTO item = parseItem(resultObject);
itemList.add(item);
} else {
List<JSONObject> items = resultObject.getList("n_tbk_item", JSONObject.class);
if (!CollectionUtils.isEmpty(items)) {
items.forEach(itemJson -> itemList.add(parseItem(itemJson)));
}
}
}
response.setItems(itemList);
return response;
}
/**
* 解析单个商品数据
*/
private ItemDTO parseItem(JSONObject itemJson) {
ItemDTO item = new ItemDTO();
item.setItemId(itemJson.getString("item_id"));
item.setTitle(itemJson.getString("title"));
item.setPrice(new BigDecimal(itemJson.getString("zk_final_price")));
item.setSalesCount(itemJson.getLong("volume"));
item.setPicUrl(itemJson.getString("pict_url"));
item.setItemUrl(itemJson.getString("item_url"));
item.setShopName(itemJson.getString("shop_title"));
return item;
}
}(4)控制器:对外暴露 RESTful 接口
package com.ecommerce.taobao.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.ecommerce.taobao.dto.TaobaoSearchResponseDTO;
import com.ecommerce.taobao.service.TaobaoSearchService;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@Validated
@RequiredArgsConstructor
public class TaobaoSearchController {
private final TaobaoSearchService taobaoSearchService;
/**
* 淘宝搜索接口
* @param keyword 搜索关键词(必填)
* @param pageNo 页码(默认1)
* @param pageSize 每页条数(默认20)
* @return 标准化搜索结果
*/
@GetMapping("/search")
@SentinelResource(value = "taobaoSearch", blockHandler = "searchBlockHandler")
public ResponseEntity<TaobaoSearchResponseDTO> search(
@RequestParam @NotBlank(message = "搜索关键词不能为空") String keyword,
@RequestParam(defaultValue = "1") @Positive(message = "页码必须为正整数") Integer pageNo,
@RequestParam(defaultValue = "20") @Positive(message = "每页条数必须为正整数") Integer pageSize) {
TaobaoSearchResponseDTO result = taobaoSearchService.search(keyword, pageNo, pageSize);
return new ResponseEntity<>(result, HttpStatus.OK);
}
/**
* Sentinel限流降级处理
*/
public ResponseEntity<TaobaoSearchResponseDTO> searchBlockHandler(
String keyword, Integer pageNo, Integer pageSize, BlockException e) {
log.warn("淘宝搜索接口触发限流,关键词:{},页码:{},原因:{}", keyword, pageNo, e.getRule());
TaobaoSearchResponseDTO response = new TaobaoSearchResponseDTO();
response.setStatus("fail");
response.setMessage("请求过于频繁,请稍后重试");
response.setKeyword(keyword);
response.setPageNo(pageNo);
response.setPageSize(pageSize);
return new ResponseEntity<>(response, HttpStatus.TOO_MANY_REQUESTS);
}
}(5)启动类
package com.ecommerce.taobao;
import com.alibaba.csp.sentinel.init.InitExecutor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TaobaoSearchServiceApplication {
public static void main(String[] args) {
// 初始化Sentinel
InitExecutor.doInit();
SpringApplication.run(TaobaoSearchServiceApplication.class, args);
}
}4. Sentinel 限流规则配置
创建 Sentinel 配置类,实现接口 QPS 限流:
package com.ecommerce.taobao.config;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class SentinelConfig {
@Value("${sentinel.rules.flow.qps-threshold:10}")
private int qpsThreshold;
@PostConstruct
public void initFlowRules() {
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
// 对应控制器中的@SentinelResource值
rule.setResource("taobaoSearch");
// 限流阈值类型:QPS
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
// QPS阈值
rule.setCount(qpsThreshold);
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
}四、服务测试与部署
1. 本地测试
启动 Redis 服务,替换配置文件中的淘宝 ApiKey 和 ApiSecret,运行启动类,通过 Postman 或浏览器访问接口:
GET http://localhost:8081/taobao-search/search?keyword=手机&pageNo=1&pageSize=20
响应示例:
{
"status": "success",
"message": "",
"keyword": "手机",
"pageNo": 1,
"pageSize": 20,
"totalCount": 1000,
"items": [
{
"itemId": "123456789",
"title": "新款智能手机 全网通",
"price": 1999.00,
"salesCount": 10000,
"picUrl": "https://img.alicdn.com/imgextra/i1/xxx.jpg",
"itemUrl": "https://detail.tmall.com/item.htm?id=123456789",
"shopName": "XX官方旗舰店"
}
]
}2. 微服务部署
该服务可通过 Docker 容器化部署,或接入 Spring Cloud 注册中心(如 Nacos)实现服务发现,配合 Gateway 网关实现统一入口,结合 Sentinel Dashboard 实现限流规则的动态配置。
五、扩展与优化
分布式缓存:若服务集群部署,可使用 Redis 集群或 Redisson 实现分布式锁,避免缓存击穿;
异步处理:对于大批量数据采集,可引入消息队列(如 RocketMQ)实现异步请求,提升吞吐量;
数据持久化:将采集的核心数据存入 MySQL/ClickHouse,支持离线分析;
监控告警:接入 Prometheus + Grafana 监控 API 调用成功率、响应时间,配置告警规则;
多平台适配:基于接口抽象,扩展京东、拼多多等平台的 API 封装,实现多平台数据采集统一接口。
六、总结
将淘宝搜索 API 封装为独立的微服务,不仅解决了传统单体架构中数据采集与业务逻辑耦合的问题,还通过缓存、限流、重试等机制保障了服务的高可用。该服务作为电商数据中台的基础组件,可灵活对接上层的数据分析、智能推荐、竞品监控等业务系统,为电商企业的数字化运营提供稳定、高效的数据支撑。在实际应用中,可根据业务需求进一步扩展功能,适配更多电商平台的 API,构建完整的电商数据采集体系。