# MicroService **Repository Path**: pcpcode/MicroService ## Basic Information - **Project Name**: MicroService - **Description**: 记录微服务的集成过程 - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2019-12-29 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # [项目源码地址](https://gitee.com/pcpcode/SSM-Teamplate) # # 1.Git/TortoiseGit > 需要生成密钥使用,并在配置文件.gitconfig中配置: [credential] helper = store 这样拉取代码时就不必一直登录了 # 2.IDEA社区版 > 需要下载YAML,Spring Assistant两个插件,yml文件才有提示 > 然后配置好jdk8,maven # 3.Maven配置 > 指定本地仓库 D:\idea\MavenRepository > 指定远程仓库 alimaven central aliyun maven http://maven.aliyun.com/nexus/content/repositories/central/ repo1 central Human Readable Name for this Mirror. http://repo1.maven.org/maven2/ repo2 central Human Readable Name for this Mirror. http://repo2.maven.org/maven2/ # 4.Swagger2 项目连接 [FeignAndHystrix](https://gitee.com/pcpcode/SSM-Teamplate/tree/master/FeignAndHystrix)和[MybatisPlus](https://gitee.com/pcpcode/SSM-Teamplate/tree/master/MybatisPlus) > PS:当使用jar查看文档时,可使用如下命令开/关文档 io.springfox springfox-swagger2 2.9.2 io.springfox springfox-swagger-ui 2.9.2 org.springframework.restdocs spring-restdocs-mockmvc test io.springfox springfox-staticdocs 2.6.1 org.springframework.boot spring-boot-maven-plugin io.github.swagger2markup swagger2markup-maven-plugin 1.3.7 http://127.0.0.1:8082/v2/api-docs src/main/doc/apiall CONFLUENCE_MARKUP org.asciidoctor asciidoctor-maven-plugin 1.6.0 src/main/doc/apiall src/main/doc/html html coderay left jcenter-snapshots jcenter http://oss.jfrog.org/artifactory/oss-snapshot-local/ jcenter-releases jcenter http://jcenter.bintray.com false ## 4.2 Swagger的yml设置 # 上线需要关闭,设置为false swagger: enabled: true ## 4.3 config配置 ## import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration @EnableSwagger2 public class SwaggerConfig { //是否开启swagger,正式环境一般是需要关闭的,可根据springboot的多环境配置进行设置 @Value(value = "${swagger.enabled}") Boolean swaggerEnabled; @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) .enable(swaggerEnabled) // 是否开启 .apiInfo(apiInfo()) //去掉默认返回的状态码和message .useDefaultResponseMessages(false) .select() //扫描swagger2注解所在的包,通常是controller .apis(RequestHandlerSelectors.basePackage("com.pcp.controller")) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("互调与熔断Api") .contact(new Contact("文档作者:PCP", "", "1361815282@qq.com")) .version("1.0.0") .description("路由路径:http://127.0.0.1:8092/api/feignroute/feign") .build(); } } ## 4.4 注解说明 ## > paramType: >> header --> 请求参数的获取:@RequestHeader >> query --> 请求参数的获取:@RequestParam >> path(用于restful接口)--> 请求参数的获取:@PathVariable >> body(--> 请求参数的获取:@RequestBody >> form(不常用) > responseContainer(@ApiResponse里的属性): >> List >> Set >> Map @Api(tags = "互调与熔断使用例子API") @ApiOperation(value = "用户信息",notes = "根据姓名和年龄得到用户信息") @ApiImplicitParams({ @ApiImplicitParam(name = "name",value = "用户姓名",paramType = "query",dataType = "String"), @ApiImplicitParam(name = "age",value = "用户年龄",paramType = "query",dataType = "Integer"), }) @ApiResponses({ @ApiResponse(code = 2001,message = "失败",response = xxError.class) }) ## 4.5 额外说明 > 当获取不到GitHub内容时,可在当前项目的pom中设置如下远程仓库: jcenter-snapshots jcenter http://oss.jfrog.org/artifactory/oss-snapshot-local/ jcenter-releases jcenter http://jcenter.bintray.com false jcentral bintray http://jcenter.bintray.com false jcenter-snapshots jcenter http://oss.jfrog.org/artifactory/oss-snapshot-local/ # 5.Mybatis Plus 项目连接 [Mybatis Plus](https://gitee.com/pcpcode/SSM-Teamplate/tree/master/Mysql-MoreDataSource) > 安装MybatisX插件,作用: >> Java 与 XML 调回跳转 >> Mapper 方法自动生成 XML ## 5.1 mysql数据库与自动代码生成器依赖 > 自动代码生成器类:[CodeGenerator.java](https://gitee.com/pcpcode/MicroService/blob/master/CodeGenerator.java) org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-jdbc mysql mysql-connector-java 8.0.18 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test org.projectlombok lombok true com.baomidou mybatis-plus-boot-starter 3.3.0 com.baomidou mybatis-plus-generator 3.3.0 org.freemarker freemarker 2.3.29 io.springfox springfox-swagger2 2.9.2 io.springfox springfox-swagger-ui 2.9.2 p6spy p6spy 3.8.7 org.springframework.cloud spring-cloud-starter-openfeign org.springframework.cloud spring-cloud-starter-netflix-hystrix org.springframework.cloud spring-cloud-starter-netflix-eureka-client ## 5.2 mysql额外说明 ## ### 5.2.1 mysql8.0 ### > mysql8.0的驱动是:com.mysql.cj.jdbc.Driver而不是com.mysql.jdbc.Driver > 连接url时,需要加上:useSSL=false&serverTimezone=GMT%2B8&zeroDateTimeBehavior=convertToNull ### 5.2.2 连接数修改 ### 1.只在当前服务进程中有效,一旦MySQL重启,就会恢复到初始状态 > 查看MySQL最大连接数 >> show variables like '%max_connections%'; > > 修改最大连接数 >> set GLOBAL max_connections = 600; 2.永久有效的修改 > 打开MySQL配置文件/etc/my.cnf,添加配置项max_connections=600,保存。 > 这样,最大连接数就被设置成了600,重启MySQL后生效 ## 5.3 自动代码生成器需要注意的点 ## 说明 | 图片 :-: | :-: 执行main方法,按提示填写模块名和表 | ![ssm](https://gitee.com/pcpcode/MicroService/blob/master/img/ssm.jpg) 配置@MapperScan注解和mybatis.mapper-locations,两者缺一不可 | @MapperScan | ![mapperLocation](https://gitee.com/pcpcode/MicroService/blob/master/img/mapperLocation.jpg) mybatis.mapper-locations | ![mapperScan](https://gitee.com/pcpcode/MicroService/blob/master/img/mapperScan.jpg) mapper的xml放在resource下,官网说必须用classpath\*。需要注意的是,一定要写全包名,再写 \*.xml,否则可能读取不到xml | 结构间的依赖调用说明 | UserController 调用 UserServiceImpl 调用 baseMapper(里面封装了UserMapper及其xml) 其中IUserService不用管了,它只是起了封装作用。| ## 5.4 Hikari单数据源配置及简单调用使用 ## ### 5.4.1 yml配置 ### server: port: 8086 swagger: enabled: true mybatis-plus: mapper-locations: - classpath*:mapper/mine/*.xml spring: application: name: MybatisPlus datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mine?useUnicode=true&useSSL=false&characterEncoding=utf8&&serverTimezone=GMT%2B8&zeroDateTimeBehavior=convertToNull username: root password: 123456 type: com.zaxxer.hikari.HikariDataSource hikari: minimum-idle: 5 maximum-pool-size: 15 eureka: client: service-url: defaultZone: http://127.0.0.1:8090/eureka/ # 客户端注册地址 instance: # 设置微服务调用地址为IP优先(缺省为false) prefer-ip-address: true # 心跳时间,即服务续约间隔时间(缺省为30s) lease-renewal-interval-in-seconds: 30 # 发呆时间,即服务续约到期时间(缺省为90s) lease-expiration-duration-in-seconds: 90 feign: # feign默认的超时时间是1秒,重试1次.openfeign默认依赖了ribbon hystrix: # hystrix的超时时长,默认为1秒,太短了 enabled: true hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 15000 # 断路器的超时时间需要大于ribbon[ReadTimeout+ConnectionTimeout]*2的超时时间,不然重试没有意义 ribbon: ReadTimeout: 3000 # 处理请求的超时时间,默认为1秒 ConnectionTimeout: 3000 # 连接建立的超时时长,默认1秒 OkToRetryOnAllOperations: true # 是否对所有操作都重试,默认false MaxAutoRetriesNextServer: 0 # 重试负载均衡其他实例的最大重试次数,不包括首次调用,默认为0次 MaxAutoRetries: 1 # 同一台实例的最大重试次数,但是不包括首次调用,默认为1次 ### 5.4.2 简单调用使用 ### 说明 | 图片 | :---:|:---: controller | ![controller](https://gitee.com/pcpcode/MicroService/blob/master/img/controller.jpg) service | ![service](https://gitee.com/pcpcode/MicroService/blob/master/img/service.jpg) mapper | ![mapper](https://gitee.com/pcpcode/MicroService/blob/master/img/mapper.jpg) mappersql | ![mappersql](https://gitee.com/pcpcode/MicroService/blob/master/img/mappersql.jpg) ## 5.5 Hikari配置多数据源(不建议一个模块配多数据源,怕有事务或其他冲突。建议每个模块一个数据源,各个模块openfeign互相调用) [Mysql-MoreDataSource](https://gitee.com/pcpcode/SSM-Teamplate/tree/master/Mysql-MoreDataSource) > 实际开发中,我们需要用到多个数据库,因此很有必要引入多数据源配置 ### 5.5.1 多数据源的yml配置 ### server: port: 8084 # Mapper包扫描 #mybatis-plus: # mapper-locations: # - classpath*:mapper/mine/*.xml spring: datasource: # driver-class-name: com.mysql.cj.jdbc.Driver # 在Spring boot 2.+的版本中,不需要配置driverClassName,会根据url来检测加载哪个driverClassName # 据源one one: jdbc-url: jdbc:mysql://localhost:3306/mine?useUnicode=true&useSSL=false&characterEncoding=utf8&&serverTimezone=GMT%2B8&zeroDateTimeBehavior=convertToNull username: root password: 123456 # 设置最小和最大连接数 minimum-idle: 5 maximum-pool-size: 15 # 数据源two two: jdbc-url: jdbc:mysql://localhost:3306/pc?useUnicode=true&useSSL=false&characterEncoding=utf8&&serverTimezone=GMT%2B8&zeroDateTimeBehavior=convertToNull username: root password: 123456 # 设置最小和最大连接数 minimum-idle: 5 maximum-pool-size: 15 ### 5.5.2 配置各个数据源的config ### #### 5.5.2.1 one数据源(默认为主数据源)的config配置 #### /** * * @MapperScan 扫描mapper所在路径,也就是常说的DAO层 */ @Configuration @MapperScan(basePackages = "com.pcp.mine.mapper.one",sqlSessionFactoryRef = "oneSqlSessionFactory") public class OneDataSourceConfig { /** * @Bean 注册Bean对象 * @Primary 表示默认数据源 * @ConfigurationProperties 读取properties中的配置参数映射成为一个对象 */ @Bean(name = "oneDataSource") @Primary @ConfigurationProperties(prefix = "spring.datasource.one") public HikariDataSource getOneDateSource() { return new HikariDataSource(); } /** * @param datasource 数据源 * @return SqlSessionFactory * @Primary 默认SqlSessionFactory */ @Bean(name = "oneSqlSessionFactory") @Primary public SqlSessionFactory oneSqlSessionFactory(@Qualifier("oneDataSource") DataSource datasource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(datasource); //mybatis扫描xml所在位置 bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/mine/one/*.xml")); return bean.getObject(); } /** * SqlSessionTemplate是个线称安全的类,每运行一个SqlSessionTemplate时,它就会重新获取一个新的SqlSession */ @Bean("oneSessionTemplate") @Primary public SqlSessionTemplate oneSqlSessionTemplate(@Qualifier("oneSqlSessionFactory") SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } /** * 事务 */ @Bean @Primary public PlatformTransactionManager oneTransactionManager(@Qualifier("oneDataSource") DataSource datasource) { return new DataSourceTransactionManager(datasource); } } #### 5.5.2.2 two数据源(副数据源)的config配置 #### /** * * @MapperScan 扫描mapper所在路径,也就是常说的DAO层 */ @Configuration @MapperScan(basePackages = "com.pcp.mine.mapper.two",sqlSessionFactoryRef = "twoSqlSessionFactory") public class TwoDataSourceConfig { /** * @Bean 注册Bean对象 * @Primary 表示默认数据源 * @ConfigurationProperties 读取properties中的配置参数映射成为一个对象 */ @Bean(name = "twoDataSource") @ConfigurationProperties(prefix = "spring.datasource.two") public HikariDataSource getTwoDateSource() { return new HikariDataSource(); } /** * @param datasource 数据源 * @return SqlSessionFactory * @Primary 默认SqlSessionFactory */ @Bean(name = "twoSqlSessionFactory") public SqlSessionFactory twoSqlSessionFactory(@Qualifier("twoDataSource") DataSource datasource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(datasource); //mybatis扫描xml所在位置 bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/mine/two/*.xml")); return bean.getObject(); } /** * SqlSessionTemplate是个线称安全的类,每运行一个SqlSessionTemplate时,它就会重新获取一个新的SqlSession */ @Bean("twoSessionTemplate") public SqlSessionTemplate twoSqlSessionTemplate(@Qualifier("twoSqlSessionFactory") SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } /** * 事务 */ @Bean public PlatformTransactionManager twoTransactionManager(@Qualifier("twoDataSource") DataSource datasource) { return new DataSourceTransactionManager(datasource); } } ### 5.5.3 注意源码结构变化 ### > 1.需要注意的是,最好将不同Mapper接口(DAO层)和对应的Mapper.xml分别放在不同的文件夹,以免混淆,以此区分来维护代码。结构如下图所示: ![多数据源结构图](https://gitee.com/pcpcode/MicroService/blob/master/img/datasource.jpg) > 2.配置了多数据源后,只有默认的数据源才能使用Mybatis Plus的baseMapper,因此在xServiceImpl中,推荐使用注入xMapper方式调用sql,不推荐使用baseMapper.如图: ![注入xMapper](https://gitee.com/pcpcode/MicroService/blob/master/img/autoware.jpg) ### 5.5.4 Mapper CRUD基本使用 ### #### Select #### // 根据 ID 查询 T selectById(Serializable id); // 根据 entity 条件,查询一条记录 T selectOne(@Param(Constants.WRAPPER) Wrapper queryWrapper); // 查询(根据ID 批量查询) List selectBatchIds(@Param(Constants.COLLECTION) Collection idList); // 根据 entity 条件,查询全部记录 List selectList(@Param(Constants.WRAPPER) Wrapper queryWrapper); // 查询(根据 columnMap 条件) List selectByMap(@Param(Constants.COLUMN_MAP) Map columnMap); // 根据 Wrapper 条件,查询全部记录 List> selectMaps(@Param(Constants.WRAPPER) Wrapper queryWrapper); // 根据 Wrapper 条件,查询全部记录。注意: 只返回第一个字段的值 List selectObjs(@Param(Constants.WRAPPER) Wrapper queryWrapper); // 根据 entity 条件,查询全部记录(并翻页) IPage selectPage(IPage page, @Param(Constants.WRAPPER) Wrapper queryWrapper); // 根据 Wrapper 条件,查询全部记录(并翻页) IPage> selectMapsPage(IPage page, @Param(Constants.WRAPPER) Wrapper queryWrapper); // 根据 Wrapper 条件,查询总记录数 Integer selectCount(@Param(Constants.WRAPPER) Wrapper queryWrapper); #### Insert #### // 插入一条记录 int insert(T entity); #### Update #### // 根据 whereEntity 条件,更新记录 int update(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper updateWrapper); // 根据 ID 修改 int updateById(@Param(Constants.ENTITY) T entity); #### Delete #### // 根据 entity 条件,删除记录 int delete(@Param(Constants.WRAPPER) Wrapper wrapper); // 删除(根据ID 批量删除) int deleteBatchIds(@Param(Constants.COLLECTION) Collection idList); // 根据 ID 删除 int deleteById(Serializable id); // 根据 columnMap 条件,删除记录 int deleteByMap(@Param(Constants.COLUMN_MAP) Map columnMap); ### 5.5.5 注意事项 ### > 0.0 在一个模块多数据源的情况下,除了默认数据源,其他数据源的条件构造器无法使用,只能使用原生的sql。因此推荐所有的crud操作都写在Mapper.xml上,然后调用使用. > 0.1 如果非要使用条件构造器,那么可以将多数据源拆分为多模块,每个模块配置一个数据源,再使用openfeign互调方法。 > 1.如果entity是使用代码自动生成的,则已经对应数据库表了,如果其中的id名字不为默认的id,比如user_id,则需要加注解@TableId > 2.Mapper CRUD中涉及的参数有Map columnMap,其中String指的是表中的列名,而不是entity中的属性名。在表的列名与entity的属性名不同的情况下,尤其需要注意。 > 3.查询的条件构造器:QueryWrapper queryWrapper = new QueryWrapper<>(); > 4.更新的条件构造器:UpdateWrapper queryWrapper = new UpdateWrapper<>(); > 5.LambdaQueryWrapper lambdaQueryWrapper = Wrappers.lambdaQuery(); > 6.@Param主要是用来注解dao类中方法的参数,便于在对应的dao.xml文件中引用 ### 5.5.6 Mybatis Plus [项目地址](https://gitee.com/pcpcode/SSM-Teamplate/tree/master/MybatisPlus) ### #### 1.配置分页查询插件 #### > 只要把Page对象放在第一个参数上,第二个参数为Wrapper就好了,具体例子:[例子](https://gitee.com/pcpcode/SSM-Teamplate/blob/master/MybatisPlus/src/main/java/com/pcp/mine/service/impl/UserServiceImpl.java) import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.EnableTransactionManagement; //Spring boot方式 //jdk动态代理是接口的代理,cglib是类代理 @EnableTransactionManagement(proxyTargetClass = true) @Configuration @MapperScan("com.pcp.mine.mapper") public class MybatisPlusConfig { @Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false // paginationInterceptor.setOverflow(false); // 设置最大单页限制数量,默认 500 条,-1 不受限制 // paginationInterceptor.setLimit(500); return paginationInterceptor; } } #### 2.查询需要注意的点 #### /** * 分页查询涉及两条sql,取决与new Page()的第三个参数 * 第一条sql是查询总记录数 * 第二条sql是使用limit进行数据分页的查询 * 默认为true,查询上面的2条sql,若为false,则只查询第2条sql * @return */ public List selectUserPage(){ QueryWrapper userQueryWrapper = new QueryWrapper<>(); userQueryWrapper.ge("age",23); //1 表示当前页,2 表示每页多少条记录 Page page = new Page<>(1,2); //第一种分页查询方式:selectPage IPage iPage = userMapper.selectPage(page,userQueryWrapper); //第二种分页查询方式:selectMapsPage //Page> page2 = new Page<>(1,2); //IPage> e = userMapper.selectMapsPage(page2, userQueryWrapper); System.out.println("当前页:"+ iPage.getCurrent()); System.out.println("总页数:"+ iPage.getPages()); System.out.println("总记录数:"+ iPage.getTotal()); //遍历当前页的数据记录 List records = iPage.getRecords(); records.forEach(System.out::println); return iPage.getRecords(); } #### 3.单数据源配置 #### server: port: 8086 mybatis-plus: mapper-locations: - classpath*:mapper/mine/*.xml spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mine?useUnicode=true&useSSL=false&characterEncoding=utf8&&serverTimezone=GMT%2B8&zeroDateTimeBehavior=convertToNull username: root password: 123456 type: com.zaxxer.hikari.HikariDataSource hikari: minimum-idle: 5 maximum-pool-size: 15 # 6.redis缓存与Redission分布式锁 # [项目地址](https://gitee.com/pcpcode/SSM-Teamplate/tree/master/RedissionLockDemo) ## 6.1.redis安装与配置(这里以window版为例) ## > PS:windows版的redis是由微软官方维护的。[地址](https://github.com/microsoftarchive/redis) > 解压redis-5.0.7到某个文件夹,并安装Redis Desktop Manager图形化界面 > 介绍:为什么使用redis > 1.优点: >> 1.性能和并发:单线程;可以做缓存,不必一直请求数据库;内存操作,还可以持久化到磁盘,也就是dump.rdb(快照文件) >> 2.纯内存操作 >> 3.单线程操作,避免了频繁的上下文切换 >> 4.采用了非阻塞I/O多路复用机制[只有单个线程(一个快递员),通过跟踪每个I/O流的状态(每个快递的送达地点),来管理多个I/O流。] > 2.缺点: >> 1.缓存和数据库双写一致性问题 >>> 分析:一致性问题是分布式常见问题,还可以再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题。答这个问题,先明白一个前提。就是如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终一致性。另外,我们所做的方案其实从根本上来说,只能说降低不一致发生的概率,无法完全避免。因此,有强一致性要求的数据,不能放缓存。 >> 2.缓存雪崩问题【即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常。】 >>> 1.给缓存的失效时间,加上一个随机值,避免集体失效。 >>> 2.使用互斥锁,但是该方案吞吐量明显下降了。 >>> 3.我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B不设失效时间。自己做缓存预热操作 >>>> 1.从缓存A读数据库,有则直接返回 >>>> 2.A没有数据,直接从B读数据,直接返回,并且异步启动一个更新线程。 >>>> 3.更新线程同时更新缓存A和缓存B。 >> 3.缓存穿透问题【即黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而数据库连接异常】 >>> 1.用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试 >>> 2.采用异步更新策略,无论key是否取到值,都直接返回。value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。 >>> 3.提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的key。迅速判断出,请求所携带的Key是否合法有效。如果不合法,则直接返回。 >> 4.缓存的并发竞争问题 >>> 1.不要求顺序的话,使用分布式锁(Redission) >>> 2.如果要求顺序,比如期望按照key1的value值按照 valueA–>valueB–>valueC的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳。假设时间戳如下: >>>> 系统A key 1 {valueA 3:00} >>>> 系统B key 1 {valueB 3:05} >>>> 系统C key 1 {valueC 3:10} >>>> 那么,假设这会系统B先抢到锁,将key1设置为{valueB 3:05}。接下来系统A抢到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做set操作了。以此类推。 ### 6.1.1 配置最大内存和key过期策略 ### > 1.打开redis.conf,开头处有内存单元介绍: # Note on units: when memory size is needed, it is possible to specify # it in the usual form of 1k 5GB 4M and so forth: # # 1k => 1000 bytes # 1kb => 1024 bytes # 1m => 1000000 bytes # 1mb => 1024*1024 bytes # 1g => 1000000000 bytes # 1gb => 1024*1024*1024 bytes # # units are case insensitive so 1GB 1Gb 1gB are all the same. 在文件种查找:# maxmemory ,并新增 maxmemory 1g,也即最大内存为1g. 在文件种查找:# requirepass foobared,添加redis密码,requirepass 123456 > 2.redis的过期策略以及内存淘汰机制 >> redis采用的是定期删除+惰性删除策略 >>> 定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。 >>> 惰性删除,在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。 >>>> 缺点:如果定期删除没删除完过时的key。也没即时去请求key,也即惰性删除也没生效,redis的内存会越来越高。此时就应该采用内存淘汰机制。 > 3.设置内存淘汰机制 >> 在redis.conf中有一行配置,# maxmemory-policy volatile-lru >>> noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。应该没人用吧。 >>> allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。推荐使用,目前项目在用这种 >>> allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。应该也没人用吧,你不删最少使用Key,去随机删。 >>> volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。这种情况一般是把redis既当缓存,又做持久化存储的时候才用。不推荐 >>> volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。依然不推荐 >>> volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。不推荐 >>>> **ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。** ### 6.1.2 启动redis,并使用redis desktop manager连接 ### > 1.windows下redis启动:redis-server.exe redis.windows.conf,命令行登录:auth 密码 > 将redis加入到windows的服务中:redis-server --service-install redis.windows.conf --loglevel verbose >> 启动:redis-server --service-start >> 关闭:redis-server --service-stop >> 卸载:redis-server --service-uninstall > 还可以安装多个实例: >> redis-server --service-install –service-name redisService1 –port 10001 >> redis-server --service-install –service-name redisService1 –port 10002 >> redis-server --service-install –service-name redisService1 –port 10003 > 2.redis desktop manager连接启动后的redis >> 获取缓存:User user = (User) redisTemplate.opsForValue().get("age"); >> 设置缓存:redisTemplate.opsForValue().set("age",user,10, TimeUnit.SECONDS); >> 删除缓存:redisTemplate.delete(String.valueOf(oldAge)); ### 6.1.3 redis缓存的序列化和反序列化 ### > 默认情况下,RedisTemplate使用JDK序列化器,使得Redis中的key和value值不可读。 > 1.其中,key最常用的序列化为 StringRedisSerializer > 2.序列化的方式有这几种: >> 2.1 GenericToStringSerializer: 可以将任何对象泛化为字符串并序列化 >> 2.2 Jackson2JsonRedisSerializer: 跟JacksonJsonRedisSerializer实际上是一样的 >> 2.3 JacksonJsonRedisSerializer: 序列化object对象为json字符串 >> 2.4 JdkSerializationRedisSerializer: 序列化java对象 >> 2.5 StringRedisSerializer: 简单的字符串序列化 >>> 其中,jdk的序列化占用长度最小,而Jackson2Json效率最高。 >>>> 1.考虑效率和可读性,牺牲部分空间,key选StringRedisSerializer,value选Jackson2JsonRedisSerializer >>>> 2.如果空间比较敏感,效率要求不高,key选StringRedisSerializer,value选JdkSerializationRedisSerializer 实际上value序列号分为2种: 1.第一种是带类型的序列化,比如Jackson2JsonRedisSerializer,需要指定具体的类型或者JavaType,弊端就是我们的项目不止一个JavaBean,没办法通用。 2.第二种是不带类型的序列化,比如GenericJackson2JsonRedisSerializer,不需要指定具体的类型,实际上内部的操作是转换为了Object类型进行处理的,接受value的时候必须声明为Object类,否则会报错,因为它不能识别List/set/Long等带有泛型的数据。 **因此,这里我们选择key和value都指定为StringRedisSerializer。使用jackson将数据转化为json数据存到redis,取值时再使用jackson将json数据转为对应的类型数据。** **jackson常用的一些方法** import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ObjectMapperConfig { /** //将json字符串转换成对象 Map map = objectMapper.readValue(jsonString, Map.class); //转换对象类型 SomethingPOJO pojo = objectMapper.convertValue(map, SomethingPOJO.class); //将对象转换成json字符串 Sting string = objectMapper.writeValueAsString(pojo); //将json字符串转换成List JavaType javaType = mapper.getTypeFactory() .constructParametricType(List.class, Person.class); List jsonToPersonList = objectMapper.readValue(arrayToJson, javaType); //或者 final ObjectMapper mapper = new ObjectMapper(); jsonString=[{"id": "123","name": "李四",}, {"id": "6666","name": "李五",}]; List listll = mapper.readValue(jsonString, new TypeReference>(){}); */ @Bean public ObjectMapper ObjectMapper() { ObjectMapper objectMapper = new ObjectMapper(); // 忽略json字符串中不识别的属性 //objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 忽略无法转换的对象 “No serializer found for class com.xxx.xxx” //objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); return objectMapper; } } 解决缓存穿透:redis的布隆过滤器 # 7.Eureka和Feign和Hystrix熔断器和aop [项目地址](https://gitee.com/pcpcode/SSM-Teamplate/tree/master/FeignAndHystrix) # ## 7.1 Eureka ## > Eureka包含两个组件:Eureka Server和Eureka Client >> Eureka Server提供服务注册服务 >> Eureka Client是一个java客户端,用于简化与Eureka Server的交互 ### 7.2 Eureka Server ### org.springframework.cloud spring-cloud-starter-netflix-eureka-server 1.yml配置: server: port: 8092 eureka: server: enable-self-preservation: true # 是否开启自我保护,默认是true eviction-interval-timer-in-ms: 10000 # 续期时间,即扫描失效服务的间隔时间 client: register-with-eureka: false # 是否将自己注册到eureka中。本身就是Server,所以不用注册 fetch-registry: false # 是否需要从Eureka中获取信息。本身时Server,所以不需要 service-url: #因为这是一个单点的EurekaServer,不需要同步其它EurekaServer节点的数据,故设为false defaultZone: http://127.0.0.1:${server.port}/eureka/ 2.启动类配置: @SpringBootApplication @EnableEurekaServer public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class,args); } } 访问: http://127.0.0.1:${server.port}/eureka/ 或者 http://127.0.0.1:${server.port} ### 7.3 Feign需要和Eureka Client一起使用 ### > 需要注意的是,feign依赖eureka的注册服务来使用,而feign熔断器依赖hystrix来使用 org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-openfeign org.springframework.cloud spring-cloud-starter-netflix-hystrix > 0.PS:feign要在eureka的基础上使用,因为要用到eureka的name服务发现功能。 > 1.为什么需要Feign?因为服务间通常需要交互,而每个微服务的端口号又都不一致,因此需要Feign通过yml的spring.application.name进行调用 > 2.@FeignClient注解用于指定从哪个服务中调用功能,不能包含下划线 > 3.@PathVariable注解一定要指定参数名称,否则出错 0.yml配置: server: port: 8091 jwt: config: key: itpcp # token签发/解析密钥 time: 60000 # 单位毫秒。 # 上线需要关闭,设置为false swagger: enabled: true spring: application: name: FeignAndHystrix eureka: client: service-url: defaultZone: http://127.0.0.1:8090/eureka/ # 客户端注册地址 instance: # 设置微服务调用地址为IP优先(缺省为false) prefer-ip-address: true # 心跳时间,即服务续约间隔时间(缺省为30s) lease-renewal-interval-in-seconds: 30 # 发呆时间,即服务续约到期时间(缺省为90s) lease-expiration-duration-in-seconds: 90 feign: # feign默认的超时时间是1秒,重试1次.openfeign默认依赖了ribbon hystrix: # hystrix的超时时长,默认为1秒,太短了 enabled: true hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 15000 # 断路器的超时时间需要大于ribbon[ReadTimeout+ConnectionTimeout]*2的超时时间,不然重试没有意义 ribbon: ReadTimeout: 3000 # 处理请求的超时时间,默认为1秒 ConnectionTimeout: 3000 # 连接建立的超时时长,默认1秒 OkToRetryOnAllOperations: true # 是否对所有操作都重试,默认false MaxAutoRetriesNextServer: 0 # 重试负载均衡其他实例的最大重试次数,不包括首次调用,默认为0次 MaxAutoRetries: 1 # 同一台实例的最大重试次数,但是不包括首次调用,默认为1次 1.启动类配置如下: import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableEurekaClient @EnableFeignClients(basePackages = {"com.pcp.client"})//开启Feign服务 public class FeignHystrixApplication { public static void main(String[] args) { SpringApplication.run(FeignHystrixApplication.class,args); } } 2.client包下写接口(该接口必须与某个微服务模块的Controller方法一致,只是去掉了Controller方法里的方法体): import com.pcp.entity.User; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.GetMapping; import java.util.List; /** * 这里与MybatisPlus做交互演示 */ @FeignClient(name = "MybatisPlus",path = "/mine/user") public interface IFeignClient { @GetMapping("/all") public List getAllUsers(); } name与path共同组成了被调用模块的调用路径(@RequestMapping的映射路径)。 @FeignClient里的属性: name: 指定要调用的微服务的名字,用于服务发现,必填 value: 同name属性,alias for name url: url一般用于调试,可以手动指定调用的绝对地址 configuration: Feign配置类,可以自定义Feign的Encoder、Decoder、LogLevel、Contract decode404:当发生http 404错误时,如果该字段位true,会调用decoder进行解码,否则抛出FeignException,默认为false fallback: 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口 fallbackFactory: 工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码 path: 被调用模块的对应映射路径。(除了调用方法路径外的全部映射路径) 3.在模块的Controller中注入该feign接口,并调用接口方法(该接口方法会自动调用对应模块的方法): @RestController @RequestMapping("/feign") public class CustomController { @Autowired public IFeignClient iFeignClient; @GetMapping("/getMysql") public List getAllUsers(){ return iFeignClient.getAllUsers(); } @GetMapping("/hello") public String mysqlPlus(){ return "hello mysqlPlus"; } } ### 7.4 Feign里的Hystrix熔断器 ### > 基础服务的故障可能会导致级联故障,进而造成整个系统不可用的情况,这种现象被称为服务雪崩效应 > Hystrix 能使你的系统在出现依赖服务失效的时候,通过隔离系统所依赖的服务,防止服务级联失败,同时提供失败回退机制 1.yml配置: feign: # feign默认的超时时间是1秒,重试1次.openfeign默认依赖了ribbon hystrix: # hystrix的超时时长,默认为1秒,太短了 enabled: true hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 15000 # 断路器的超时时间需要大于ribbon[ReadTimeout+ConnectionTimeout]*2的超时时间,不然重试没有意义 ribbon: ReadTimeout: 3000 # 处理请求的超时时间,默认为1秒 ConnectionTimeout: 3000 # 连接建立的超时时长,默认1秒 OkToRetryOnAllOperations: true # 是否对所有操作都重试,默认false MaxAutoRetriesNextServer: 0 # 重试负载均衡其他实例的最大重试次数,不包括首次调用,默认为0次 MaxAutoRetries: 1 # 同一台实例的最大重试次数,但是不包括首次调用,默认为1次 2.client包下新建impl包,创建熔断实现类,该类实现被@FeignClient注解的接口类(IFeignClient): public class IFeignClientImpl implements IFeignClient { @Override public List getAllUsers() { return null; } } 3.修改xClient的@FeignClient注解,添加fallback : @FeignClient(name = "MybatisPlus",path = "/mine/user",fallback = IFeignClientImpl.class) public interface IFeignClient { @GetMapping("/all") public List getAllUsers(); } 不建议使用Feign里的Hystrix,功能太过鸡肋,因为它是针对某个方法的,而不是全局。 ### 7.5 spring-aop切面编程 ### > spring-aop已经在start-web中作为起步依赖引入了. > 新建一个aspect包,为该包下的所有类加入两个注解: >> @Aspect ,表示该类是切面类 >> @Component 这个注解一定要加,将该类的bean注入ioc容器以供使用,不然会报错。 import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; /** * 在对应的execution标注的类上加上以下注解 * @EnableAspectJAutoProxy */ @Aspect @Component public class AspectDemo { /** * execution(<修饰符>? <返回类型> <方法名>(<参数>) <异常>?) * <返回类型> <方法名>(<参数>) ----- 这些是必须的 * <修饰符> <异常> ------ 这些不是必须的 * <方法名>通常需要加包名限制 * * 例子: * 定义在service包和所有子包里的任意类的任意方法的执行:execution(* com.xyz.service..*.*(..)) * 第一个*表示匹配任意的方法返回值, ..(两个点)表示零个或多个; * 第一个..表示service包及其子包; * 第二个*表示所有类,;第三个*表示所有方法,第二个..表示方法的任意参数个数 * * @Before: 标识一个前置增强方法,相当于BeforeAdvice的功能. * @After: final增强,不管是抛出异常或者正常退出都会执行. * @AfterReturning: 后置增强,似于AfterReturningAdvice, 方法正常退出时执行. * @AfterThrowing: 异常抛出增强,相当于ThrowsAdvice. * @Around: 环绕增强,相当于MethodInterceptor. * execution:用于匹配方法执行的连接点; */ @Pointcut("execution(* com.pcp.controller.CustomController.*(..))") public void pointCut(){}; //@before代表在目标方法执行前切入, 并指定在哪个方法前切入 @Before("pointCut()") public void logStart(){ System.out.println("切面:Before"); } @After("pointCut()") public void logEnd(){ System.out.println("切面:After"); } @AfterReturning("pointCut()") public void logReturn(){ System.out.println("切面:AfterReturning"); } @AfterThrowing("pointCut()") public void logException(){ System.out.println("切面:AfterThrowing"); } @Around("pointCut()") public Object Around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{ System.out.println("@Arount:执行目标方法之前..."); Object obj = proceedingJoinPoint.proceed(); System.out.println("@Arount:执行目标方法之后..."); return obj; } 值得注意的是,@Around方法可用于Hystrix熔断器的统一异常处理。前提是返回类型全部都是Result(code,msg,data): @Around("pointCut()") public Result Around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{ System.out.println("@Arount:执行目标方法之前..."); //Object obj = proceedingJoinPoint.proceed(); System.out.println("@Arount:执行目标方法之后..."); return new Result(code,msg,data); } # 8.BaseExceptionHandler # > controller的统一异常处理类 > 将该类放在controller包下即可 import entity.Result; import entity.StatusCode; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * 统一异常处理类 */ @RestControllerAdvice public class BaseExceptionHandler { @ExceptionHandler(value = Exception.class) public Result error(Exception e){ e.printStackTrace(); return new Result(false, StatusCode.ERROR, "执行出错"+e.getMessage()); } } # 9.Java的JJWT实现JWT [项目地址](https://gitee.com/pcpcode/SSM-Teamplate/tree/master/FeignAndHystrix)# io.jsonwebtoken jjwt 0.9.1 1.编写工具类(JwtUtil.java)[https://gitee.com/pcpcode/SSM-Teamplate/tree/master/FeignAndHystrix/com/pcp/util/JwtUtil] > createJWT:Token创建 > parseJWT: Token解析 @ConfigurationProperties(prefix = "jwt.config") public class JwtUtil { /** * jwt加密解码的密钥 */ private String key ; /** * jwt的有效时间,单位毫秒 */ private long time ;//一个小时 public String getKey() { return key; } public void setKey(String key) { this.key = key; } public long getTime() { return time; } public void setTime(long time) { this.time = time; } /** * 生成JWT * * @param userStr 签发的用户的相关信息,应该具有唯一性,比如id。不强求设置 * @param subject 签发的用户,比如用户昵称。不强求设置 * @param role 自定义的私有声明,该用户的角色。 * 可以自定义很多私有的声明 * @return */ public String createJWT(String userStr, String subject, String role) { long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); JwtBuilder builder = Jwts.builder() .setId(userStr) .setSubject(subject) .setIssuedAt(now)//jwt的签发时间 .signWith(SignatureAlgorithm.HS256, key)//签发算法和密钥 .claim("role", role); if (time > 0) { builder.setExpiration( new Date( nowMillis + time)); } return builder.compact(); } /** * 解析JWT * @param jwtStr * @return */ public Claims parseJWT(String jwtStr){ return Jwts.parser() .setSigningKey(key)//解析需要的密钥key .parseClaimsJws(jwtStr)//解析自定义的私有声明claim .getBody(); } 2.yml自定义配置属性 jwt: config: key: itpcp # token签发/解析密钥 time: 3600000 # 单位毫秒。这里1小时 3.在Application类中注入JwtUtil的bean到IOC容器中。 @SpringBootApplication @EnableEurekaClient //开启Feign服务 @EnableFeignClients(basePackages = {"com.pcp.client"}) public class FeignHystrixApplication { public static void main(String[] args) { SpringApplication.run(FeignHystrixApplication.class,args); } @Bean public JwtUtil jwtUtil(){ return new JwtUtil(); } } 4.登录时签发token: > 在Service或者Controller类注入JwtUtil,并在登录方法里签发token: @Autowired public JwtUtil jwtUtil; @GetMapping("/login") public Map login(){ //假设user已经存在数据库中了 //模拟数据,得到用户的用户id,用户昵称.role是用户角色 //String token = jwtUtil.createJWT(user.getName(),user.getName(),"user"); String token = jwtUtil.createJWT("pcp","123456","user"); //如果前端需要直接定位角色,则可以这样写 Map tokenMap = new HashMap<>(); //签发的时候加上Bearer_等字母,避免别人获取token时直接用于恶意请求 tokenMap.put("token","Bearer_"+ token); tokenMap.put("role","user"); return tokenMap; } 5.客户端请求时服务端要解析Token: > 意味着我们需要拦截请求,并解析请求中携带的Token 1.添加jwt拦截器: > 新建拦截包interceptor,实现HandlerInterceptor接口: @Component public class JWTInterceptor implements HandlerInterceptor { @Autowired private JwtUtil jwtUtil; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { //System.out.println("jwt拦截器"); //无论如何都放行。具体能不能操作还是在具体的操作中去判断 //拦截器只是负责把请求头中包含的Token令牌进行一个解析验证 //拦截的请求中,不一定非得携带Token //1.得到携带Token的请求头 //String header = request.getHeader("Authorization"); String header = request.getHeader("token"); //2.判断是否存在该请求头,如果有就解析。没有就不理会。因为在所有的请求中,不一定都携带Token if (header !=null && !"".equals(header)){ if (header.startsWith("Bearer_")){ //3.得到Token String token = header.substring(7); //4.对令牌进行验证 try { Claims claims = jwtUtil.parseJWT(token); String role = (String) claims.get("role"); //这是管理员 if (claims != null && "admin".equals(role)) { request.setAttribute("token_admin",token); } //这是普通用户 if (claims != null && "user".equals(role)) { request.setAttribute("token_user",token); } } catch (Exception e) { //如果令牌不正确,说明是中间人瞎写的/伪造的/过期的 throw new RuntimeException("请重新登录"); } } } return true; } } 2.在Service/Controller中注入HttpServletRequest和JwtUtil,拿到Token并解析Token: @Autowired public JwtUtil jwtUtil; @Autowired public HttpServletRequest request; /** * 假如需要管理员权限才能删除 * @return */ @GetMapping("/delete") public String delete(){ String token_admin = (String) request.getAttribute("token_admin"); //token中没有管理员权限 if (token_admin == null || "".equals(token_admin)){ return "你没有管理员权限"; } //有管理员权限,执行 //doSomething(); return "你有管理员权限"; } # 8.定时任务调度 # > 引用起步依赖web的时候spring自带了任务调度计时器。 1.在启动类加注解:@EnableScheduling//自动任务调度 @SpringBootApplication //开启Feign服务 @EnableFeignClients(basePackages = {"com.pcp.client"}) @EnableScheduling//自动任务调度 public class FeignHystrixApplication { public static void main(String[] args) { SpringApplication.run(FeignHystrixApplication.class,args); } @Bean public JwtUtil jwtUtil(){ return new JwtUtil(); } } 2.新建一个task包,建一个xTask类: import com.pcp.controller.CustomController; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component //组件类,IOC容器扫描到后会自动实例化加载 public class TaskDemo { @Autowired public CustomController customController; //public void xxx(){} //* * * * * ? 每秒钟执行一次 //秒 分 小时 日 月 星期 //* 代表所有时间 //“/”——字符用来指定一个值的的增加幅度。比如在“秒”字段中设置为"0/15"表示"第0, 15, 30, 和 45秒"。而 "5/15"则表示"第5, 20, 35, 和 50"。 @Scheduled(cron = "0/5 * * * * ?") public void doSomething(){ System.out.println(customController.task()); } /** * @Scheduled注解可以控制方法定时执行,其中有三个参数可选择: * * 1、fixedDelay控制方法执行的间隔时间,是以上一次方法执行完开始算起,如上一次方法执行阻塞住了,那么直到上一次执行完,并间隔给定的时间后,执行下一次。 * * 2、fixedRate是按照一定的速率执行,是从上一次方法执行开始的时间算起,如果上一次方法阻塞住了,下一次也是不会执行,但是在阻塞这段时间内累计应该执行的次数,当不再阻塞时,一下子把这些全部执行掉,而后再按照固定速率继续执行。 * * 3、cron表达式可以定制化执行任务,但是执行的方式是与fixedDelay相近的,也是会按照上一次方法结束时间开始算起。 * * 4、initialDelay 。如: @Scheduled(initialDelay = 10000,fixedRate = 15000 * 这个定时器就是在上一个的基础上加了一个initialDelay = 10000 意思就是在容器启动后,延迟10秒后再执行一次定时器,以后每15秒再执行一次该定时器。 */ } 启动运行即可 # 9.Zuul路由/过滤/降级 [Zuul](https://gitee.com/pcpcode/SSM-Teamplate/blob/master/Zuul) > 依赖 org.springframework.cloud spring-cloud-starter-netflix-zuul org.springframework.cloud spring-cloud-starter-netflix-eureka-client io.jsonwebtoken jjwt 0.9.1 io.springfox springfox-swagger2 2.9.2 io.springfox springfox-swagger-ui 2.9.2 > yml配置 server: port: 8092 jwt: config: key: itpcp time: 10000 swagger: enabled: true spring: application: name: Zuul eureka: client: service-url: defaultZone: http://127.0.0.1:8090/eureka/ # 客户端注册地址 instance: # 设置微服务调用地址为IP优先(缺省为false) prefer-ip-address: true # 心跳时间,即服务续约间隔时间(缺省为30s) lease-renewal-interval-in-seconds: 30 # 发呆时间,即服务续约到期时间(缺省为90s) lease-expiration-duration-in-seconds: 90 zuul: # 当Spring Security在项目的classpath中,同时又需要使用下游微服务的Spring Security的Header时 ignore-security-headers: false # 过滤服务之间通信附带的headers ignored-headers: - Cookie - Set-Cookie - Authorization # 过滤转发的request的headers sensitive-headers: - Cookie - Set-Cookie - Authorization # 所有请求的前缀 prefix: /api # 表示所有的 Eureka 中的服务名称的信息访问都要忽略掉 ignored-services: "*" routes: # 代码里面出现的“mycompany”是一个逻辑名称,该名称的主要作用是将 path 与 serviceId 绑定在一起 # mycompany.path: /company-proxy/** # mycompany.serviceId: microcloud-provider-company MybatisPlus: /mysqlroute/** FeignAndHystrix: /feignroute/** ## 9.1 开启路由功能 ## @SpringBootApplication @EnableEurekaClient @EnableZuulProxy public class ZuulApplication { public static void main(String[] args) { SpringApplication.run(ZuulApplication.class,args); } @Bean public JwtUtil jwtUtil(){ return new JwtUtil(); } } ## 9.2 过滤访问 ## > responeBody可以设置为Result的json import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import com.pcp.zuul.util.JwtUtil; import io.jsonwebtoken.Claims; import org.springframework.beans.factory.annotation.Autowired; import javax.servlet.http.HttpServletRequest; /** * 请求拦截 */ public class AuthorizedRequestFilter extends ZuulFilter { @Autowired private JwtUtil jwtUtil; //pre表示请求执行之前,post表示请求执行之后 @Override public String filterType() { return "pre"; } //多个过滤器执行的优先级 //越小表示优先级越高 @Override public int filterOrder() { return 0; } //是否开启该过滤器 @Override public boolean shouldFilter() { return true; } //过滤器执行的操作,return 任意的Object值都表示继续执行 //setSendZuulResponse(false) 表示不再继续执行 @Override public Object run() throws ZuulException { System.out.println("后台网关过滤器启动了"); //得到request上下文 RequestContext requestContext = RequestContext.getCurrentContext(); //得到request的域 HttpServletRequest request = requestContext.getRequest(); //网关要分发请求,也即要对OPTIONS操作进行放行 if ("OPTIONS".equals(request.getMethod())){ return null; } //如果请求时登录地址,则放行 if (request.getRequestURI().indexOf("login") > 0){ return null; } //得到Token的头信息 String header = request.getHeader("token"); System.out.println("网关启动,token为:"+ header); //判断是否有头信息,有的话则让zuul网关转发 if (header != null && !"".equals(header)){ if (header.startsWith("bearer_")){ String token = header.substring(7); //对令牌进行验证 try { Claims claims = jwtUtil.parseJWT(token); String roles = (String) claims.get("role"); //这是管理员 if (claims != null && "admin".equals(roles)) { //将Token转发 requestContext.addZuulRequestHeader("token",token); return null; } //这是普通用户 if (claims != null && "user".equals(roles)) { //将Token分发 requestContext.addZuulRequestHeader("token",token); return null; } } catch (Exception e) { //如果令牌不正确,说明是 瞎写的/伪造的/过期的 //终止该请求 requestContext.setSendZuulResponse(false); requestContext.setResponseStatusCode(401); requestContext.setResponseBody("token失效,请重新登录"); requestContext.getResponse().setContentType("text/html;charset=utf-8"); } } } //如果不规定所有的请求都要求token的话,则可以注释掉下面4行代码 /** requestContext.setSendZuulResponse(false); requestContext.setResponseStatusCode(401); requestContext.setResponseBody("权限不足"); //设置编码 requestContext.getResponse().setContentType("text/html;charset=utf-8"); */ return null; } } > 配置程序类作为认证请求的配置 Bean /** * 认证请求的配置 Bean */ @Configuration public class ZuulConfig { @Bean public AuthorizedRequestFilter authorizedRequestFilter() { return new AuthorizedRequestFilter(); } } ### 9.3 zuul降级 ### > 当zuul代理的服务崩了的时候,为了日后好排查问题,所以需要zuul的降级处理 import com.fasterxml.jackson.databind.ObjectMapper; import com.pcp.zuul.dao.Result; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpResponse; import org.springframework.stereotype.Component; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; /** * zuul服务降级 */ @Component public class MybatisPlusProviderFallback implements FallbackProvider { @Autowired public ObjectMapper objectMapper; @Override public String getRoute() { //服务name,可以用* 或者 null 代表所有服务都过滤 return null; } @Override public ClientHttpResponse fallbackResponse(String route, Throwable cause) { return new ClientHttpResponse() { @Override public HttpStatus getStatusCode() throws IOException { return HttpStatus.OK; // 返回状态常量 } @Override public int getRawStatusCode() throws IOException { // 返回状态码 return HttpStatus.OK.value(); } @Override public String getStatusText() throws IOException { // 返回状态码对应的状态短语 return HttpStatus.OK.getReasonPhrase(); } @Override public void close() { } @Override public InputStream getBody() throws IOException { // 设置降级信息 Result result = new Result(501,"zuul服务降级","我是数据"); String json = objectMapper.writeValueAsString(result); return new ByteArrayInputStream(json.getBytes("UTF-8")); //返回前端的内容 } @Override public HttpHeaders getHeaders() { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.valueOf(MediaType.APPLICATION_JSON_VALUE)); //设置头 return httpHeaders; } }; } } ### 9.3.2 Zuul聚合Swagger2所有文档 ### 增加yml配置: swagger: enabled: true config配置: import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration @EnableSwagger2 public class SwaggerConfig { //是否开启swagger,正式环境一般是需要关闭的,可根据springboot的多环境配置进行设置 @Value(value = "${swagger.enabled}") Boolean swaggerEnabled; @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) .enable(swaggerEnabled) // 是否开启 .apiInfo(apiInfo()); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("全部微服务的文档集合") .contact(new Contact("pcp", "", "1361815282@qq.com")) .version("1.0.0") .description("文档接口查阅") .build(); } } SwaggerResourcesProvider配置: import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.netflix.zuul.filters.Route; import org.springframework.cloud.netflix.zuul.filters.RouteLocator; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; import springfox.documentation.swagger.web.SwaggerResource; import springfox.documentation.swagger.web.SwaggerResourcesProvider; import java.util.ArrayList; import java.util.List; @Component @Primary public class DocumentationConfig implements SwaggerResourcesProvider { @Autowired private RouteLocator routeLocator; @Override public List get() { List resources = new ArrayList<>(); resources.add(swaggerResource("default", "/v2/api-docs","1.0")); List routes= routeLocator.getRoutes(); //fullPath:/api/mysqlroute/**,routeId:mysqlroute //fullPath:/api/feignroute/**,routeId:feignroute //location为zuul前缀+微服务的路由路径 //name为oute.getId(),也即微服务的路由路径 routes.forEach(route->{ resources.add(swaggerResource(route.getId(), route.getFullPath().replace("**", "v2/api-docs"), "1.0")); System.out.println("routeId:"+route.getId()); System.out.println("fullPath:"+route.getFullPath()); }); //resources.add(swaggerResource("FeignAndHystrix", "/FeignAndHystrix/v2/api-docs", "1.0.0")); return resources; } private SwaggerResource swaggerResource(String name, String location, String version) { SwaggerResource swaggerResource = new SwaggerResource(); swaggerResource.setName(name); swaggerResource.setLocation(location); swaggerResource.setSwaggerVersion(version); return swaggerResource; } } # 10.gateway网关 # > zuul底层是servlet,Zuul处理的是http请求,没有提供异步支持 > gateway底层依然是servlet,但使用了webflux: >> 特性一 异步非阻塞 >> 特性二 响应式(reactive)函数编程 >> 不再拘束于Servlet容器,比如Tomcat, Jetty。现在还能运行IO的Netty和Undertow >> 内部实现了限流、负载均衡等,扩展性也更强 ## 10.1 gateway全局路由 ## org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-gateway org.springframework.retry spring-retry org.springframework.cloud spring-cloud-starter-netflix-hystrix io.jsonwebtoken jjwt 0.9.1 io.springfox springfox-swagger2 2.9.2 io.springfox springfox-swagger-ui 2.9.2 org.projectlombok lombok true application: @EnableEurekaClient @SpringBootApplication public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class,args); } @Bean public JwtUtil jwtUtil(){ return new JwtUtil(); } yml配置(重试/全局熔断): server: port: 8093 jwt: config: key: itpcp # token签发/解析密钥 time: 60000 # 单位毫秒。 eureka: client: service-url: defaultZone: http://127.0.0.1:8090/eureka/ # 客户端注册地址 instance: # 设置微服务调用地址为IP优先(缺省为false) prefer-ip-address: true # 心跳时间,即服务续约间隔时间(缺省为30s) lease-renewal-interval-in-seconds: 30 # 发呆时间,即服务续约到期时间(缺省为90s) lease-expiration-duration-in-seconds: 90 spring: application: name: Zuul cloud: gateway: discovery: locator: enabled: true # 为每个服务注册一个router #lower-case-service-id: true # 将请求路径上的服务名配置为小写。Eureka注册时全是大写的服务名 default-filters: # 默认的全局filter #- PrefixPath=/api # 全局路径前缀 - StripPrefix=2 # 访问后端服务过滤掉Path路径 必填否则找不到后端服务。也可以在服务加上统一路径 - name: Hystrix # 全局熔断器 args: name: authHystrixCommand fallbackUri: forward:/hystrixTimeout - name: Retry # 全局GEt重连 args: retries: 3 # 重试次数,不包含本次,默认3 status: 404 # 重试response code,默认无 statusSeries: 500 # 重试response code的系列 method: GET # 重试的请求,默认GET routes: - id: FeignAndHystrix #随便定义不重复就好 # lb://FeignAndHystrix uri是目标路径(分lb(注册中心名称)和ws(webservice路径)) uri: lb://FeignAndHystrix #服务名称 predicates: - Path=/api/feignroute/** #前端访问需加入例如 http:ip:port/api/feignroute - id: MybatisPlus # lb://MybatisPlus uri: lb://MybatisPlus predicates: - Path=/api/mysqlroute/** # filters: # 有bug,无法过滤 # - name: RequestRateLimiter # redis限流 , filter名称必须是RequestRateLimiter # args: # key-resolver: '#{@apiKeyResolver}' # 使用SpEL名称引用Bean,与上面新建的RateLimiterConfig类中的bean的name相同 # redis-rate-limiter-replenishRate: 20 # 每秒最大访问次数 # redis-rate-limiter-burstCapacity: 20 # 每秒令牌桶最大容量 hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 15000 # 断路器的超时时间需要大于ribbon[ReadTimeout+ConnectionTimeout]*2的超时时间,不然重试没有意义 ribbon: ReadTimeout: 3000 # 处理请求的超时时间,默认为1秒 ConnectionTimeout: 3000 # 连接建立的超时时长,默认1秒 OkToRetryOnAllOperations: true # 是否对所有操作都重试,默认false MaxAutoRetriesNextServer: 0 # 重试负载均衡其他实例的最大重试次数,不包括首次调用,默认为0次 MaxAutoRetries: 1 # 同一台实例的最大重试次数,但是不包括首次调用,默认为1次 ## 10.2 全局token认证 ## import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.pcp.gateway.entity.Result; import com.pcp.gateway.util.JwtUtil; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.nio.charset.StandardCharsets; import java.util.Arrays; /** * 全局token认证 */ @Component public class JwtCheckFilter implements GlobalFilter, Ordered { @Autowired public JwtUtil jwtUtil; @Autowired public ObjectMapper objectMapper; //过滤请求路径,直接放行 //swagger请求,登录请求 private String[] skipAuthUrls = {"v2/api-docs","login"}; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { System.out.println("后台网关过滤器启动了"); //得到请求的uri路径 String url = exchange.getRequest().getURI().getPath(); //得到request ServerHttpRequest request = exchange.getRequest(); //得到response ServerHttpResponse response = exchange.getResponse(); //得到token String token = request.getHeaders().getFirst("token"); System.out.println("网关启动,token为:"+ token); //跳过不需要验证的路径 if(Arrays.asList(skipAuthUrls).contains(url)){ return chain.filter(exchange); } //判断是否有头信息,有的话则让gateway网关转发 if (token != null && !"".equals(token)){ if (token.startsWith("bearer_")){ token = token.substring(7); //对令牌进行验证 try { Claims claims = jwtUtil.parseJWT(token); String roles = (String) claims.get("role"); //这是管理员 if (claims != null && "admin".equals(roles)) { //将Token转发 //request.getHeaders().add("token",token); return chain.filter(exchange); } //这是普通用户 if (claims != null && "user".equals(roles)) { //将Token分发 return chain.filter(exchange); } }catch (ExpiredJwtException e){ //如果令牌不正确,说明是 瞎写的/伪造的/过期的 return responseError(response,"token过期,请重新登录","引导用户重新登录"); //结束请求 //response.setComplete(); }catch (Exception e) { //认证失败 return responseError(response,"token认证失败,请重新登录","引导用户重新登录"); //结束请求 //response.setComplete(); } } } //并非所有的请求都携带token的 return chain.filter(exchange); } /** * 认证错误输出 * @param response 响应对象 * @param msg 错误信息 * @param msg 错误信息 * @return object 响应对象 */ private Mono responseError(ServerHttpResponse response,String msg,Object object) { //设置错误码 response.setStatusCode(HttpStatus.UNAUTHORIZED); //设置返回格式为json response.getHeaders().add("Content-Type","application/json;charset=UTF-8"); //响应数据 Result errorResult = new Result(401,msg,object); String errJson = ""; try { errJson = objectMapper.writeValueAsString(errorResult); } catch (JsonProcessingException e) { e.printStackTrace(); Result jsonParseErrorResult = new Result(401,"token过期","请重新登录"); errJson = objectMapper.writeValueAsString(jsonParseErrorResult); }finally { DataBuffer buffer = response.bufferFactory().wrap(errJson.getBytes(StandardCharsets.UTF_8)); return response.writeWith(Flux.just(buffer)); } } @Override public int getOrder() { return 0; } } ## 10.3 全局网关熔断器 ## @RestController public class GatewayHystrixController { //降级处理 @GetMapping("/hystrixTimeout") public Result hystrixTimeout(){ System.out.println("某个微服务触发了断路由"); return new Result(401,"gateway服务降级","请重启服务"); } //触发断路由 @HystrixCommand(commandKey = "authHystrixCommand") public void authHystrixCommand(){ //这里适合埋点日志 System.out.println("触发断路由"); } } # 10.4 限流 # org.springframework.boot spring-boot-starter-data-redis-reactive > default-filters配置全局限流: # filters: # 有bug,无法过滤 # - name: RequestRateLimiter # redis限流 , filter名称必须是RequestRateLimiter # args: # key-resolver: '#{@apiKeyResolver}' # 使用SpEL名称引用Bean,与上面新建的RateLimiterConfig类中的bean的name相同 # redis-rate-limiter-replenishRate: 20 # 每秒最大访问次数 # redis-rate-limiter-burstCapacity: 20 # 每秒令牌桶最大容量 config配置: > 目前在yml配置后在idea某些版本有bug import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import reactor.core.publisher.Mono; /** * 配置限流key */ @Configuration public class GatewayRateConfig { /** * 接口限流操作 * * @return */ @Bean(name = "apiKeyResolver") public KeyResolver apiKeyResolver() { //根据api接口来限流 return exchange -> Mono.just(exchange.getRequest().getPath().value()); } /** * ip限流操作 * * @return */ /* @Bean(name = "ipKeyResolver") public KeyResolver ipKeyResolver() { return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName()); }*/ /** * 用户限流 * 使用这种方式限流,请求路径中必须携带userId参数。 * @return */ // @Bean // KeyResolver userKeyResolver() { // return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId")); // } } # 10.5 gateway集成swagger2 # io.springfox springfox-swagger2 2.9.2 io.springfox springfox-swagger-ui 2.9.2 因为Swagger暂不支持webflux项目,所以Gateway里不能配置SwaggerConfig import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.config.GatewayProperties; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.support.NameUtils; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; import springfox.documentation.swagger.web.SwaggerResource; import springfox.documentation.swagger.web.SwaggerResourcesProvider; import java.util.ArrayList; import java.util.List; @Component @Primary public class DocumentationConfig implements SwaggerResourcesProvider { public static final String API_URI = "/v2/api-docs"; @Autowired private RouteLocator routeLocator; @Autowired private GatewayProperties gatewayProperties; @Override public List get() { List resources = new ArrayList<>(); List routes = new ArrayList<>(); //取出gateway的route routeLocator.getRoutes().subscribe(route -> routes.add(route.getId())); //结合配置的route-路径(Path),和route过滤,只获取有效的route节点 gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId())) .forEach(routeDefinition -> routeDefinition.getPredicates().stream() .filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName())) .forEach(predicateDefinition -> resources.add(swaggerResource(routeDefinition.getId(), predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0") .replace("/**", API_URI))))); return resources; } private SwaggerResource swaggerResource(String name, String location) { SwaggerResource swaggerResource = new SwaggerResource(); swaggerResource.setName(name); swaggerResource.setLocation(location); swaggerResource.setSwaggerVersion("2.0"); return swaggerResource; } } Gateway里没有配置SwaggerConfig,而运行Swagger-ui又需要依赖一些接口,所以我的想法是自己建立相应的swagger-resource端点 import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; import springfox.documentation.swagger.web.*; import java.util.Optional; @RestController @RequestMapping("/swagger-resources") public class SwaggerHandler { @Autowired(required = false) private SecurityConfiguration securityConfiguration; @Autowired(required = false) private UiConfiguration uiConfiguration; @Autowired private final SwaggerResourcesProvider swaggerResources; @Autowired public SwaggerHandler(SwaggerResourcesProvider swaggerResources) { this.swaggerResources = swaggerResources; } @GetMapping("/configuration/security") public Mono> securityConfiguration() { return Mono.just(new ResponseEntity<>( Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK)); } @GetMapping("/configuration/ui") public Mono> uiConfiguration() { return Mono.just(new ResponseEntity<>( Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK)); } @GetMapping("") public Mono swaggerResources() { return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK))); } } > 值得注意的是,在gateway的全局filter中,看情况放行某些请求路径: //过滤请求路径,直接放行 //swagger请求,登录请求 private String[] skipAuthUrls = {"v2/api-docs","login"}; //跳过不需要验证的路径 if(Arrays.asList(skipAuthUrls).contains(url)){ return chain.filter(exchange); }