-
【分布式】-- 基于Nacos、OpenFeign搭建的微服务抽奖系统后台小案例
1.项目介绍
最近入项目之前要求熟悉一下SpringCloud Nacos微服务基于Feign接口调用并整合Swagger2进行接口文档展示给前端,所以自己按照要求来编写并整合了一套基于SpringCloudAlibaba Nacos、Feign、MyBatis、Swagger2的简单微服务抽奖系统,并结合数据库数据进行数据返回。
框架提供了基础的微服务注册与发现,接口Swagger访问、MyBatis注解实现接口Dao层数据访问,可用于快速搭建一个微服务CRUD基础框架。
抽奖接口主要包含:添加商品接口(包含商品名称和中奖率);抽奖接口,对添加的商品进行抽奖返回中奖商品的功能。
1.1.项目框架搭建
①项目主要结构说明:
- common-api模块:用于存放公共的pojo类、接口返回值枚举类、公共的抽奖函数
- consumer-product7800模块:服务消费方
- provider-product6700模块:服务提供方
②pom.xml依赖说明:
父工程主要pom依赖包:
其中主要的pom依赖有
spring-cloud-alibaba-dependencies
mybatis-spring-boot-starter
lombok
springfox-swagger2
swagger-bootstrap-ui
springcloudnacos-provider-product6700
springcloudnacos-consumer-product7800
③application.yml配置
重点主要是在服务生产方provider-product6700进行数据库连接、mybatis框架、nacos服务注册相关的配置,具体如下:
server: port: 6700 spring: application: name: nacos-product-provider-6700 cloud: nacos: discovery: server-addr: localhost:8848 #数据库连接池配置 datasource: username: root password: admin #假如时区报错,增加时区配置serverTimezone=UTC url: jdbc:mysql://localhost:3306/nacosproduct?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver #整合mybatis配置 mybatis: #config-location: classpath:mybatis/mybatis-config.xml 使用了configuration注解则无需再指定mybatis-config.xml文件 mapper-locations: classpath:mybatis/mapper/*.xml configuration: #指定mybatis全局配置文件中的相关配置项 map-underscore-to-camel-case: true type-aliases-package: com.fengye.springcloud.entities #消费者将要去访问的微服务名称 server-url: nacos-user-service: http://nacos-product-provider
1.2.项目分包结构说明
以一个服务提供方6700来说,就是简单地controller、mapper、service/impl,mapper层使用xml与注解结合的方式都可以。这里不再多说。
而在服务消费方7800来说,主要分为Feign接口调用与Swagger2Config配置类:
2.Swagger2/Feign接口/抽奖接口说明
2.1.Swagger2配置类
①这里主要的就是在项目中会引入Swagger2的依赖包,以及基于国人UI风格的jar包。
<!--引入Swagger2组件--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <!--swagger第三方ui依赖--> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>swagger-bootstrap-ui</artifactId> <version>1.9.6</version> </dependency>
②编写Swagger2Config配置类:
//将此类交给Spring管理,表示一个配置类 @Configuration //开启Swagger2 @EnableSwagger2 public class Swagger2Config { /** * 创建API应用 * apiInfo() 增加API相关信息 * 通过select()函数返回一个ApiSelectorBuilder实例,用来控制哪些接口暴露给Swagger来展现, * 本例采用指定扫描的包路径来定义指定要建立API的目录 * * @return 返回Swagger的Docket配置Bean实例 */ @Bean public Docket createRestApi(Environment environment) { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .enable(true) //enable是否启动swagger,如果为False,则swagger不能在浏览器中访问 .select() //指定API对象扫描哪个包下面的controller //参数any():扫描全部; none():都不扫描 //withClassAnnotation:扫描类上的注解,参数是一个注解的反射对象 //withMethodAnnotation:扫描方法上的注解 .apis(RequestHandlerSelectors.basePackage("com.fengye.springcloud")) //过滤什么路径 .paths(PathSelectors.any()) .build(); } /** * 创建该API的基本信息(这些基本信息会展现在文档页面中) * 访问地址:http://项目实际地址/swagger-ui.html * @return 返回API基本信息 */ private ApiInfo apiInfo() { return new ApiInfoBuilder() //Swagger2展示界面的标题(重要) .title("抽奖接口API文档") //描述信息(主要) .description("抽奖接口API文档") .version("1.0") //.termsOfServiceUrl("https://swagger.io/docs") //.license("Apache 2.0") //.licenseUrl("http://www.apache.org/licenses/LICENSE-2.0") //作者信息 .contact(new Contact("fengye", "https://www.cnblogs.com/yif0118/", "hyfmailsave@163.com")) .build(); } }
启动整体的项目之后,访问:http://localhost:7800/doc.html,即可看到具体的UI风格的API文档界面:
2.2.Feign接口请求
Feign的接口请求调用方式主要是基于服务提供方的Controller接口,并在服务消费方编写一个基于Controller接口一样的Service接口层,根据服务名及对应一致的方法名进行调用。
springcloudnacos-provider-product6700
服务提供方Controller接口:
springcloudnacos-consumer-product7800
服务消费方Servcie Feign接口:
暴露给Swagger2访问的Controller外部接口:
2.3.抽奖接口实现
主要用到的抽奖商品类:
@Data @AllArgsConstructor @NoArgsConstructor @ApiModel("用户实体类") public class Product { @ApiModelProperty("主键id") private Integer id; //主键id @ApiModelProperty("商品名称") private String productName; //商品名称 @ApiModelProperty("中奖率") private float winRate; //中奖率 -- 请用户输入小数点后两位 }
公共接口返回值封装类:
@Data @AllArgsConstructor @NoArgsConstructor public class CommonResult<T> { private Integer code; private T data; private String exception; private String msg; private boolean success; private String url; }
返回结果枚举类:
@Getter public enum ResultCodeEnum { /** * 返回结果枚举,每个枚举代表着一个状态 */ SUCCESS(200, "操作成功!"), ERROR(400, "操作失败!"), DATA_NOT_FOUND(401, "查询失败!"), PARAMS_NULL(402, "参数不能为空!"), PARAMS_ERROR(405, "参数不合法!"), NOT_LOGIN(403, "当前账号未登录!"); private Integer code; private String msg; ResultCodeEnum(Integer code, String msg) { this.code = code; this.msg = msg; } }
主要的抽奖接口实现工具类:
public class LotteryUtil { /** * 抽奖设计接口: * 产生一个随机数,0-5为一等奖商品,6-15为二等奖商品,16-40为三等奖商品,41-100为谢谢惠顾 * 在比较的时候,比较随机数(百分比)与获取商品的概率(百分比)的绝对值,40%以下的才中奖 * 之后计算随机数与中奖概率的绝对值,选择绝对值相差最小的那个为中奖商品 * @param products * @return */ public static Product luckyDraw(List<Product> products) { //1.产生一个随机数 int probabilityCount = 100; int randomNum = (int) (Math.random()* probabilityCount); //2.41-100表示不中奖 if(randomNum > 40){ return null; } Map<String, Product> map = new HashMap<>(); List<Integer> list = new ArrayList<>(); for (Product product : products) { int intValue = new BigDecimal(product.getWinRate() * 100).intValue(); int absVal = Math.abs(randomNum - intValue); list.add(absVal); } Integer min = Collections.min(list); for (Product product : products) { int value = new BigDecimal(product.getWinRate() * 100).intValue(); if(Math.abs(randomNum - value) == min){ return product; } } return null; } }
Nacos微服务注册中心:
使用Swagger接口测试返回中奖结果:
抽奖算法需求:抽奖接口按照添加商品接口的名称和中奖率进行抽奖返回中奖商品,中奖率小于等于40%。即有可能按实际概率来抽奖返回不中奖情况。
抽奖算法这里实际的情况应该是按照插入奖品的实际概率0.15来计算真实的抽奖概率,本人这里实现的比较简单,具体的抽奖概率可以
根据实际情况进行优化,也欢迎博友提出相对应的算法建议。
2.4.抽奖接口(离散算法实现)
最终晚上把抽奖接口的算法进行了实现,使用的是正态分布的离散算法,算法参考博文:抽奖概率--三种算法
下面是这种算法的具体Java代码实现:
/** * 抽奖接口二:离散算法,具有较好的正态分布随机性 * 竟然1-20都是靴子,21-45都是披风,那抽象成小于等于20的是靴子,大于20且小于等于45是披风, * 就变成几个点[20,45,55,60,100],然后也是从1到99随机取一个数R,按顺序在这些点进行比较, * 知道找到第一个比R大的数的下标,比一般算法减少占用空间, * 还可以采用二分法找出R,这样,预处理O(N),随机数生成O(logN),空间复杂度O(N) * @param products * @return */ public static Product discreteDraw(List<Product> products){ List<Integer> integers = products.stream() .map(product -> new BigDecimal(product.getWinRate() * 100).intValue()) .collect(Collectors.toList()); //1.划分区间,将概率划分为与概率值对应的几个点的概率区间 Integer[] arr = new Integer[integers.size()]; for (int i = 0; i < integers.size(); i++) { int sum = 0; for (int j = 0; j < i+1; j++) { sum += integers.get(j); } arr[i] = sum; } //2.最后arr就变成了0-100对应的商品的概率区间,如:[20,45,55,60,100] System.out.println("原抽奖商品的概率(%)为:" + integers); integers.forEach(System.out::println); //3.从1到99随机取一个数R,按照顺序在对这些点进行比较,找出第一个比R大的数的下标,这个下标对应就是数组中 //的抽到的商品的下标值 int probabilityCount = 100; //产生1-100的下标值 int randomNum = (int) (Math.random()* probabilityCount); //生成0-100的随机数 int maxIndex = getMaxIndex(arr, randomNum); //4.根据索引值去查询出中奖商品 Product target = maxIndex == -1 ? null : products.get(maxIndex); return target; } /** * * @param arr 传入的分区后的0-100区间的概率数组arr * @param randomNum 随机数 * @return 成功返回索引值,不成功返回-1 */ private static int getMaxIndex(Integer[] arr, int randomNum) { for (int index = 0; index < arr.length; index++) { if(arr[index] >= randomNum){ return index; } } return -1; }
另外在新增抽奖商品的接口上也增加了插入判断概率是否大于1的容错处理:
/** * 添加商品接口:使用Product参数进行JSON数据插入传递参数 * * @param product * @return */ @PostMapping(value = "/provider/insert") public Integer insertProduct(@RequestBody Product product) { int sum = productService.getProductList() .stream() .map(p -> new BigDecimal(p.getWinRate() * 100).intValue()).mapToInt(p -> p).sum(); int pVal = new BigDecimal(product.getWinRate() * 100).intValue(); int newRes = sum + pVal; //结果相加大于1,概率超过100%,返回-2,表示概率超过限制 if((pVal + newRes) > 100){ return -2; } return productService.insertProduct(product); }
这样接口的实现就完整了。
来源:https://www.cnblogs.com/yif0118/p/14827602.html