应对高并发:淘宝商品评论实时数据 API 高效接入开发实践

7 阅读7分钟

在电商平台的运营中,商品评论数据是用户决策、商家优化及平台运营的重要依据。淘宝作为国内领先的电商平台,其商品评论数据具有实时性强、数据量大、并发访问频繁等特点。本文将围绕淘宝商品评论实时数据 API 的高效接入展开,探讨在高并发场景下的开发实践,包括技术选型、架构设计、代码实现及性能优化等方面。

一、场景分析与技术挑战

1. 场景特点

  • 高并发访问:热门商品的评论数据往往会吸引大量用户同时查看,API 接口需要承受瞬间的高请求量。
  • 实时性要求高:用户发布评论后,希望能尽快在页面上展示,这就要求 API 能实时获取并返回最新的评论数据。
  • 数据量大且结构复杂:淘宝商品的评论包含文本、图片、评分、追评等多种信息,数据结构复杂,且随着商品销量的增加,评论数据量会急剧增长。

2. 技术挑战

  • 接口性能瓶颈:在高并发情况下,API 接口的响应速度可能会变慢,甚至出现超时、崩溃等问题。
  • 数据一致性:如何保证获取到的评论数据与淘宝平台上的实际数据一致,是需要解决的关键问题。
  • 限流与防爬虫:淘宝 API 通常会有限流策略,同时为了防止恶意爬虫,还会有一些安全验证机制,如何在遵守规则的前提下高效获取数据是一大挑战。

二、技术选型

针对上述场景和挑战,我们进行如下技术选型:

  • 开发语言:选用 Java,其具有良好的性能、丰富的生态系统和成熟的并发处理机制,适合开发高并发的后端服务。
  • HTTP 客户端:使用 OkHttp,它是一个高效的 HTTP 客户端,支持连接池、异步请求等功能,能提高 API 调用的效率。
  • 缓存框架:采用 Redis 作为缓存,用于存储热门商品的评论数据,减少对 API 接口的直接调用,提高响应速度。
  • 消息队列:引入 RabbitMQ,当并发请求量超过 API 接口的处理能力时,将请求放入消息队列,进行异步处理,避免接口被压垮。
  • 服务注册与发现:使用 Spring Cloud Eureka,实现服务的注册与发现,便于服务的扩展和负载均衡。

三、架构设计

整体架构采用分层设计,分为接入层、业务层、数据层和缓存层,具体如下:

  • 接入层:负责接收客户端的请求,进行参数校验、限流控制和安全验证,然后将请求转发给业务层。
  • 业务层:实现核心的业务逻辑,包括调用淘宝 API 获取评论数据、对数据进行处理和转换、与缓存层和数据层进行交互等。
  • 数据层:用于持久化存储评论数据,可选用 MySQL 等关系型数据库。
  • 缓存层:使用 Redis 存储热门商品的评论数据,提高数据的访问速度。

同时,为了应对高并发,还采用了以下策略:

  • 负载均衡:通过 Eureka 实现服务的多实例部署,结合 Ribbon 实现负载均衡,将请求均匀地分发到不同的服务实例上。
  • 熔断降级:使用 Spring Cloud Hystrix,当淘宝 API 出现故障或响应超时等情况时,进行熔断处理,返回降级数据,保证服务的可用性。
  • 异步处理:对于非实时性要求特别高的请求,通过 RabbitMQ 进行异步处理,提高系统的吞吐量。

四、代码实现

1. 引入依赖

pom.xml文件中引入相关依赖:

<dependencies>
    <!-- Spring Boot 核心依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- OkHttp -->
    <dependency>
        <groupId>com.squareup.okhttp3</groupId>
        <artifactId>okhttp</artifactId>
        <version>4.9.3</version>
    </dependency>
    
    <!-- Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <!-- RabbitMQ -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    
    <!-- Spring Cloud Eureka Client -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    
    <!-- Spring Cloud Hystrix -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
</dependencies>

 

2. 配置文件

application.yml配置文件如下:

spring:
  application:
    name: taobao-comment-service
  redis:
    host: localhost
    port: 6379
    password: 123456
    timeout: 5000
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true

