Parcourir la source

1.content-service 中添加一个 consumer 的 hystrix 降级熔断 demo
2.使用 feign 实现了 SpringCloud 服务间调用, feign 调用 service-provider 提供的服务时也通过 CircuitBreaker 实现了降级熔断

reghao il y a 8 mois
Parent
commit
6384ce9fcd

+ 5 - 0
content/content-service/pom.xml

@@ -149,6 +149,11 @@
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-starter-openfeign</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
+            <version>2.2.10.RELEASE</version>
+        </dependency>
 
         <dependency>
             <groupId>org.springframework.boot</groupId>

+ 13 - 0
content/content-service/src/main/java/cn/reghao/tnb/content/app/config/SpringCloudConfig.java

@@ -0,0 +1,13 @@
+package cn.reghao.tnb.content.app.config;
+
+import org.springframework.cloud.netflix.hystrix.EnableHystrix;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author reghao
+ * @date 2025-07-22 09:30:18
+ */
+@Configuration
+@EnableHystrix
+public class SpringCloudConfig {
+}

+ 16 - 0
content/content-service/src/main/java/cn/reghao/tnb/content/app/mall/controller/ProductController.java

@@ -9,6 +9,9 @@ import cn.reghao.tnb.content.app.mall.model.dto.ProductAddDto;
 import cn.reghao.tnb.content.app.mall.model.po.Product;
 import cn.reghao.tnb.content.app.mall.service.BuyService;
 import cn.reghao.tnb.content.app.mall.service.ProductService;
+import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
+import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
+import com.netflix.hystrix.contrib.javanica.conf.HystrixPropertiesManager;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import io.swagger.v3.oas.annotations.Operation;
 import org.springframework.http.MediaType;
@@ -54,11 +57,24 @@ public class ProductController {
 
     @Operation(summary = "获取商品详情", description = "N")
     @GetMapping(value = "/{itemId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    @HystrixCommand(
+            fallbackMethod = "getProductFallback",
+            commandProperties = {
+                    @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_ENABLED, value = "false"),
+                    @HystrixProperty(name="metrics.rollingStats.timeInMilliseconds", value="5000"),
+                    @HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="4"),
+                    @HystrixProperty(name="circuitBreaker.errorThresholdPercentage", value="50"),
+                    @HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds", value="3000")
+            })
     public String getProduct(@PathVariable("itemId") Long itemId) {
         Product product = productService.getProduct(itemId);
         return WebResult.success(product);
     }
 
