锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

spring cloud alibaba开发笔记八(商品微服务,异步及管理)

时间:2022-10-25 11:00:01 7p8pin连接器

微服务的初始化

在service在服务下,创建商品微服务goods-service

启动类

/**  * 

启动商品微服务入口

* 启动依赖组件/中间件: Redis MySQL Nacos Kafka Zipkin * http://127.0.0.1:8001/ecommerce-goods-service/doc.html */ @EnableJpaAuditing @EnableDiscoveryClient @SpringCloudApplication public class GoodsApplication { public static void main(String[] args) { SpringApplication.run(GoodsApplication.class, args); } }

pom

               e-commerce-service         com.taluohui.ecommerce         1.0-SNAPSHOT          4.0.0      e-commerce-goods-service     1.0-SNAPSHOT     jar           e-commerce-goods-service     商品服务                                     com.alibaba.cloud             spring-cloud-starter-alibaba-nacos-discovery                                        org.springframework.cloud             spring-cloud-starter-zipkin                               org.springframework.kafka             spring-kafka             2.5.0.RELEASE                                        org.springframework.boot             spring-boot-starter-data-redis                                        org.springframework.boot             spring-boot-starter-data-jpa                                        mysql             mysql-connector-java             8.0.12             runtime                                        org.springframework.boot             spring-boot-starter-aop                               com.taluohui.ecommerce             e-commerce-service-config             1.0-SNAPSHOT                               com.taluohui.ecommerce             e-commerce-service-sdk             1.0-SNAPSHOT                                  ${artifactId}                                       org.springframework.boot                 spring-boot-maven-plugin                                                                                            repackage                                                                                            

配置项

server:   port: 8001   servlet:     context-path: /ecommerce-goods-service  spring:   application:     name: e-commerce-goods-service # 也构成了应用名称 Nacos 配置管理 dataId 部分字段 (当 config.prefix 为空时)   cloud:     nacos:       # 发现服务注册       discovery:         enabled: rue # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
        server-addr: 127.0.0.1:8848
        # server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850 # Nacos 服务器地址
        namespace: 22d40198-8462-499d-a7fe-dbb2da958648
        metadata:
          management:
            context-path: ${server.servlet.context-path}/actuator
  kafka:
    bootstrap-servers: 1.15.247.9:9092
    producer:
      retries: 3
    consumer:
      auto-offset-reset: latest
  sleuth:
    sampler:
      # ProbabilityBasedSampler 抽样策略
      probability: 1.0  # 采样比例, 1.0 表示 100%, 默认是 0.1
      # RateLimitingSampler 抽样策略, 设置了限速采集, spring.sleuth.sampler.probability 属性值无效
      rate: 100  # 每秒间隔接受的 trace 量
  zipkin:
    sender:
      type: kafka # 默认是 web
    base-url: http://127.0.0.1:9411/
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: none
    properties:
      hibernate.show_sql: true
      hibernate.format_sql: true
    open-in-view: false
  datasource:
    # 数据源
    url: jdbc:mysql://127.0.0.1:3306/ecommerce?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false
    username: root
    password: Cjw970404
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    # 连接池
    hikari:
      maximum-pool-size: 8
      minimum-idle: 4
      idle-timeout: 30000
      connection-timeout: 30000
      max-lifetime: 45000
      auto-commit: true
      pool-name: ImoocEcommerceHikariCP

# 暴露端点
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always

枚举类和转换方法

枚举类示例,在文件夹constant下

/**
 * 

品牌分类

* */ @Getter @AllArgsConstructor public enum BrandCategory { BRAND_A("20001", "品牌A"), BRAND_B("20002", "品牌B"), BRAND_C("20003", "品牌C"), BRAND_D("20004", "品牌D"), BRAND_E("20005", "品牌E"), ; /** 品牌分类编码 */ private final String code; /** 品牌分类描述信息 */ private final String description; /** *

根据 code 获取到 BrandCategory

* */ public static BrandCategory of(String code) { Objects.requireNonNull(code); return Stream.of(values()) .filter(bean -> bean.code.equals(code)) .findAny() .orElseThrow( () -> new IllegalArgumentException(code + " not exists") ); } }

转换方法,在文件夹converter下

/**
 * 

品牌分类枚举属性转换器

* */ public class BrandCategoryConverter implements AttributeConverter { @Override public String convertToDatabaseColumn(BrandCategory brandCategory) { return brandCategory.getCode(); } @Override public BrandCategory convertToEntityAttribute(String code) { return BrandCategory.of(code); } }

再用以下方法创建两个枚举类和他们的转换方法

