第2章 服务注册与发现
2.1 服务注册与发现概述
什么是服务注册与发现
在微服务架构中,服务注册与发现是一个核心概念。随着服务数量的增加和动态扩缩容的需求,手动管理服务实例的地址变得不现实。服务注册与发现机制解决了以下问题:
- 服务定位:如何找到可用的服务实例
- 负载均衡:如何在多个服务实例间分配请求
- 故障检测:如何检测和剔除不健康的服务实例
- 动态配置:如何处理服务实例的动态变化
服务注册与发现架构
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 服务提供者A │ │ 服务提供者B │ │ 服务提供者C │
│ (User Service) │ │ (Order Service) │ │(Product Service)│
└─────────┬───────┘ └─────────┬───────┘ └─────────┬───────┘
│ 注册 │ 注册 │ 注册
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ 服务注册中心 │
│ (Service Registry) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────────┐ │
│ │ User Service│ │Order Service│ │ Product Service │ │
│ │ 192.168.1.10│ │ 192.168.1.20│ │ 192.168.1.30,192.168.1.31 │ │
│ │ :8081 │ │ :8082 │ │ :8083,:8084 │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────────┘ │
└─────────────────────┬───────────────────────────────────────────┘
│ 发现
▼
┌─────────────────┐
│ 服务消费者 │
│ (API Gateway) │
└─────────────────┘
核心组件
服务注册中心(Service Registry)
- 存储服务实例的元数据信息
- 提供服务注册和注销接口
- 健康检查和故障检测
服务提供者(Service Provider)
- 启动时向注册中心注册自己
- 定期发送心跳保持注册状态
- 关闭时注销服务
服务消费者(Service Consumer)
- 从注册中心获取服务实例列表
- 根据负载均衡策略选择服务实例
- 缓存服务实例信息
2.2 Eureka 服务注册中心
Eureka 架构
Eureka是Netflix开源的服务发现组件,采用AP(可用性和分区容错性)设计,适合云环境下的服务发现。
┌─────────────────────────────────────────────────────────────────┐
│ Eureka 架构 │
├─────────────────────────────────────────────────────────────────┤
│ Eureka Server 集群 │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Eureka Server │◄──►│ Eureka Server │ │
│ │ (Primary) │ │ (Secondary) │ │
│ │ Zone: us-east │ │ Zone: us-west │ │
│ └─────────┬───────┘ └─────────┬───────┘ │
├────────────┼─────────────────────┼────────────────────────────┤
│ │ 注册/续约/获取 │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Eureka Client │ │ Eureka Client │ │
│ │ (Service A) │ │ (Service B) │ │
│ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
搭建 Eureka Server
1. 创建 Eureka Server 项目
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>2.7.14</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>eureka-server</artifactId>
<version>1.0.0</version>
<name>eureka-server</name>
<description>Eureka Server for Service Discovery</description>
<properties>
<java.version>11</java.version>
<spring-cloud.version>2021.0.8</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. 启动类配置
EurekaServerApplication.java
package com.example.eurekaserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
/**
* Eureka服务注册中心启动类
*
* @EnableEurekaServer 启用Eureka服务端功能
*/
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
3. 配置文件
application.yml
server:
port: 8761
spring:
application:
name: eureka-server
security:
user:
name: admin
password: admin123
roles: ADMIN
eureka:
instance:
hostname: localhost
prefer-ip-address: true
instance-id: ${spring.application.name}:${server.port}
client:
# 是否向注册中心注册自己
register-with-eureka: false
# 是否从注册中心获取服务列表
fetch-registry: false
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
server:
# 关闭自我保护模式(开发环境)
enable-self-preservation: false
# 清理间隔(毫秒)
eviction-interval-timer-in-ms: 5000
# 响应缓存更新间隔
response-cache-update-interval-ms: 5000
# 响应缓存过期时间
response-cache-auto-expiration-in-seconds: 180
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
logging:
level:
com.netflix.eureka: DEBUG
com.netflix.discovery: DEBUG
4. 安全配置
SecurityConfig.java
package com.example.eurekaserver.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
/**
* Eureka Server 安全配置
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests(authz -> authz
.requestMatchers("/actuator/**").permitAll()
.anyRequest().authenticated()
)
.httpBasic();
return http.build();
}
}
Eureka Server 集群配置
高可用配置
application-peer1.yml
server:
port: 8761
spring:
application:
name: eureka-server
profiles:
active: peer1
eureka:
instance:
hostname: eureka-peer1
prefer-ip-address: false
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://admin:admin123@eureka-peer2:8762/eureka/,http://admin:admin123@eureka-peer3:8763/eureka/
server:
enable-self-preservation: true
renewal-percent-threshold: 0.85
application-peer2.yml
server:
port: 8762
spring:
application:
name: eureka-server
profiles:
active: peer2
eureka:
instance:
hostname: eureka-peer2
prefer-ip-address: false
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://admin:admin123@eureka-peer1:8761/eureka/,http://admin:admin123@eureka-peer3:8763/eureka/
server:
enable-self-preservation: true
renewal-percent-threshold: 0.85
application-peer3.yml
server:
port: 8763
spring:
application:
name: eureka-server
profiles:
active: peer3
eureka:
instance:
hostname: eureka-peer3
prefer-ip-address: false
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://admin:admin123@eureka-peer1:8761/eureka/,http://admin:admin123@eureka-peer2:8762/eureka/
server:
enable-self-preservation: true
renewal-percent-threshold: 0.85
2.3 Eureka Client 服务注册
创建服务提供者
1. 用户服务项目结构
user-service/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/example/userservice/
│ │ ├── UserServiceApplication.java
│ │ ├── controller/
│ │ │ └── UserController.java
│ │ ├── service/
│ │ │ ├── UserService.java
│ │ │ └── impl/
│ │ │ └── UserServiceImpl.java
│ │ ├── entity/
│ │ │ └── User.java
│ │ ├── repository/
│ │ │ └── UserRepository.java
│ │ └── config/
│ │ └── DatabaseConfig.java
│ └── resources/
│ ├── application.yml
│ ├── application-dev.yml
│ └── application-prod.yml
└── pom.xml
2. 依赖配置
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>2.7.14</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>user-service</artifactId>
<version>1.0.0</version>
<name>user-service</name>
<description>User Service for Microservices Demo</description>
<properties>
<java.version>11</java.version>
<spring-cloud.version>2021.0.8</spring-cloud.version>
</properties>
<dependencies>
<!-- Spring Boot Starters -->
<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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Spring Cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Database -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<!-- Utilities -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
3. 启动类
UserServiceApplication.java
package com.example.userservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/**
* 用户服务启动类
*
* @EnableEurekaClient 启用Eureka客户端功能
*/
@SpringBootApplication
@EnableEurekaClient
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
4. 实体类
User.java
package com.example.userservice.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.time.LocalDateTime;
/**
* 用户实体类
*/
@Entity
@Table(name = "users")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 50, message = "用户名长度必须在3-50个字符之间")
@Column(unique = true, nullable = false)
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
@Column(unique = true, nullable = false)
private String email;
@NotBlank(message = "密码不能为空")
@Size(min = 6, message = "密码长度不能少于6个字符")
@Column(nullable = false)
private String password;
@Column(name = "full_name")
private String fullName;
@Column(name = "phone_number")
private String phoneNumber;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
@Builder.Default
private UserStatus status = UserStatus.ACTIVE;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
/**
* 用户状态枚举
*/
public enum UserStatus {
ACTIVE, // 活跃
INACTIVE, // 非活跃
SUSPENDED, // 暂停
DELETED // 已删除
}
}
5. Repository 层
UserRepository.java
package com.example.userservice.repository;
import com.example.userservice.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.Optional;
/**
* 用户数据访问层
*/
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
/**
* 根据用户名查找用户
*/
Optional<User> findByUsername(String username);
/**
* 根据邮箱查找用户
*/
Optional<User> findByEmail(String email);
/**
* 检查用户名是否存在
*/
boolean existsByUsername(String username);
/**
* 检查邮箱是否存在
*/
boolean existsByEmail(String email);
/**
* 根据状态查找用户
*/
Page<User> findByStatus(User.UserStatus status, Pageable pageable);
/**
* 根据用户名或邮箱模糊查询
*/
@Query("SELECT u FROM User u WHERE u.username LIKE %:keyword% OR u.email LIKE %:keyword% OR u.fullName LIKE %:keyword%")
Page<User> findByKeyword(@Param("keyword") String keyword, Pageable pageable);
/**
* 统计活跃用户数量
*/
@Query("SELECT COUNT(u) FROM User u WHERE u.status = 'ACTIVE'")
long countActiveUsers();
}
6. Service 层
UserService.java
package com.example.userservice.service;
import com.example.userservice.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.util.Optional;
/**
* 用户服务接口
*/
public interface UserService {
/**
* 创建用户
*/
User createUser(User user);
/**
* 根据ID获取用户
*/
Optional<User> getUserById(Long id);
/**
* 根据用户名获取用户
*/
Optional<User> getUserByUsername(String username);
/**
* 根据邮箱获取用户
*/
Optional<User> getUserByEmail(String email);
/**
* 更新用户信息
*/
User updateUser(Long id, User user);
/**
* 删除用户
*/
void deleteUser(Long id);
/**
* 分页查询用户
*/
Page<User> getUsers(Pageable pageable);
/**
* 根据关键字搜索用户
*/
Page<User> searchUsers(String keyword, Pageable pageable);
/**
* 检查用户名是否存在
*/
boolean existsByUsername(String username);
/**
* 检查邮箱是否存在
*/
boolean existsByEmail(String email);
/**
* 获取活跃用户数量
*/
long getActiveUserCount();
}
UserServiceImpl.java
package com.example.userservice.service.impl;
import com.example.userservice.entity.User;
import com.example.userservice.repository.UserRepository;
import com.example.userservice.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
/**
* 用户服务实现类
*/
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
@Override
public User createUser(User user) {
log.info("Creating user: {}", user.getUsername());
// 检查用户名和邮箱是否已存在
if (existsByUsername(user.getUsername())) {
throw new IllegalArgumentException("用户名已存在: " + user.getUsername());
}
if (existsByEmail(user.getEmail())) {
throw new IllegalArgumentException("邮箱已存在: " + user.getEmail());
}
// 加密密码
user.setPassword(passwordEncoder.encode(user.getPassword()));
User savedUser = userRepository.save(user);
log.info("User created successfully: {}", savedUser.getId());
return savedUser;
}
@Override
@Transactional(readOnly = true)
public Optional<User> getUserById(Long id) {
log.debug("Getting user by id: {}", id);
return userRepository.findById(id);
}
@Override
@Transactional(readOnly = true)
public Optional<User> getUserByUsername(String username) {
log.debug("Getting user by username: {}", username);
return userRepository.findByUsername(username);
}
@Override
@Transactional(readOnly = true)
public Optional<User> getUserByEmail(String email) {
log.debug("Getting user by email: {}", email);
return userRepository.findByEmail(email);
}
@Override
public User updateUser(Long id, User user) {
log.info("Updating user: {}", id);
User existingUser = userRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("用户不存在: " + id));
// 更新用户信息
if (user.getFullName() != null) {
existingUser.setFullName(user.getFullName());
}
if (user.getPhoneNumber() != null) {
existingUser.setPhoneNumber(user.getPhoneNumber());
}
if (user.getStatus() != null) {
existingUser.setStatus(user.getStatus());
}
User updatedUser = userRepository.save(existingUser);
log.info("User updated successfully: {}", updatedUser.getId());
return updatedUser;
}
@Override
public void deleteUser(Long id) {
log.info("Deleting user: {}", id);
User user = userRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("用户不存在: " + id));
// 软删除:更新状态为已删除
user.setStatus(User.UserStatus.DELETED);
userRepository.save(user);
log.info("User deleted successfully: {}", id);
}
@Override
@Transactional(readOnly = true)
public Page<User> getUsers(Pageable pageable) {
log.debug("Getting users with pagination: {}", pageable);
return userRepository.findAll(pageable);
}
@Override
@Transactional(readOnly = true)
public Page<User> searchUsers(String keyword, Pageable pageable) {
log.debug("Searching users with keyword: {}", keyword);
return userRepository.findByKeyword(keyword, pageable);
}
@Override
@Transactional(readOnly = true)
public boolean existsByUsername(String username) {
return userRepository.existsByUsername(username);
}
@Override
@Transactional(readOnly = true)
public boolean existsByEmail(String email) {
return userRepository.existsByEmail(email);
}
@Override
@Transactional(readOnly = true)
public long getActiveUserCount() {
return userRepository.countActiveUsers();
}
}
7. Controller 层
UserController.java
package com.example.userservice.controller;
import com.example.userservice.entity.User;
import com.example.userservice.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Min;
import java.util.HashMap;
import java.util.Map;
/**
* 用户控制器
*/
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Slf4j
@Validated
@RefreshScope
public class UserController {
private final UserService userService;
@Value("${server.port}")
private String serverPort;
@Value("${spring.application.name}")
private String applicationName;
/**
* 健康检查接口
*/
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> health() {
Map<String, Object> response = new HashMap<>();
response.put("status", "UP");
response.put("service", applicationName);
response.put("port", serverPort);
response.put("timestamp", System.currentTimeMillis());
response.put("activeUsers", userService.getActiveUserCount());
return ResponseEntity.ok(response);
}
/**
* 创建用户
*/
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
log.info("Creating user: {}", user.getUsername());
try {
User createdUser = userService.createUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
} catch (IllegalArgumentException e) {
log.error("Error creating user: {}", e.getMessage());
return ResponseEntity.badRequest().build();
}
}
/**
* 根据ID获取用户
*/
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable @Min(1) Long id) {
log.debug("Getting user by id: {}", id);
return userService.getUserById(id)
.map(user -> ResponseEntity.ok().body(user))
.orElse(ResponseEntity.notFound().build());
}
/**
* 根据用户名获取用户
*/
@GetMapping("/username/{username}")
public ResponseEntity<User> getUserByUsername(@PathVariable String username) {
log.debug("Getting user by username: {}", username);
return userService.getUserByUsername(username)
.map(user -> ResponseEntity.ok().body(user))
.orElse(ResponseEntity.notFound().build());
}
/**
* 分页查询用户
*/
@GetMapping
public ResponseEntity<Page<User>> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "asc") String sortDir,
@RequestParam(required = false) String keyword) {
log.debug("Getting users - page: {}, size: {}, sortBy: {}, sortDir: {}, keyword: {}",
page, size, sortBy, sortDir, keyword);
Sort sort = sortDir.equalsIgnoreCase("desc") ?
Sort.by(sortBy).descending() :
Sort.by(sortBy).ascending();
Pageable pageable = PageRequest.of(page, size, sort);
Page<User> users = (keyword != null && !keyword.trim().isEmpty()) ?
userService.searchUsers(keyword, pageable) :
userService.getUsers(pageable);
return ResponseEntity.ok(users);
}
/**
* 更新用户
*/
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(
@PathVariable @Min(1) Long id,
@Valid @RequestBody User user) {
log.info("Updating user: {}", id);
try {
User updatedUser = userService.updateUser(id, user);
return ResponseEntity.ok(updatedUser);
} catch (IllegalArgumentException e) {
log.error("Error updating user: {}", e.getMessage());
return ResponseEntity.notFound().build();
}
}
/**
* 删除用户
*/
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable @Min(1) Long id) {
log.info("Deleting user: {}", id);
try {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
} catch (IllegalArgumentException e) {
log.error("Error deleting user: {}", e.getMessage());
return ResponseEntity.notFound().build();
}
}
/**
* 检查用户名是否存在
*/
@GetMapping("/check/username/{username}")
public ResponseEntity<Map<String, Boolean>> checkUsername(@PathVariable String username) {
boolean exists = userService.existsByUsername(username);
Map<String, Boolean> response = new HashMap<>();
response.put("exists", exists);
return ResponseEntity.ok(response);
}
/**
* 检查邮箱是否存在
*/
@GetMapping("/check/email/{email}")
public ResponseEntity<Map<String, Boolean>> checkEmail(@PathVariable String email) {
boolean exists = userService.existsByEmail(email);
Map<String, Boolean> response = new HashMap<>();
response.put("exists", exists);
return ResponseEntity.ok(response);
}
/**
* 获取用户统计信息
*/
@GetMapping("/stats")
public ResponseEntity<Map<String, Object>> getUserStats() {
Map<String, Object> stats = new HashMap<>();
stats.put("activeUsers", userService.getActiveUserCount());
stats.put("service", applicationName);
stats.put("port", serverPort);
return ResponseEntity.ok(stats);
}
}
8. 配置文件
application.yml
server:
port: 8081
spring:
application:
name: user-service
profiles:
active: dev
# 数据库配置
datasource:
url: jdbc:mysql://localhost:3306/microservice_user?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
# JPA配置
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
format_sql: true
# Jackson配置
jackson:
default-property-inclusion: non_null
date-format: yyyy-MM-dd HH:mm:ss
time-zone: Asia/Shanghai
# Eureka客户端配置
eureka:
client:
service-url:
defaultZone: http://admin:admin123@localhost:8761/eureka/
register-with-eureka: true
fetch-registry: true
# 从注册中心获取服务列表的间隔时间
registry-fetch-interval-seconds: 30
instance:
# 使用IP地址注册
prefer-ip-address: true
# 实例ID
instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${server.port}
# 心跳间隔
lease-renewal-interval-in-seconds: 30
# 服务失效时间
lease-expiration-duration-in-seconds: 90
# 健康检查路径
health-check-url-path: /actuator/health
# 状态页面路径
status-page-url-path: /actuator/info
# 元数据
metadata-map:
version: 1.0.0
description: User management service
# Actuator配置
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
info:
env:
enabled: true
# 应用信息
info:
app:
name: ${spring.application.name}
description: User Service for Microservices Architecture
version: 1.0.0
build:
artifact: user-service
group: com.example
# 日志配置
logging:
level:
com.example.userservice: DEBUG
org.springframework.cloud: DEBUG
com.netflix.eureka: INFO
com.netflix.discovery: INFO
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level [%logger{50}] - %msg%n"
application-dev.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
h2:
console:
enabled: true
path: /h2-console
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
eureka:
client:
service-url:
defaultZone: http://admin:admin123@localhost:8761/eureka/
logging:
level:
root: INFO
com.example.userservice: DEBUG
application-prod.yml
spring:
datasource:
url: jdbc:mysql://mysql-server:3306/microservice_user?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai
username: ${DB_USERNAME:user_service}
password: ${DB_PASSWORD:password}
jpa:
hibernate:
ddl-auto: validate
show-sql: false
eureka:
client:
service-url:
defaultZone: http://admin:admin123@eureka-server:8761/eureka/
instance:
prefer-ip-address: false
hostname: user-service
logging:
level:
root: WARN
com.example.userservice: INFO
file:
name: /var/log/user-service.log
2.4 服务发现与调用
使用 RestTemplate 进行服务调用
配置 RestTemplate
RestTemplateConfig.java
package com.example.orderservice.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* RestTemplate配置类
*/
@Configuration
public class RestTemplateConfig {
/**
* 创建负载均衡的RestTemplate
* @LoadBalanced 注解启用客户端负载均衡
*/
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
服务调用示例
OrderService.java
package com.example.orderservice.service.impl;
import com.example.orderservice.entity.Order;
import com.example.orderservice.service.OrderService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
/**
* 订单服务实现类
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class OrderServiceImpl implements OrderService {
private final RestTemplate restTemplate;
@Override
public Order createOrder(CreateOrderRequest request) {
log.info("Creating order for user: {}", request.getUserId());
// 调用用户服务获取用户信息
String userServiceUrl = "http://user-service/api/users/" + request.getUserId();
try {
User user = restTemplate.getForObject(userServiceUrl, User.class);
if (user == null) {
throw new IllegalArgumentException("用户不存在: " + request.getUserId());
}
// 创建订单
Order order = Order.builder()
.userId(user.getId())
.userName(user.getUsername())
.userEmail(user.getEmail())
.totalAmount(request.getTotalAmount())
.status(Order.OrderStatus.PENDING)
.build();
// 保存订单
Order savedOrder = orderRepository.save(order);
log.info("Order created successfully: {}", savedOrder.getId());
return savedOrder;
} catch (Exception e) {
log.error("Error calling user service: {}", e.getMessage());
throw new RuntimeException("创建订单失败: " + e.getMessage());
}
}
}
使用 DiscoveryClient 进行服务发现
ServiceDiscoveryController.java
package com.example.orderservice.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 服务发现控制器
*/
@RestController
@RequestMapping("/api/discovery")
@RequiredArgsConstructor
public class ServiceDiscoveryController {
private final DiscoveryClient discoveryClient;
/**
* 获取所有服务列表
*/
@GetMapping("/services")
public List<String> getServices() {
return discoveryClient.getServices();
}
/**
* 获取指定服务的实例列表
*/
@GetMapping("/services/{serviceName}/instances")
public List<ServiceInstance> getServiceInstances(@PathVariable String serviceName) {
return discoveryClient.getInstances(serviceName);
}
/**
* 获取服务实例详细信息
*/
@GetMapping("/services/{serviceName}/info")
public ServiceInstance getServiceInfo(@PathVariable String serviceName) {
List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
if (instances.isEmpty()) {
throw new RuntimeException("Service not found: " + serviceName);
}
// 返回第一个实例
return instances.get(0);
}
}
2.5 总结
本章详细介绍了Spring Cloud中的服务注册与发现机制:
核心概念
- 服务注册中心:集中管理服务实例信息
- 服务提供者:注册服务并提供服务能力
- 服务消费者:发现并调用其他服务
Eureka 特点
- AP模式:保证可用性和分区容错性
- 自我保护机制:防止网络分区时误删服务实例
- 客户端缓存:提高服务发现性能
- 集群支持:支持多节点部署提高可用性
最佳实践
- 健康检查:配置合适的健康检查路径
- 实例元数据:添加有用的实例元数据信息
- 负载均衡:使用@LoadBalanced注解启用客户端负载均衡
- 异常处理:妥善处理服务调用异常
- 监控告警:监控服务注册状态和调用情况
在下一章中,我们将学习如何使用OpenFeign进行声明式服务调用,以及Ribbon负载均衡的配置和使用。