+    public String getProductFallback(Long itemId) {
+        return WebResult.fallback("fallback");
+    }
+
     @Operation(summary = "购买商品", description = "N")
     @PostMapping("/buy")
     public String buyProduct(@RequestBody @Validated BuyProductDto buyProductDto) {

+ 77 - 0
content/content-service/src/main/java/cn/reghao/tnb/content/app/mall/feign/CircuitBreakerConfig.java

@@ -0,0 +1,77 @@
+package cn.reghao.tnb.content.app.mall.feign;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
+import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
+import org.springframework.cloud.client.circuitbreaker.ConfigBuilder;
+import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * @author reghao
+ * @date 2025-07-22 10:46:17
+ */
+@Slf4j
+@Configuration
+public class CircuitBreakerConfig {
+    @Bean
+    MyCircuitBreaker myCircuitBreaker() {
+        return new MyCircuitBreaker();
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Bean
+    CircuitBreakerFactory circuitBreakerFactory(MyCircuitBreaker myCircuitBreaker) {
+        return new CircuitBreakerFactory() {
+            @Override
+            public CircuitBreaker create(String id) {
+                log.info("Creating a circuit breaker with id [{}]", id);
+                return myCircuitBreaker;
+            }
+
+            @Override
+            protected ConfigBuilder configBuilder(String id) {
+                return Object::new;
+            }
+
+            @Override
+            public void configureDefault(Function defaultConfiguration) {
+
+            }
+        };
+    }
+
+    static class MyCircuitBreaker implements CircuitBreaker {
+        AtomicBoolean runWasCalled = new AtomicBoolean();
+
+        @Override
+        public <T> T run(Supplier<T> toRun) {
+            try {
+                this.runWasCalled.set(true);
+                return toRun.get();
+            }
+            catch (Throwable throwable) {
+                throw new NoFallbackAvailableException("No fallback available.", throwable);
+            }
+        }
+
+        @Override
+        public <T> T run(Supplier<T> toRun, Function<Throwable, T> fallback) {
+            try {
+                return run(toRun);
+            }
+            catch (Throwable throwable) {
+                return fallback.apply(throwable);
+            }
+        }
+
+        public void clear() {
+            this.runWasCalled.set(false);
+        }
+    }
+}

+ 1 - 1
content/content-service/src/main/java/cn/reghao/tnb/content/app/vod/feign/FeignClientConfig.java → content/content-service/src/main/java/cn/reghao/tnb/content/app/mall/feign/FeignClientConfig.java

@@ -1,4 +1,4 @@
-package cn.reghao.tnb.content.app.vod.feign;
+package cn.reghao.tnb.content.app.mall.feign;
 
 import org.springframework.cloud.openfeign.EnableFeignClients;
 import org.springframework.context.annotation.Configuration;

+ 28 - 0
content/content-service/src/main/java/cn/reghao/tnb/content/app/mall/feign/UserFeignClient.java

@@ -0,0 +1,28 @@
+package cn.reghao.tnb.content.app.mall.feign;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+/**
+ * @author reghao
+ * @date 2025-07-19 23:57:25
+ */
+@FeignClient(name = "user-service", fallback = UserFeignClient.UserFeignClientImpl.class)
+public interface UserFeignClient {
+    @GetMapping(value = "/api/user/hystrix/info", produces = MediaType.APPLICATION_JSON_VALUE)
+    String getUserInfo(@RequestParam("userId") Long userId);
+
+    @Slf4j
+    @Component
+    class UserFeignClientImpl implements UserFeignClient {
+        @Override
+        public String getUserInfo(Long userId) {
+            log.info("getUserInfo fallback");
+            return "";
+        }
+    }
+}

+ 11 - 1
content/content-service/src/main/java/cn/reghao/tnb/content/app/mall/service/ProductService.java

@@ -3,6 +3,7 @@ package cn.reghao.tnb.content.app.mall.service;
 import cn.reghao.file.api.iface.OssService;
 import cn.reghao.jutil.jdk.db.PageList;
 import cn.reghao.jutil.jdk.result.Result;
+import cn.reghao.jutil.jdk.serializer.JsonConverter;
 import cn.reghao.jutil.tool.id.SnowFlake;
 import cn.reghao.oss.sdk.model.dto.media.ImageInfo;
 import cn.reghao.tnb.content.api.dto.TaobaoItem;
@@ -10,6 +11,8 @@ import cn.reghao.tnb.content.app.mall.db.mapper.ProductMapper;
 import cn.reghao.tnb.content.app.mall.db.repository.MallRepository;
 import cn.reghao.tnb.content.app.mall.model.dto.ProductAddDto;
 import cn.reghao.tnb.content.app.mall.model.po.Product;
+import cn.reghao.tnb.content.app.mall.feign.UserFeignClient;
+import cn.reghao.tnb.user.api.dto.UserInfo;
 import org.apache.dubbo.config.annotation.DubboReference;
 import org.springframework.stereotype.Service;
 
@@ -28,11 +31,13 @@ public class ProductService {
     private final SnowFlake idGenerator;
     private final ProductMapper productMapper;
     private final MallRepository mallRepository;
+    private final UserFeignClient userFeignClient;
 
-    public ProductService(ProductMapper productMapper, MallRepository mallRepository) {
+    public ProductService(ProductMapper productMapper, MallRepository mallRepository, UserFeignClient userFeignClient) {
         this.idGenerator = new SnowFlake(1L, 1L);
         this.productMapper = productMapper;
         this.mallRepository = mallRepository;
+        this.userFeignClient = userFeignClient;
     }
 
     public void add(TaobaoItem taobaoItem) {
@@ -63,6 +68,11 @@ public class ProductService {
     }
 
     public Product getProduct(Long itemId) {
+        Product product = mallRepository.getProduct(itemId);
+        long sellerId = product.getSellerId();
+        String userInfoJson = userFeignClient.getUserInfo(sellerId);
+        UserInfo userInfo = JsonConverter.jsonToObject(userInfoJson, UserInfo.class);
+
         return mallRepository.getProduct(itemId);
     }
 }

+ 0 - 16
content/content-service/src/main/java/cn/reghao/tnb/content/app/vod/feign/UserFeignClient.java

@@ -1,16 +0,0 @@
-package cn.reghao.tnb.content.app.vod.feign;
-
-import org.springframework.cloud.openfeign.FeignClient;
-import org.springframework.http.MediaType;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-
-/**
- * @author reghao
- * @date 2025-07-19 23:57:25
- */
-@FeignClient(value = "user-service")
-public interface UserFeignClient {
-    @GetMapping(value = "/api/user/feign/info", produces = MediaType.APPLICATION_JSON_VALUE)
-    String getUserInfo(@RequestParam("userId") Long userId);
-}

+ 1 - 9
content/content-service/src/main/java/cn/reghao/tnb/content/app/vod/service/impl/VideoPostQueryImpl.java

@@ -3,7 +3,6 @@ package cn.reghao.tnb.content.app.vod.service.impl;
 import cn.reghao.file.api.iface.OssService;
 import cn.reghao.jutil.jdk.db.Page;
 import cn.reghao.jutil.jdk.db.PageList;
-import cn.reghao.jutil.jdk.serializer.JsonConverter;
 import cn.reghao.jutil.web.WebResult;
 import cn.reghao.oss.sdk.model.dto.media.VideoInfo;
 import cn.reghao.oss.sdk.model.dto.media.VideoUrlDto;
@@ -17,7 +16,6 @@ import cn.reghao.tnb.content.api.dto.VideoCard;
 import cn.reghao.tnb.content.api.dto.VideoPostCard;
 import cn.reghao.tnb.content.app.vod.db.mapper.*;
 import cn.reghao.tnb.content.app.vod.db.repository.VideoRepository;
-import cn.reghao.tnb.content.app.vod.feign.UserFeignClient;
 import cn.reghao.tnb.content.app.vod.model.po.*;
 import cn.reghao.tnb.content.app.vod.model.vo.BannerVideoVO;
 import cn.reghao.tnb.content.app.vod.model.vo.VideoDetail;
@@ -26,7 +24,6 @@ import cn.reghao.tnb.content.app.vod.service.CategoryService;
 import cn.reghao.tnb.content.app.vod.service.ContentPermission;
 import cn.reghao.tnb.content.app.vod.service.VideoPostQuery;
 import cn.reghao.tnb.user.api.dto.UserCard;
-import cn.reghao.tnb.user.api.dto.UserInfo;
 import cn.reghao.tnb.user.api.iface.UserService;
 import org.apache.dubbo.config.annotation.DubboReference;
 import org.springframework.stereotype.Service;
@@ -64,13 +61,12 @@ public class VideoPostQueryImpl implements VideoPostQuery {
     private final VideoRepository videoRepository;
     private final Random random = new SecureRandom();
     private final BannerVideoMapper bannerVideoMapper;
-    private final UserFeignClient userFeignClient;
 
     public VideoPostQueryImpl(VideoPostMapper videoPostMapper, VideoFileMapper videoFileMapper, VideoTagMapper videoTagMapper,
                               VideoStatisticMapper videoStatisticMapper, VideoCategoryPostMapper videoCategoryPostMapper,
                               ContentPermission contentPermission, CategoryService categoryService,
                               VideoRepository videoRepository, VideoPostTagMapper videoPostTagMapper,
-                              BannerVideoMapper bannerVideoMapper, UserFeignClient userFeignClient) {
+                              BannerVideoMapper bannerVideoMapper) {
         this.videoPostMapper = videoPostMapper;
         this.videoFileMapper = videoFileMapper;
         this.videoTagMapper = videoTagMapper;
@@ -81,7 +77,6 @@ public class VideoPostQueryImpl implements VideoPostQuery {
         this.videoRepository = videoRepository;
         this.videoPostTagMapper = videoPostTagMapper;
         this.bannerVideoMapper = bannerVideoMapper;
-        this.userFeignClient = userFeignClient;
     }
 
     public List<VideoCard> getVideoCards(List<String> videoIds) {
@@ -262,9 +257,6 @@ public class VideoPostQueryImpl implements VideoPostQuery {
         }
 
         long publishBy = videoPost.getPublishBy();
-        String userInfoJson = userFeignClient.getUserInfo(publishBy);
-        UserInfo userInfo = JsonConverter.jsonToObject(userInfoJson, UserInfo.class);
-
         String publishByStr = accountQuery.getUserIdStr(publishBy);
         VideoStatistic videoStatistic = videoStatisticMapper.findByVideoId(videoId);
         VideoDetail videoDetail = new VideoDetail(videoPost, videoStatistic, publishByStr);

+ 11 - 0
content/content-service/src/main/resources/application.yml

@@ -37,6 +37,9 @@ spring:
         - veth.*
       preferred-networks:
         - 192.168
+    loadbalancer:
+      cache:
+        enabled: true
 mybatis:
   configuration:
     map-underscore-to-camel-case: true
@@ -48,6 +51,14 @@ eureka:
   client:
     register-with-eureka: true
     fetch-registry: true
+feign:
+  circuitbreaker:
+    enabled: true
+  httpclient:
+    hc5:
+      enabled: true
+      connection-request-timeout-unit: seconds
+      connection-request-timeout: 1
 management:
   endpoints:
     web: