目录
一、系统架构概览
1.1 微服务模块划分
1.2 数据流图
1.3 核心业务流程
二、技术选型与版本
三、项目搭建与关键代码
3.1 父工程依赖管理
3.2 商品服务 (product-service)
3.2.1 依赖
3.2.2 实体与 Repository
3.2.3 发送商品变更事件
3.3 搜索服务 (search-service)
3.3.1 依赖
3.3.2 Elasticsearch 配置
3.3.3 商品索引映射实体
3.3.4 监听 Kafka 消息,同步索引
3.3.5 搜索 API
3.4 用户行为服务 (user-action-service)
3.4.1 依赖
3.4.2 接收行为并发送到 Kafka
3.5 推荐服务 (recommend-service)
3.5.1 依赖
3.5.2 Redis 配置
3.5.3 消费行为消息,更新用户偏好到 Redis
3.5.4 推荐 API(基于用户偏好的商品召回)
3.6 API 网关 (gateway-service)
3.6.1 依赖
3.6.2 路由配置
3.7 服务注册中心 (eureka-server)
3.7.1 依赖
3.7.2 启动类与配置
四、配置与启动
4.1 基础设施准备
4.2 各服务配置文件
4.3 启动顺序
五、测试与验证
5.1 商品创建与索引同步
5.2 搜索测试
5.3 用户行为上报
5.4 推荐获取
六、扩展思考
七、总结
本文将带你搭建一个完整的电商微服务 DEMO,实现 实时商品搜索 和 基于用户行为的个性化推荐。核心技术栈:Spring Cloud (微服务治理) + Elasticsearch (搜索引擎) + Redis (实时缓存) + Kafka (消息队列)。
一、系统架构概览
1.1 微服务模块划分
| Eureka Server | 服务注册与发现 | Spring Cloud Netflix Eureka |
| Spring Cloud Gateway | API 网关,统一入口,路由转发 | Spring Cloud Gateway |
| Product Service | 商品核心服务(CRUD),商品变更时发送消息 | MySQL, Spring Data JPA, Kafka |
| Search Service | 监听商品变更消息,维护 ES 索引;提供商品搜索 API | Elasticsearch, Kafka, RestHighLevelClient |
| User Action Service | 接收用户前端行为(点击、购买等),发送到 Kafka | Kafka, Spring Web |
| Recommend Service | 消费用户行为消息,更新 Redis 中的用户偏好;提供个性化推荐 API | Redis, Kafka, 简单推荐逻辑 |
| Config Server | 集中配置管理(可选) | Spring Cloud Config |
1.2 数据流图
用户前端
│
├─► 搜索请求 ── Gateway ──► Search Service ──► Elasticsearch
│
├─► 行为上报 ── Gateway ──► User Action Service ──► Kafka ──► Recommend Service ──► Redis
│
└─► 推荐请求 ── Gateway ──► Recommend Service ──► (Redis偏好 + ES查询)
1.3 核心业务流程
商品数据同步:Product Service 对商品进行增删改后,发送 ProductChangeEvent 到 Kafka。Search Service 消费该事件,同步更新 Elasticsearch 中的商品索引。
用户行为收集:用户在前端点击商品、购买等行为,通过 API 上报给 User Action Service,后者将行为消息发送到 Kafka(topic: user-actions)。
用户偏好实时计算:Recommend Service 消费 user-actions,更新 Redis 中的用户实时偏好(例如最近浏览的商品分类、品牌等)。
个性化推荐:当用户请求推荐时,Recommend Service 从 Redis 读取该用户的偏好(如分类 ID),然后去 Elasticsearch 查询同分类下的热门商品,返回给用户。
实时搜索:用户输入关键词,Search Service 直接查询 Elasticsearch,返回分页商品列表。
二、技术选型与版本
-
Spring Boot 3.1.5
-
Spring Cloud 2022.0.4
-
Elasticsearch 8.11.0 (使用官方 Java API Client)
-
Redis 7.x (Spring Data Redis)
-
Kafka 3.5.x (Spring Kafka / Spring Cloud Stream)
-
MySQL 8.0 (仅 Product Service 使用)
-
Maven 3.8+
注:Spring Boot 3.x 已支持 Jakarta EE 9+,使用 jakarta.* 包。
三、项目搭建与关键代码
3.1 父工程依赖管理
<!– pom.xml (parent) –>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
<relativePath/>
</parent>
<properties>
<spring-cloud.version>2022.0.4</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
3.2 商品服务 (product-service)
3.2.1 依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
3.2.2 实体与 Repository
@Entity
@Table(name = "product")
public class Product {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
private BigDecimal price;
private Long categoryId;
private String brand;
// getters/setters
}
public interface ProductRepository extends JpaRepository<Product, Long> {}
3.2.3 发送商品变更事件
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Autowired
private KafkaTemplate<String, Object> kafkaTemplate;
public Product save(Product product) {
Product saved = productRepository.save(product);
// 发送事件到 Kafka
ProductChangeEvent event = new ProductChangeEvent(saved.getId(), "CREATE");
kafkaTemplate.send("product-change", event);
return saved;
}
// 更新、删除同理
}
// 简单的 POJO 事件
public record ProductChangeEvent(Long productId, String type) {}
3.3 搜索服务 (search-service)
3.3.1 依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>8.11.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
3.3.2 Elasticsearch 配置
@Configuration
public class ElasticsearchConfig {
@Bean
public RestClient restClient() {
return RestClient.builder(HttpHost.create("http://localhost:9200")).build();
}
@Bean
public ElasticsearchTransport elasticsearchTransport(RestClient restClient) {
return new RestClientTransport(restClient, new JacksonJsonpMapper());
}
@Bean
public ElasticsearchClient elasticsearchClient(ElasticsearchTransport transport) {
return new ElasticsearchClient(transport);
}
}
3.3.3 商品索引映射实体
public class EsProduct {
private Long id;
private String name;
private String description;
private BigDecimal price;
private Long categoryId;
private String brand;
// getters/setters
}
3.3.4 监听 Kafka 消息,同步索引
@Component
public class ProductChangeConsumer {
@Autowired
private ElasticsearchClient esClient;
@KafkaListener(topics = "product-change", groupId = "search-service")
public void handleProductChange(ProductChangeEvent event) {
if ("CREATE".equals(event.type()) || "UPDATE".equals(event.type())) {
// 从数据库或直接接收完整商品信息(此处简化,假设消息包含商品完整信息)
// 实际中可能需要调用 product-service API 获取最新数据
EsProduct esProduct = fetchProductFromDb(event.productId());
indexProduct(esProduct);
} else if ("DELETE".equals(event.type())) {
deleteProductIndex(event.productId());
}
}
private void indexProduct(EsProduct product) {
IndexRequest<EsProduct> request = IndexRequest.of(i -> i
.index("products")
.id(product.getId().toString())
.document(product)
);
esClient.index(request);
}
// 其他方法…
}
3.3.5 搜索 API
@RestController
@RequestMapping("/search")
public class SearchController {
@Autowired
private ElasticsearchClient esClient;
@GetMapping("/products")
public List<EsProduct> search(@RequestParam String keyword,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) throws IOException {
SearchResponse<EsProduct> response = esClient.search(s -> s
.index("products")
.query(q -> q
.bool(b -> b
.should(should -> should.match(m -> m.field("name").query(keyword)))
.should(should -> should.match(m -> m.field("description").query(keyword)))
)
)
.from(page)
.size(size),
EsProduct.class
);
return response.hits().hits().stream().map(Hit::source).collect(Collectors.toList());
}
}
3.4 用户行为服务 (user-action-service)
3.4.1 依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
3.4.2 接收行为并发送到 Kafka
@RestController
@RequestMapping("/actions")
public class UserActionController {
@Autowired
private KafkaTemplate<String, Object> kafkaTemplate;
@PostMapping
public void recordAction(@RequestBody UserAction action) {
kafkaTemplate.send("user-actions", action);
}
}
public record UserAction(Long userId, String actionType, Long productId, Long categoryId, Long timestamp) {}
3.5 推荐服务 (recommend-service)
3.5.1 依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>8.11.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
3.5.2 Redis 配置
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
return template;
}
}
3.5.3 消费行为消息,更新用户偏好到 Redis
@Component
public class UserActionConsumer {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@KafkaListener(topics = "user-actions", groupId = "recommend-service")
public void consume(UserAction action) {
String key = "user:pref:" + action.userId();
// 简单偏好:最近浏览的分类ID(使用 Set 存储,并记录时间戳,此处简化)
redisTemplate.opsForSet().add(key, action.categoryId());
// 设置过期时间(例如7天)
redisTemplate.expire(key, 7, TimeUnit.DAYS);
// 也可以使用 ZSet 存储带时间的浏览历史,这里不再展开
}
}
3.5.4 推荐 API(基于用户偏好的商品召回)
@RestController
@RequestMapping("/recommend")
public class RecommendController {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ElasticsearchClient esClient;
@GetMapping("/{userId}")
public List<EsProduct> recommend(@PathVariable Long userId,
@RequestParam(defaultValue = "10") int size) throws IOException {
// 1. 从 Redis 获取用户最近浏览的分类ID(这里取一个,实际可多个)
Set<Object> categoryIds = redisTemplate.opsForSet().members("user:pref:" + userId);
if (categoryIds == null || categoryIds.isEmpty()) {
return Collections.emptyList();
}
Long categoryId = (Long) categoryIds.iterator().next(); // 简化,取第一个
// 2. 从 ES 查询该分类下的商品,按某种策略排序(如随机或热度)
SearchResponse<EsProduct> response = esClient.search(s -> s
.index("products")
.query(q -> q.term(t -> t.field("categoryId").value(categoryId)))
.size(size),
EsProduct.class
);
return response.hits().hits().stream().map(Hit::source).collect(Collectors.toList());
}
}
3.6 API 网关 (gateway-service)
3.6.1 依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
3.6.2 路由配置
spring:
cloud:
gateway:
routes:
– id: product-service
uri: lb://product-service
predicates:
– Path=/product/**
– id: search-service
uri: lb://search-service
predicates:
– Path=/search/**
– id: user-action-service
uri: lb://user-action-service
predicates:
– Path=/actions/**
– id: recommend-service
uri: lb://recommend-service
predicates:
– Path=/recommend/**
3.7 服务注册中心 (eureka-server)
3.7.1 依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
3.7.2 启动类与配置
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
# application.yml
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
四、配置与启动
4.1 基础设施准备
-
启动 MySQL,创建数据库 ecommerce,并执行建表语句。
-
启动 Elasticsearch(8.x 需要配置安全,可暂时禁用)。
-
启动 Redis。
-
启动 Kafka(需要 Zookeeper)。
4.2 各服务配置文件
每个服务的 application.yml 需要包含:
-
服务名
-
Eureka 客户端配置
-
Kafka 配置(bootstrap-servers)
-
各自的数据源/ES/Redis 配置
以 search-service 为例:
spring:
application:
name: search-service
kafka:
bootstrap-servers: localhost:9092
consumer:
group-id: search-service
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
properties:
spring.json.trusted.packages: '*'
elasticsearch:
uris: http://localhost:9200
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
4.3 启动顺序
Eureka Server
其余所有微服务(product, search, user-action, recommend, gateway)
五、测试与验证
5.1 商品创建与索引同步
curl http://localhost:9200/products/_search?pretty
# 创建商品
curl -X POST http://localhost:8080/product -H "Content-Type: application/json" -d '{
"name": "华为Mate 60 Pro",
"description": "鸿蒙系统,卫星通信",
"price": 6999.00,
"categoryId": 1,
"brand": "华为"
}'
等待片刻,查看 Elasticsearch 索引:
curl http://localhost:9200/products/_search?pretty
5.2 搜索测试
curl "http://localhost:8080/search/products?keyword=华为&page=0&size=5"
应返回刚创建的商品。
5.3 用户行为上报
curl -X POST http://localhost:8080/actions -H "Content-Type: application/json" -d '{
"userId": 1001,
"actionType": "VIEW",
"productId": 1,
"categoryId": 1,
"timestamp": 1700000000000
}'
5.4 推荐获取
curl "http://localhost:8080/recommend/1001?size=5"
应返回 categoryId=1 的商品。
六、扩展思考
-
推荐算法升级:可使用 Spark MLlib 或 TensorFlow 构建离线模型,通过 Kafka 推送实时特征。
-
数据一致性:可使用 Kafka 事务和幂等生产者保证 Exactly-Once。
-
性能优化:ES 查询可使用缓存,Redis 可存储更复杂的用户画像(如偏好向量)。
-
监控:集成 Spring Boot Admin 或 Prometheus + Grafana。
七、总结
本文搭建了一个完整的电商微服务 DEMO,涵盖了 实时搜索 和 个性化推荐 的核心流程。通过 Spring Cloud 实现了服务治理,利用 Elasticsearch 提供高性能搜索,Redis 缓存用户实时偏好,Kafka 解耦异步数据流。该架构具有良好的扩展性,可作为真实电商系统的技术原型。