/**
 * 

商品类别

* 电器 -> 手机、电脑 * */ public enum GoodsCategory { DIAN_QI("10001", "电器"), JIA_JU("10002", "家具"), FU_SHI("10003", "服饰"), MY_YIN("10004", "母婴"), SHI_PIN("10005", "食品"), TU_SHU("10006", "图书"), ; ······································· } /** *

商品状态枚举类

*/ @Getter @AllArgsConstructor public enum GoodsStatus { ONLINE(101, "上线"), OFFLINE(102, "下线"), STOCK_OUT(103, "缺货"), ; ····································· }

在mysql中创建商品表

-- 创建 t_ecommerce_goods 数据表
CREATE TABLE IF NOT EXISTS `ecommerce`.`t_ecommerce_goods` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `goods_category` varchar(64) NOT NULL DEFAULT '' COMMENT '商品类别',
  `brand_category` varchar(64) NOT NULL DEFAULT '' COMMENT '品牌分类',
  `goods_name` varchar(64) NOT NULL DEFAULT '' COMMENT '商品名称',
  `goods_pic` varchar(256) NOT NULL DEFAULT '' COMMENT '商品图片',
  `goods_description` varchar(512) NOT NULL DEFAULT '' COMMENT '商品描述信息',
  `goods_status` int(11) NOT NULL DEFAULT 0 COMMENT '商品状态',
  `price` int(11) NOT NULL DEFAULT 0 COMMENT '商品价格',
  `supply` bigint(20) NOT NULL DEFAULT 0 COMMENT '总供应量',
  `inventory` bigint(20) NOT NULL DEFAULT 0 COMMENT '库存',
  `goods_property` varchar(1024) NOT NULL DEFAULT '' COMMENT '商品属性',
  `create_time` datetime NOT NULL DEFAULT '0000-01-01 00:00:00' COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT '0000-01-01 00:00:00' COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `goods_category_brand_name` (`goods_category`, `brand_category`, `goods_name`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='商品表';

在entity文件下,创建对应的类以及对应的转换方法,在vo文件夹下创建对应的展示类。在sdk通用模块下创建对应的通用类。

Dao

public interface EcommerceGoodsDao extends PagingAndSortingRepository {

    /**
     * 

根据查询条件查询商品表, 并限制返回结果

* select * from t_ecommerce_goods where goods_category = ? and brand_category = ? * and goods_name = ? limit 1; * */ Optional findFirst1ByGoodsCategoryAndBrandCategoryAndGoodsName( GoodsCategory goodsCategory, BrandCategory brandCategory, String goodsName ); }

Service接口

/**
 * 

商品微服务相关服务接口定义

* */ public interface IGoodsService { /** *

根据 TableId 查询商品详细信息

* */ List getGoodsInfoByTableId(TableId tableId); /** *

获取分页的商品信息

* */ PageSimpleGoodsInfo getSimpleGoodsInfoByPage(int page); /** *

根据 TableId 查询简单商品信息

* */ List getSimpleGoodsInfoByTableId(TableId tableId); /** *

扣减商品库存

* */ Boolean deductGoodsInventory(List deductGoodsInventories); }

使用异步的方式进行入库操作

异步类,在service文件夹下再创建async文件夹

/**
 * 

异步服务接口定义

*/ public interface IAsyncService { /** *

异步将商品信息保存下来

* */ void asyncImportGoods(List goodsInfos, String taskId); }

在vo文件夹下,创建用于异步管理的类

/**
 * 

异步任务执行信息

* */ @Data @NoArgsConstructor @AllArgsConstructor public class AsyncTaskInfo { /** 异步任务 id */ private String taskId; /** 异步任务开始时间 */ private Date startTime; /** 异步任务结束时间 */ private Date endTime; /** 异步任务总耗时 */ private String totalTime; }

在商品微服务下创建config文件夹,自定义异步任务线程池,异步任务异常捕获处理器

import org.apache.commons.lang3.time.StopWatch;

/**
 * 

自定义异步任务线程池,异步任务异常捕获处理器

*/ @Slf4j @EnableAsync //开启Spring异步任务支持 @Configuration public class AsyncPoolConfig implements AsyncConfigurer { /** *

将自定义的线程池注入到 Spring 容器中

* */ @Bean @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(20); executor.setQueueCapacity(20); executor.setKeepAliveSeconds(60); executor.setThreadNamePrefix("Qinyi-Async-"); // 这个非常重要 // 等待所有任务结果候再关闭线程池 executor.setWaitForTasksToCompleteOnShutdown(true); executor.setAwaitTerminationSeconds(60); // 定义拒绝策略 executor.setRejectedExecutionHandler( new ThreadPoolExecutor.CallerRunsPolicy() ); // 初始化线程池, 初始化 core 线程 executor.initialize(); return executor; } /** *

指定系统中的异步任务在出现异常时使用到的处理器

* */ @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return new AsyncExceptionHandler(); } /** *

异步任务异常捕获处理器

* */ @SuppressWarnings("all") class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler { @Override public void handleUncaughtException(Throwable throwable, Method method, Object... objects) { throwable.printStackTrace(); log.error("Async Error: [{}], Method: [{}], Param: [{}]", throwable.getMessage(), method.getName(), JSON.toJSONString(objects)); // TODO 发送邮件或者是短信, 做进一步的报警处理 } } }

异步任务的实现,商品信息会同步到redis中

/**
 * 

异步服务接口实现

* */ @Slf4j @Service @Transactional(rollbackFor = Exception.class) public class AsyncServiceImpl implements IAsyncService { private final EcommerceGoodsDao ecommerceGoodsDao; private final StringRedisTemplate redisTemplate; public AsyncServiceImpl(EcommerceGoodsDao ecommerceGoodsDao, StringRedisTemplate redisTemplate) { this.ecommerceGoodsDao = ecommerceGoodsDao; this.redisTemplate = redisTemplate; } /** *

异步任务需要加上注解, 并指定使用的线程池

* 异步任务处理两件事: * 1. 将商品信息保存到数据表 * 2. 更新商品缓存 * */ @Async("getAsyncExecutor") @Override public void asyncImportGoods(List goodsInfos, String taskId) { log.info("async task running taskId: [{}]", taskId); StopWatch watch = StopWatch.createStarted(); // 1. 如果是 goodsInfo 中存在重复的商品, 不保存; 直接返回, 记录错误日志 // 请求数据是否合法的标记 boolean isIllegal = false; // 将商品信息字段 joint 在一起, 用来判断是否存在重复 Set goodsJointInfos = new HashSet<>(goodsInfos.size()); // 过滤出来的, 可以入库的商品信息(规则按照自己的业务需求自定义即可) List filteredGoodsInfo = new ArrayList<>(goodsInfos.size()); // 走一遍循环, 过滤非法参数与判定当前请求是否合法 for (GoodsInfo goods : goodsInfos) { // 基本条件不满足的, 直接过滤器 if (goods.getPrice() <= 0 || goods.getSupply() <= 0) { log.info("goods info is invalid: [{}]", JSON.toJSONString(goods)); continue; } // 组合商品信息 String jointInfo = String.format( "%s,%s,%s", goods.getGoodsCategory(), goods.getBrandCategory(), goods.getGoodsName() ); if (goodsJointInfos.contains(jointInfo)) { isIllegal = true; } // 加入到两个容器中 goodsJointInfos.add(jointInfo); filteredGoodsInfo.add(goods); } // 如果存在重复商品或者是没有需要入库的商品, 直接打印日志返回 if (isIllegal || CollectionUtils.isEmpty(filteredGoodsInfo)) { watch.stop(); log.warn("import nothing: [{}]", JSON.toJSONString(filteredGoodsInfo)); log.info("check and import goods done: [{}ms]", watch.getTime(TimeUnit.MILLISECONDS)); return; } List ecommerceGoods = filteredGoodsInfo.stream() .map(EcommerceGoods::to) .collect(Collectors.toList()); List targetGoods = new ArrayList<>(ecommerceGoods.size()); // 2. 保存 goodsInfo 之前先判断下是否存在重复商品 ecommerceGoods.forEach(g -> { // limit 1 if (null != ecommerceGoodsDao .findFirst1ByGoodsCategoryAndBrandCategoryAndGoodsName( g.getGoodsCategory(), g.getBrandCategory(), g.getGoodsName() ).orElse(null)) { return; } targetGoods.add(g); }); // 商品信息入库 List savedGoods = IterableUtils.toList( ecommerceGoodsDao.saveAll(targetGoods) ); // 将入库商品信息同步到 Redis 中 saveNewGoodsInfoToRedis(savedGoods); log.info("save goods info to db and redis: [{}]", savedGoods.size()); watch.stop(); log.info("check and import goods success: [{}ms]", watch.getTime(TimeUnit.MILLISECONDS)); } /** *

将保存到数据表中的数据缓存到 Redis 中

* dict: key -> * */ private void saveNewGoodsInfoToRedis(List savedGoods) { // 由于 Redis 是内存存储, 只存储简单商品信息 List simpleGoodsInfos = savedGoods.stream() .map(EcommerceGoods::toSimple) .collect(Collectors.toList()); Map id2JsonObject = new HashMap<>(simpleGoodsInfos.size()); simpleGoodsInfos.forEach( g -> id2JsonObject.put(g.getId().toString(), JSON.toJSONString(g)) ); // 保存到 Redis 中 redisTemplate.opsForHash().putAll( GoodsConstant.ECOMMERCE_GOODS_DICT_KEY, id2JsonObject ); } }

redis的Key值,定义到常量中

/**
 * 

商品常量信息

* */ public class GoodsConstant { /** redis key */ public static final String ECOMMERCE_GOODS_DICT_KEY = "ecommerce:goods:dict:20220308"; }

异步任务的管理

定义枚举类

/**
 * 

异步任务状态枚举

* */ @Getter @AllArgsConstructor public enum AsyncTaskStatusEnum { STARTED(0, "已经启动"), RUNNING(1, "正在运行"), SUCCESS(2, "执行成功"), FAILED(3, "执行失败"), ; /** 执行状态编码 */ private final int state; /** 执行状态描述 */ private final String stateInfo; }

异步任务执行管理器

我们并不直接调用service中的方法,通过多方法的包装,我们可以实现对任务的监控。

/**
 * 

异步任务执行管理器

* 对异步任务进行包装管理, 记录并塞入异步任务执行信息 * */ @Slf4j @Component public class AsyncTaskManager { /** 异步任务执行信息容器,这里作为演示,使用的map,可以使用mysql之类的做持久化 */ private final Map taskContainer = new HashMap<>(16); private final IAsyncService asyncService; public AsyncTaskManager(IAsyncService asyncService) { this.asyncService = asyncService; } /** *

初始化异步任务

* */ public AsyncTaskInfo initTask() { AsyncTaskInfo taskInfo = new AsyncTaskInfo(); // 设置一个唯一的异步任务 id, 只要唯一即可 taskInfo.setTaskId(UUID.randomUUID().toString()); taskInfo.setStatus(AsyncTaskStatusEnum.STARTED); taskInfo.setStartTime(new Date()); // 初始化的时候就要把异步任务执行信息放入到存储容器中 taskContainer.put(taskInfo.getTaskId(), taskInfo); return taskInfo; } /** *

提交异步任务

* */ public AsyncTaskInfo submit(List goodsInfos) { // 初始化一个异步任务的监控信息 AsyncTaskInfo taskInfo = initTask(); asyncService.asyncImportGoods(goodsInfos, taskInfo.getTaskId()); return taskInfo; } /** *

设置异步任务执行状态信息

* */ public void setTaskInfo(AsyncTaskInfo taskInfo) { taskContainer.put(taskInfo.getTaskId(), taskInfo); } /** *

获取异步任务执行状态信息

* */ public AsyncTaskInfo getTaskInfo(String taskId) { return taskContainer.get(taskId); } }

使用AOP实现对异步任务的监控,以及状态的修改

/**
 * 

异步任务执行监控切面

* */ @Slf4j @Aspect @Component public class AsyncTaskMonitor { /** 注入异步任务管理器 */ private final AsyncTaskManager asyncTaskManager; public AsyncTaskMonitor(AsyncTaskManager asyncTaskManager) { this.asyncTaskManager = asyncTaskManager; } /** *

异步任务执行的环绕切面

* 环绕切面让我们可以在方法执行之前和执行之后做一些 "额外" 的操作 * */ @Around("execution(* com.taluohui.ecommerce.service.async.AsyncServiceImpl.*(..))") public Object taskHandle(ProceedingJoinPoint proceedingJoinPoint) { // 获取 taskId, 调用异步任务传入的第二个参数 String taskId = proceedingJoinPoint.getArgs()[1].toString(); // 获取任务信息, 在提交任务的时候就已经放入到容器中了 AsyncTaskInfo taskInfo = asyncTaskManager.getTaskInfo(taskId); log.info("AsyncTaskMonitor is monitoring async task: [{}]", taskId); taskInfo.setStatus(AsyncTaskStatusEnum.RUNNING); asyncTaskManager.setTaskInfo(taskInfo); // 设置为运行状态, 并重新放入容器 AsyncTaskStatusEnum status; Object result; try { // 执行异步任务 result = proceedingJoinPoint.proceed(); status = AsyncTaskStatusEnum.SUCCESS; } catch (Throwable ex) { // 异步任务出现了异常 result = null; status = AsyncTaskStatusEnum.FAILED; log.error("AsyncTaskMonitor: async task [{}] is failed, Error Info: [{}]", taskId, ex.getMessage(), ex); } // 设置异步任务其他的信息, 再次重新放入到容器中 taskInfo.setEndTime(new Date()); taskInfo.setStatus(status); taskInfo.setTotalTime(String.valueOf( taskInfo.getEndTime().getTime() - taskInfo.getStartTime().getTime() )); asyncTaskManager.setTaskInfo(taskInfo); return result; } }

异步服务对外接口接口

/**
 * 

异步任务服务对外提供的 API

* */ @Api(tags = "商品异步入库服务") @Slf4j @RestController @RequestMapping("/async-goods") public class AsyncGoodsController { private final AsyncTaskManager asyncTaskManager; public AsyncGoodsController(AsyncTaskManager asyncTaskManager) { this.asyncTaskManager = asyncTaskManager; } @ApiOperation(value = "导入商品", notes = "导入商品进入到商品表", httpMethod = "POST") @PostMapping("/import-goods") public AsyncTaskInfo importGoods(@RequestBody List goodsInfos) { return asyncTaskManager.submit(goodsInfos); } @ApiOperation(value = "查询状态", notes = "查询异步任务的执行状态", httpMethod = "GET") @GetMapping("/task-info") public AsyncTaskInfo getTaskInfo(@RequestParam String taskId) { return asyncTaskManager.getTaskInfo(taskId); } }

创建Http文件,验证接口的可用性

###导入商品
POST http://localhost:9001/shuai/ecommerce-goods-service/async-goods/import-goods
Content-Type: application/json
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcInNodWFpemhhbGVcIn0iLCJqdGkiOiIzNjcxM2Y5NC03YjE2LTRkOTUtOTlkOS1lMzU0NWNhNGIwMGQiLCJleHAiOjE2NDc1MzI4MDB9.HuNiwCyukpsfRoWnvQMeYmOsBqOuCFZOFhd6p8M3dx-mc12uWT5Jv6LnyMN0hpewqHre5uST4gBumlNMmN53z0jARcSJPZvYYxnrL5XnIS5iTzAat_ZFwkc0T_t7aBjMBmjcAjbjBT-xaSB7jBKSKZIfCwBE5ZL4UYqsXF39BE6SwEgLgSpakCVPG_AFj8sCW2jIVjuM4uVOqa0LwOJtTIaOnsNu2VQjYdw3Lp08Dkg8O-DQ91h5IIg1M-OL8u2mxGcxam9zAKLTZH-m4_e9nRBHKIyM3GUiQTWMAnSa93q6201lvAxd1ItWGDDjHaP6L5AuwDVHDuQXOI_ArDq5KQ

[
{
  "goodsCategory":  "10001",
  "brandCategory":  "20001",
  "goodsName": "iphone 11",
  "goodsPic": "",
  "goodsDescription": "苹果手机",
  "price": 10000,
  "supply": 200000,
  "goodsProperty": {
    "size": "12cm * 6.5cm",
    "color": "绿色",
    "material": "金属机身",
    "pattern": "纯色"
  }
}
]

### 查询导入商品状态
GET http://127.0.0.1:9001/shuai/ecommerce-goods-service/async-goods/task-info?taskId=f5c1c6ff-4efb-45e5-a8c9-9f3d4515228a
Accept: application/json
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjAyYTRiZDcxLWUyMTgtNGZmYS1hYzQ3LWE5MGQxNWIzYmEwYSIsImV4cCI6MTYyNDgwOTYwMH0.UWbvqkIq5b5bb-WomLziZyCmjqCqsdeU1EZ0TfWrloRoY7WwqmYGDsf2GnE7JBgVLM0DibhSkkrkXu-wdjzWnqtxLkQ5UgON9BdPm1ZYLvllLcbAMv8KAdbXiC1_FiZ9q1tM6vGXlKU4-G1t88cUUP1_xXOGY9PvC5yGr31lQXCc0Nni4Ds4WwDPHvOq9YBVILdaWYeFsxIWi0pTGwcAxaCkp3BdsvPkJ3uXmrmzuLgkorkfITsmJqdaBuiSCD74LK0F-CvvCv09qizij627O3RuTrpbBfdFjDXT5xyRcKXxAR-n6oFGZdG-JUqh3iXWv_JdsyW-d8wPk3-DZ5zufA

商品普通接口的编写

/**
 * 

商品微服务相关服务功能实现

* */ @Slf4j @Service @Transactional(rollbackFor = Exception.class) public class GoodsServiceImpl implements IGoodsService { private final StringRedisTemplate redisTemplate; private final EcommerceGoodsDao ecommerceGoodsDao; public GoodsServiceImpl(StringRedisTemplate redisTemplate, EcommerceGoodsDao ecommerceGoodsDao) { this.redisTemplate = redisTemplate; this.ecommerceGoodsDao = ecommerceGoodsDao; } @Override public List getGoodsInfoByTableId(TableId tableId) { // 详细的商品信息, 不能从 redis cache 中去拿 List ids = tableId.getIds().stream() .map(TableId.Id::getId) .collect(Collectors.toList()); log.info("get goods info by ids: [{}]", JSON.toJSONString(ids)); List ecommerceGoods = IterableUtils.toList( ecommerceGoodsDao.findAllById(ids) ); return ecommerceGoods.stream() .map(EcommerceGoods::toGoodsInfo).collect(Collectors.toList()); } @Override public PageSimpleGoodsInfo getSimpleGoodsInfoByPage(int page) { // 分页不能从 redis cache 中去拿 if (page <= 1) { page = 1; // 默认是第一页 } // 这里分页的规则(你可以自由修改): 1页10调数据, 按照 id 倒序排列 Pageable pageable = PageRequest.of( page - 1, 10, Sort.by("id").descending() ); Page orderPage = ecommerceGoodsDao.findAll(pageable); // 是否还有更多页: 总页数是否大于当前给定的页 boolean hasMore = orderPage.getTotalPages() > page; return new PageSimpleGoodsInfo( orderPage.getContent().stream() .map(EcommerceGoods::toSimple).collect(Collectors.toList()), hasMore ); } @Override public List getSimpleGoodsInfoByTableId(TableId tableId) { // 获取商品的简单信息, 可以从 redis cache 中去拿, 拿不到需要从 DB 中获取并保存到 Redis 里面 // Redis 中的 KV 都是字符串类型 List goodIds = tableId.getIds().stream() .map(i -> i.getId().toString()).collect(Collectors.toList()); // FIXME 如果 cache 中查不到 goodsId 对应的数据, 返回的是 null, [null, null] List cachedSimpleGoodsInfos = redisTemplate.opsForHash() .multiGet(GoodsConstant.ECOMMERCE_GOODS_DICT_KEY, goodIds) .stream() .filter(Objects::nonNull) .collect(Collectors.toList()); // 如果从 Redis 中查到了商品信息, 分两种情况去操作 if (CollectionUtils.isNotEmpty(cachedSimpleGoodsInfos)) { // 1. 如果从缓存中查询出所有需要的 SimpleGoodsInfo if (cachedSimpleGoodsInfos.size() == goodIds.size()) { log.info("get simple goods info by ids (from cache): [{}]", JSON.toJSONString(goodIds)); return parseCachedGoodsInfo(cachedSimpleGoodsInfos); } else { // 2. 一半从数据表中获取 (right), 一半从 redis cache 中获取 (left) List left = parseCachedGoodsInfo(cachedSimpleGoodsInfos); // 取差集: 传递进来的参数 - 缓存中查到的 = 缓存中没有的 Collection subtractIds = CollectionUtils.subtract( goodIds.stream() .map(g -> Long.valueOf(g.toString())).collect(Collectors.toList()), left.stream() .map(SimpleGoodsInfo::getId).collect(Collectors.toList()) ); // 缓存中没有的, 查询数据表并缓存 List right = queryGoodsFromDBAndCacheToRedis( new TableId(subtractIds.stream().map(TableId.Id::new) .collect(Collectors.toList())) ); // 合并 left 和 right 并返回 log.info("get simple goods info by ids (from db and cache): [{}]", JSON.toJSONString(subtractIds)); return new ArrayList<>(CollectionUtils.union(left, right)); } } else { // 从 redis 里面什么都没有查到 return queryGoodsFromDBAndCacheToRedis(tableId); } } /** *

将缓存中的数据反序列化成 Java Pojo 对象

* */ private List parseCachedGoodsInfo(List cachedSimpleGoodsInfo) { return cachedSimpleGoodsInfo.stream() .map(s -> JSON.parseObject(s.toString(), SimpleGoodsInfo.class)) .collect(Collectors.toList()); } /** *

从数据表中查询数据, 并缓存到 Redis 中

* */ private List queryGoodsFromDBAndCacheToRedis(TableId tableId) { // 从数据表中查询数据并做转换 List ids = tableId.getIds().stream() .map(TableId.Id::getId).collect(Collectors.toList()); log.info("get simple goods info by ids (from db): [{}]", JSON.toJSONString(ids)); List ecommerceGoods = IterableUtils.toList( ecommerceGoodsDao.findAllById(ids) ); List result = ecommerceGoods.stream() .map(EcommerceGoods::toSimple).collect(Collectors.toList()); // 将结果缓存, 下一次可以直接从 redis cache 中查询 log.info("cache goods info: [{}]", JSON.toJSONString(ids)); Map id2JsonObject = new HashMap<>(result.size()); result.forEach(g -> id2JsonObject.put( g.getId().toString(), JSON.toJSONString(g) )); // 保存到 Redis 中 redisTemplate.opsForHash().putAll( GoodsConstant.ECOMMERCE_GOODS_DICT_KEY, id2JsonObject); return result; } @Override public Boolean deductGoodsInventory(List deductGoodsInventories) { // 检验下参数是否合法 deductGoodsInventories.forEach(d -> { if (d.getCount() <= 0) { throw new RuntimeException("purchase goods count need > 0"); } }); List ecommerceGoods = IterableUtils.toList( ecommerceGoodsDao.findAllById( deductGoodsInventories.stream() .map(DeductGoodsInventory::getGoodsId) .collect(Collectors.toList()) ) ); // 根据传递的 goodsIds 查询不到商品对象, 抛异常 if (CollectionUtils.isEmpty(ecommerceGoods)) { throw new RuntimeException("can not found any goods by request"); } // 查询出来的商品数量与传递的不一致, 抛异常 if (ecommerceGoods.size() != deductGoodsInventories.size()) { throw new RuntimeException("request is not valid"); } // goodsId -> DeductGoodsInventory Map goodsId2Inventory = deductGoodsInventories.stream().collect( Collectors.toMap(DeductGoodsInventory::getGoodsId, Function.identity()) ); // 检查是不是可以扣减库存, 再去扣减库存 ecommerceGoods.forEach(g -> { Long currentInventory = g.getInventory(); Integer needDeductInventory = goodsId2Inventory.get(g.getId()).getCount(); if (currentInventory < needDeductInventory) { log.error("goods inventory is not enough: [{}], [{}]", currentInventory, needDeductInventory); throw new RuntimeException("goods inventory is not enough: " + g.getId()); } // 扣减库存 g.setInventory(currentInventory - needDeductInventory); log.info("deduct goods inventory: [{}], [{}], [{}]", g.getId(), currentInventory, g.getInventory()); }); ecommerceGoodsDao.saveAll(ecommerceGoods); log.info("deduct goods inventory done"); return true; } }
/**
 * 

商品微服务对外暴露的功能服务 API 接口

* */ @Api(tags = "商品微服务功能接口") @Slf4j @RestController @RequestMapping("/goods") public class GoodsController { private final IGoodsService goodsService; public GoodsController(IGoodsService goodsService) { this.goodsService = goodsService; } @ApiOperation(value = "详细商品信息", notes = "根据 TableId 查询详细商品信息", httpMethod = "POST") @PostMapping("/goods-info") public List getGoodsInfoByTableId(@RequestBody TableId tableId) { return goodsService.getGoodsInfoByTableId(tableId); } @ApiOperation(value = "简单商品信息", notes = "获取分页的简单商品信息", httpMethod = "GET") @GetMapping("/page-simple-goods-info") public PageSimpleGoodsInfo getSimpleGoodsInfoByPage( @RequestParam(required = false, defaultValue = "1") int page) { return goodsService.getSimpleGoodsInfoByPage(page); } @ApiOperation(value = "简单商品信息", notes = "根据 TableId 查询简单商品信息", httpMethod = "POST") @PostMapping("/simple-goods-info") public List getSimpleGoodsInfoByTableId(@RequestBody TableId tableId) { return goodsService.getSimpleGoodsInfoByTableId(tableId); } @ApiOperation(value = "扣减商品库存", notes = "扣减商品库存", httpMethod = "PUT") @PutMapping("/deduct-goods-inventory") public Boolean deductGoodsInventory( @RequestBody List deductGoodsInventories) { return goodsService.deductGoodsInventory(deductGoodsInventories); } }
### 根据 TableId 查询详细商品信息
POST http://127.0.0.1:9001/shuai/ecommerce-goods-service/goods/goods-info
Content-Type: application/json
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjI3NGUzYzQ3LTRmNTQtNDdlYy05MGNhLTcxNzYyMjcyN2EzYyIsImV4cCI6MTYyNDk4MjQwMH0.TUy1C-9FkpyGkTxjyAKP9tX4mFzdZ22RWYvtKOOUUwjFefHSESamFWTJ2l0PcJJp07EIpzKgk9sNnVRZ5NmW6_Beo2AQgPOMWbYHiJg7eiR0bVC2CK6Tw8rUwgpkoWSXePgUM_3kntvXc19mgzO1NLVPNw5gahkBigzDffrXVUuXyc6kAf6L-y37hCytqfUwpgwQYm4Z2G7tUmF0_BsnQR4qHuWHrEdHm3_8Y8V38Ph_1VAlcJGvNXZS3bqtBxWHa2Wf7WksVA-H3dO_7xO7AlGJvUNOyiMGOjvMiwXc5mbqqqe6KXnvr9W1CvAPFmR-nlmc81wiCqW5Yfwo2Rh_5A

{
  "ids": [
    {
      "id": 1
    },
    {
      "id": 2
    }
  ]
}


### 根据分页查询简单商品信息
GET http://127.0.0.1:9001/shuai/ecommerce-goods-service/goods/page-simple-goods-info?page=2
Accept: application/json
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjI3NGUzYzQ3LTRmNTQtNDdlYy05MGNhLTcxNzYyMjcyN2EzYyIsImV4cCI6MTYyNDk4MjQwMH0.TUy1C-9FkpyGkTxjyAKP9tX4mFzdZ22RWYvtKOOUUwjFefHSESamFWTJ2l0PcJJp07EIpzKgk9sNnVRZ5NmW6_Beo2AQgPOMWbYHiJg7eiR0bVC2CK6Tw8rUwgpkoWSXePgUM_3kntvXc19mgzO1NLVPNw5gahkBigzDffrXVUuXyc6kAf6L-y37hCytqfUwpgwQYm4Z2G7tUmF0_BsnQR4qHuWHrEdHm3_8Y8V38Ph_1VAlcJGvNXZS3bqtBxWHa2Wf7WksVA-H3dO_7xO7AlGJvUNOyiMGOjvMiwXc5mbqqqe6KXnvr9W1CvAPFmR-nlmc81wiCqW5Yfwo2Rh_5A


### 根据 TableId 查询简单商品信息: 完整的 goods cache
### 第二步验证, 删掉 cache
### 第三步验证, 删除 cache 中其中一个商品
POST http://127.0.0.1:9001/shuai/ecommerce-goods-service/goods/simple-goods-info
Content-Type: application/json
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjI3NGUzYzQ3LTRmNTQtNDdlYy05MGNhLTcxNzYyMjcyN2EzYyIsImV4cCI6MTYyNDk4MjQwMH0.TUy1C-9FkpyGkTxjyAKP9tX4mFzdZ22RWYvtKOOUUwjFefHSESamFWTJ2l0PcJJp07EIpzKgk9sNnVRZ5NmW6_Beo2AQgPOMWbYHiJg7eiR0bVC2CK6Tw8rUwgpkoWSXePgUM_3kntvXc19mgzO1NLVPNw5gahkBigzDffrXVUuXyc6kAf6L-y37hCytqfUwpgwQYm4Z2G7tUmF0_BsnQR4qHuWHrEdHm3_8Y8V38Ph_1VAlcJGvNXZS3bqtBxWHa2Wf7WksVA-H3dO_7xO7AlGJvUNOyiMGOjvMiwXc5mbqqqe6KXnvr9W1CvAPFmR-nlmc81wiCqW5Yfwo2Rh_5A

{
  "ids": [
    {
      "id": 1
    },
    {
      "id": 2
    }
  ]
}


### 扣减商品库存
PUT http://127.0.0.1:9001/shuai/ecommerce-goods-service/goods/deduct-goods-inventory
Content-Type: application/json
e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjI3NGUzYzQ3LTRmNTQtNDdlYy05MGNhLTcxNzYyMjcyN2EzYyIsImV4cCI6MTYyNDk4MjQwMH0.TUy1C-9FkpyGkTxjyAKP9tX4mFzdZ22RWYvtKOOUUwjFefHSESamFWTJ2l0PcJJp07EIpzKgk9sNnVRZ5NmW6_Beo2AQgPOMWbYHiJg7eiR0bVC2CK6Tw8rUwgpkoWSXePgUM_3kntvXc19mgzO1NLVPNw5gahkBigzDffrXVUuXyc6kAf6L-y37hCytqfUwpgwQYm4Z2G7tUmF0_BsnQR4qHuWHrEdHm3_8Y8V38Ph_1VAlcJGvNXZS3bqtBxWHa2Wf7WksVA-H3dO_7xO7AlGJvUNOyiMGOjvMiwXc5mbqqqe6KXnvr9W1CvAPFmR-nlmc81wiCqW5Yfwo2Rh_5A

[
  {
    "goodsId": 1,
    "count": 100
  },
  {
    "goodsId": 2,
    "count": 34
  }
]

锐单商城拥有海量元器件数据手册IC替代型号,打造电子元器件IC百科大全!

相关文章