taobao:
  api:
    url: https://api.taobao.com/rest/api3.do
    appKey: your_app_key
    appSecret: your_app_secret
    timeout: 3000
    maxConnections: 100
    maxRequestsPerHost: 50

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 5000

redis:
  key:
    comment: "taobao:comment:{productId}"
    commentExpire: 3600

 

3. 工具类

OkHttp 工具类

import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Configuration
public class OkHttpConfig {

    @Value("${taobao.api.timeout}")
    private int timeout;

    @Value("${taobao.api.maxConnections}")
    private int maxConnections;

    @Value("${taobao.api.maxRequestsPerHost}")
    private int maxRequestsPerHost;

    @Bean
    public OkHttpClient okHttpClient() {
        return new OkHttpClient.Builder()
                .connectTimeout(timeout, TimeUnit.MILLISECONDS)
                .readTimeout(timeout, TimeUnit.MILLISECONDS)
                .writeTimeout(timeout, TimeUnit.MILLISECONDS)
                .connectionPool(new ConnectionPool(maxConnections, 5, TimeUnit.MINUTES))
                .build();
    }
}

 

签名工具类

淘宝 API 调用需要进行签名,以下是签名工具类:

import org.apache.commons.codec.digest.DigestUtils;
import java.util.Map;
import java.util.TreeMap;

public class SignUtils {

    public static String generateSign(Map<String, String> params, String appSecret) {
        // 将参数按字典序排序
        TreeMap<String, String> sortedParams = new TreeMap<>(params);
        // 拼接参数
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            if (key != null && value != null && !key.isEmpty() && !value.isEmpty()) {
                sb.append(key).append(value);
            }
        }
        // 拼接appSecret
        sb.append(appSecret);
        // 计算MD5签名
        return DigestUtils.md5Hex(sb.toString()).toUpperCase();
    }
}

 

4. 服务接口与实现

评论服务接口

import com.taobao.comment.dto.CommentDTO;
import java.util.List;

public interface CommentService {

    /**
     * 获取商品评论列表
     * @param productId 商品ID
     * @param page 页码
     * @param pageSize 每页条数
     * @return 评论列表
     */
    List<CommentDTO> getCommentList(Long productId, Integer page, Integer pageSize);
}

 评论服务实现

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.taobao.comment.dto.CommentDTO;
import com.taobao.comment.service.CommentService;
import com.taobao.comment.utils.SignUtils;
import okhttp3.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;

@Service
public class CommentServiceImpl implements CommentService {

    @Autowired
    private OkHttpClient okHttpClient;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Value("${taobao.api.url}")
    private String apiUrl;

    @Value("${taobao.api.appKey}")
    private String appKey;

    @Value("${taobao.api.appSecret}")
    private String appSecret;

    @Value("${redis.key.comment}")
    private String commentKey;

    @Value("${redis.key.commentExpire}")
    private int commentExpire;

    @Override
    public List<CommentDTO> getCommentList(Long productId, Integer page, Integer pageSize) {
        // 先从缓存中获取
        String redisKey = commentKey.replace("{productId}", productId.toString()) + ":" + page + ":" + pageSize;
        List<CommentDTO> commentList = (List<CommentDTO>) redisTemplate.opsForValue().get(redisKey);
        if (!CollectionUtils.isEmpty(commentList)) {
            return commentList;
        }

        // 缓存中没有,调用淘宝API获取
        commentList = callTaobaoApi(productId, page, pageSize);

        // 将结果存入缓存
        if (!CollectionUtils.isEmpty(commentList)) {
            redisTemplate.opsForValue().set(redisKey, commentList, commentExpire, TimeUnit.SECONDS);
        }

        return commentList;
    }

    /**
     * 调用淘宝API获取评论数据
     * @param productId 商品ID
     * @param page 页码
     * @param pageSize 每页条数
     * @return 评论列表
     */
    private List<CommentDTO> callTaobaoApi(Long productId, Integer page, Integer pageSize) {
        // 构建请求参数
        Map<String, String> params = new HashMap<>();
        params.put("app_key", appKey);
        params.put("method", "taobao.item.review.get");
        params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
        params.put("format", "json");
        params.put("v", "2.0");
        params.put("item_id", productId.toString());
        params.put("page_no", page.toString());
        params.put("page_size", pageSize.toString());

        // 生成签名
        String sign = SignUtils.generateSign(params, appSecret);
        params.put("sign", sign);

        // 构建请求URL
        HttpUrl.Builder urlBuilder = HttpUrl.parse(apiUrl).newBuilder();
        for (Map.Entry<String, String> entry : params.entrySet()) {
            urlBuilder.addQueryParameter(entry.getKey(), entry.getValue());
        }
        String url = urlBuilder.build().toString();

        // 发送请求
        Request request = new Request.Builder()
                .url(url)
                .get()
                .build();

        try (Response response = okHttpClient.newCall(request).execute()) {
            if (response.isSuccessful() && response.body() != null) {
                String responseBody = response.body().string();
                JSONObject jsonObject = JSON.parseObject(responseBody);
                if (jsonObject.containsKey("tbk_item_review_get_response")) {
                    JSONObject resultObject = jsonObject.getJSONObject("tbk_item_review_get_response").getJSONObject("results");
                    if (resultObject.containsKey("review")) {
                        return JSON.parseArray(resultObject.getString("review"), CommentDTO.class);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return Collections.emptyList();
    }
}

 5. 控制器

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.taobao.comment.dto.CommentDTO;
import com.taobao.comment.service.CommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collections;
import java.util.List;

@RestController
public class CommentController {

    @Autowired
    private CommentService commentService;

    @GetMapping("/comment/list")
    @HystrixCommand(fallbackMethod = "getCommentListFallback")
    public List<CommentDTO> getCommentList(@RequestParam Long productId,
                                           @RequestParam(defaultValue = "1") Integer page,
                                           @RequestParam(defaultValue = "20") Integer pageSize) {
        return commentService.getCommentList(productId, page, pageSize);
    }

    /**
     * 降级方法
     */
    public List<CommentDTO> getCommentListFallback(Long productId, Integer page, Integer pageSize) {
        // 返回空列表或默认提示信息
        return Collections.emptyList();
    }
}

 

6. 熔断与降级配置

通过@HystrixCommand注解实现熔断与降级,当getCommentList方法执行超时或抛出异常时,会自动调用getCommentListFallback方法返回降级数据。

五、性能优化

1. 缓存优化

  • 合理设置缓存过期时间:根据商品评论的更新频率,设置合适的缓存过期时间,既保证数据的新鲜度,又能充分利用缓存的优势。
  • 缓存预热:对于热门商品,可以在系统启动时或流量低谷期,提前将其评论数据加载到缓存中,避免在高并发时缓存失效导致的请求压力集中到 API 接口。
  • 缓存穿透防护:对于不存在的商品 ID 请求,在缓存中设置一个空值,并设置较短的过期时间,避免恶意请求对 API 接口造成冲击。

2. 并发控制

  • 连接池优化:合理配置 OkHttp 的连接池参数,如最大连接数、每个主机的最大请求数等,提高连接的复用率,减少连接建立和关闭的开销。
  • 异步请求:对于一些非核心的评论数据获取操作,可以使用 OkHttp 的异步请求方式,避免阻塞主线程。
  • 限流措施:在接入层实现限流控制,根据 API 接口的承载能力,限制单位时间内的请求数量,防止接口被压垮。

3. 数据处理优化

  • 数据压缩:在传输评论数据时,对数据进行压缩,减少网络传输量,提高传输效率。
  • 按需获取数据:根据前端的需求,只获取必要的评论字段,减少数据的处理和传输成本。

六、总结

本文围绕淘宝商品评论实时数据 API 的高效接入,从场景分析、技术选型、架构设计、代码实现到性能优化等方面进行了详细的阐述。通过采用 Java、OkHttp、Redis、RabbitMQ 等技术,结合缓存、异步处理、熔断降级等策略,有效应对了高并发场景下的各种挑战,提高了系统的性能和稳定性。

在实际开发中,还需要根据具体的业务需求和流量情况,不断调整和优化系统架构及参数配置,以确保系统能够稳定、高效地运行。同时,要密切关注淘宝 API 的更新和变化,及时调整接入方式,保证数据获取的合法性和稳定性。