这里实现按钮级别的权限判断的逻辑:每个按钮对应一个权限标识
,后台根据用户角色计算出当前用户可访问的权限标识
列表,前端登录后得到权限标识
列表存入全局,通过单个按钮的权限标识
去匹配列表里的。来实现按钮级别的权限判断。
1 | -- 系统权限表 |
登录后通过接口直接返回用户可访问指令级权限列表:
vue-element-admin模板已经封装了一个通过角色来判断的指令权限:v-permission。
这里需要修改其逻辑:
state
中添加permissions
列表1 | // ---------------- state 中添加 permissions |
v-permission
指令逻辑1 | /** |
checkPermission
权限判断函数逻辑1 | /** |
使用v-permission
指令
1 | <template> |
使用checkPermission
函数
1 | <template> |
菜单中添加按钮的权限演示
菜单/按钮对角色授权演示
完整代码请查看源码:
源码地址:https://github.com/chaooo/spring-security-jwt.git,
这里我将本文的前后端分离后台菜单权限控制放在github源码tag的V5.0中,防止后续修改后代码对不上。
1 | -- 系统用户表 |
1 | # 克隆项目 |
vue-element-admin中权限的实现方式是:通过获取当前用户的权限去比对路由表,生成当前用户具有的权限可访问的路由表,通过router.addRoutes
动态挂载到router
上。
这里改造得更灵活一点,后台根据用户计算出可访问得菜单列表,直接返回用户可访问得菜单列表,前端也需要保存一份全的路由表,用户登录后得到可访问菜单,匹配前端保存的路由表然后动态挂载。
用户登录成功之后,在全局钩子router.beforeEach
中拦截路由,判断是否已获得token
,在获得token
之后我们就要去获取用户的基本信息及可访问菜单,然后动态挂载路由。
1 | /** |
menus
动态挂载路由接口返回菜单数据:
动态挂载路由:
1 | /** |
1 | /** |
]]>源码地址:https://github.com/chaooo/spring-security-jwt.git,
这里我将本文的前后端分离后台菜单权限控制放在github源码tag的V4.0中,防止后续修改后代码对不上。
RBAC(Role-based access control)是一种以角色为基础的访问控制(Role-based access control,RBAC),它是一种较新且广为使用的权限控制机制,这种机制不是直接给用户赋予权限,而是将权限赋予角色。
RBAC 权限模型将用户按角色进行归类,通过用户的角色来确定用户对某项资源是否具备操作权限。RBAC 简化了用户与权限的管理,它将用户与角色关联、角色与权限关联、权限与资源关联,这种模式使得用户的授权管理变得非常简单和易于维护。
1 | -- 用户表 |
1 | public ResponseJson<SysUser> register(SysUser sysUser) { |
1 |
|
1 |
|
1 | public ResponseJson<List<SysMenu>> menuList(String username) { |
1 | <select id="getRoleIdsByUserId" resultType="java.lang.Long"> |
]]>源码地址:https://github.com/chaooo/spring-security-jwt.git,
这里我将本文的前后端分离后台菜单权限控制放在github源码tag的V3.0中,防止后续修改后代码对不上。
在 SpringSecurity
整合 JWT
实现无状态登录示例中,我们在 JwtAuthenticationFilter
(自定义JWT
认证过滤器) 解析 Token
成功后,提供了续签逻辑:
1 | /** |
这里的逻辑是:Token
未过期并且当前时间已经超过 Token
有效时间的一半,重新生成一个 refreshToken
,并返回给前端,前端需要用 refreshToken
替换之前旧的 Token
。
预期效果:前端不需要手动替换 Token
,每次用 Token
请求资源时自动续期。
实现方案:引入 Redis
,实现逻辑:
Token
存储到 Redis
里面(k,v都为 Token
的值),并设置 Redis
过期时间为: Token
过期时间。Token
的键去换取 Redis
的值,这里命名为 cacheToken
:cacheToken
在有效期内,重设 Redis
过期时间为:当前时间 + (cacheToken
过期时间 - cacheToken
签发时间)。cacheToken
已过期(Redis
在有效期内),则 JWT
重新生成 Token
并覆盖v值(这时候k、v值不一样了),然后设置 Redis
过期时间为: cacheToken
过期时间。Redis
也过期,取不到 cacheToken
,则拒绝访问或返回错误信息,需要重新登录。pom.xml
中引入 Redis
依赖:1 | <dependency> |
application.yml
配置文件中配置 Redis
:1 | spring: |
RedisService
封装1 |
|
JwtLoginFilter
,构造方法中加入 RedisService
,并生成 Token
后存入 Redis
:1 |
|
JwtAuthenticationFilter
,构造方法中加入 RedisService
,并添加 Token
续签逻辑:1 |
|
SpringSecurity
配置类,注入 RedisService
:1 |
|
]]>源码地址:https://github.com/chaooo/spring-security-jwt.git,
这里我将本文的基于Redis的Token自动续签优化放在github源码tag的V2.0中,防止后续修改后代码对不上。
pom.xml
中引入依赖:1 | <dependency> |
application.yml
配置文件中配置:1 | server: |
1 | /** |
BCryptPasswordEncoder
解析器注入到容器
1 |
|
实现 UserDetailsService
接口,自定义逻辑
1 |
|
1 |
|
实现 GrantedAuthority
存储权限和角色
1 | /** |
重写了其中的3个方法
attemptAuthentication
:接收并解析用户凭证。successfulAuthentication
:用户成功登录后被调用,我们在这个方法里生成token。unsuccessfulAuthentication
:认证失败后被调用1 |
|
自定义加密的签名key
1 | /** |
自定义全局API返回JSON数据对象
1 |
|
从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。如果校验通过,就认为这是一个取得授权的合法请求。
1 |
|
AuthenticationEntryPoint
自定义认证拦截器1 |
|
sql
1 | DROP TABLE IF EXISTS `sys_user`; |
Java代码
1 | /** |
1 | <insert id="insertSysUser" keyProperty="id" keyColumn="id" useGeneratedKeys="true"> |
注册一个用户:
登录,返回Token:
访问公开接口:
访问需要认证的接口,无权限返回403:
访问需要认证的接口,通过有效Token访问:
]]>源码地址:https://github.com/chaooo/spring-security-jwt.git,
这里我将本文的登录认证逻辑放在github源码tag的V1.0中,防止后续修改后对不上。
Spring Security
设计的 Servlet
安全从架构上分为三个层次,分别是「认证」、「鉴权」、「入侵防护」。通过过滤器机制将安全逻辑应用到 Servlet
项目。
请求的接收和处理是通过一个一个的过滤器顺序执行实现的,过滤器是 Servlet
项目处理请求的基础。
Spring
将自己体系内的过滤器交由「过滤器代理FilterChainProxy
」管理,FilterChainProxy
同样也是一个过滤器,被封装在 Spring
的「过滤器委托代理DelegatingFilterProxy
」中。Spring Security
在 FilterChainProxy
中加入了「安全过滤器链SecurityFilterChain
」实现安全保护功能。
其过程如图:
安全过滤器链(SecurityFilterChain
)的特点:
Spring Security
支持的 Servlet
指明了起点;Servlet
容器中,过滤器的选择是由 URL
决定的,如此便可针对不同 URL
指定相互独立的安全策略。Spring Security
内置了 33
种安全过滤器,每个过滤器有固定的顺序及应用场景;内置过滤器的参数设置通过 HttpSecurity
类相应的配置方法完成。
在认证与授权中关键的三个过滤器:
UsernamePasswordAuthenticationFilter
:该过滤器用于拦截我们表单提交的请求(默认为/login),进行用户的认证过程。FilterSecurityInterceptor
:该过滤器主要用来进行授权判断。ExceptionTranslationFilter
:该过滤器主要用来捕获处理spring security
抛出的异常,异常主要来源于FilterSecurityInterceptor
。Spring Security
的认证、授权异常在过滤器校验过程中产生,并在 ExceptionTranslationFilter
中接收并进行处理,
ExceptionTranslationFilter
过滤器首先像其他过滤器一样,调用过滤器链的执行方法 FilterChain.doFilter(request, response)
启动过滤处理;SecurityContextHolder
对象;request
」保存到 RequestCache
对象中;AuthenticationEntryPoint
对象存储的认证地址,向客户端索要身份证明。例如,使用浏览器登录的用户,将浏览器地址重定向到 /login 或者回传一个 WWW-Authenticate 认证请求头。AccessDeniedException
异常,然后访问被拒绝。继续执行拒绝处理 AccessDeniedHandler
。在 HttpSecurity
对象中增加自定义 Filter
可用于实现认证方式的扩展等场景,扩展 Filter
需要实现 javax.servlet.Filter
接口;并且需要指定新过滤器的位置。
例如,扩展自定义接口 SimpleFilter。
1 | public class SimpleFilter implements Filter { |
1 | http.addFilterBefore(new SimpleFilter(), UsernamePasswordAuthenticationFilter.class); |
组别 | 组件名 | 简述 |
---|---|---|
存储单元 | Authentication | 维护用户用于认证的信息 |
GrantedAuthority | 认证用户的权限信息比如角色、范围等等 | |
SecurityContextHolder | 用于维护 SpringContext | |
SecurityContext | 用来存储当前认证用户的信息 | |
认证管理 | AuthenticationManager | SpringSecurity 向外提供的用于认证的 API 集合 |
ProviderManager | AuthenticationManager 的常见实现类 | |
AuthenticationProvider | 用于 ProviderManager 提供认证实现 | |
AuthenticationEntryPoint | 用于获取用户认证信息 | |
流程管理 | AbstractAuthenticationProcessingFilter | 是认证过滤器的基础,用于组合认证流程 |
SecurityContextHolder
对象是整个 Spring Security
体系的核心,它维护着 SecurityContext
对象。它是唯一的。SecurityContext
对象用于衔接 SecurityContextHolder
和 Authentication
对象,是对 Authentication
的外层封装。Authentication
是用户的认证信息。Authentication
对象有三个核心属性:
GrantedAuthority
实现。GrantedAuthority
是在前述 Authentication
对象中所指的权限信息。在开发过程中,可以通过 Authentication.getAuthorities()
方法获取。权限信息通常包括角色、范围,或者其他扩展内容。Authentication
两个主要作用:
AuthenticationManager
对象提供用于认证的信息载体;AuthenticationManager
为 Spring
过滤器提供认证支持 API
。AuthenticationManager
的实现形式并没有严格限制,通常情况下使用 ProviderManager
。ProviderManager
是 AuthenticationManager
的最常用的实现类,它包含了一系列的 AuthenticationProvider
对象,用以判断认证流程是否完成、认证结构是否成功。AuthenticationProvider
:每个 ProviderManager
可以包含多个 AuthenticationProvider
,每个 AuthenticationProvider
提供一种认证类型,例如:DaoAuthenticationProvider
可以完成「用户名 / 密码」的认证,JwtAuthenticationProvider
用于完成 JWT 方式的认证。AuthenticationEntryPoint
在当一个请求包含的认证信息不全时,比如未认证终端访问受保护资源时发挥作用,如跳转到登录页面、返回认证要求等。AbstractAuthenticationProcessingFilter
是所有认证过滤器的基类。
Spring Security 包含确认身份和确认身份的可执行操作两部分,前者为认证(Authentication
),后者即为鉴权(Authorization
);
Spring Security
的权限默认是以字符串形式存储的权限信息,比如角色名称、功能名称等;
在用户身份信息得到确认后,Authentication
中会存储一系列的 GrantedAuthority
对象,这些对象用来判断用户可以使用哪些资源。
GrantedAuthority
对象通过 AuthenticationManager
插入到 Authentication
对象中,并被 AccessDecisionManager
使用,判断其权限。
GrantedAuthority
是一个接口,其仅包含一个 getAuthority()
方法,返回一个字符串值,该值作为权限的描述,当权限较为复杂,该方法需要返回 null,此时 AccessDecisionManager
会根据 getAuthority()
返回值情况判断是否要进行特殊处理。
SimpleGrantedAuthority
是 GrantedAuthority
的一个基础实现类,可以满足一般的业务需求。
AccessDecisionManager
对象判断其是否允许继续执行; 权限判断发生在方法被调用前,或者 WEB 请求之前。不满足抛出 AccessDeniedException
异常。AfterInvocationManager
进行管理;后置鉴权在资源被访问后,根据权限的判定来修改返回的内容,或者返回 AccessDeniedException
。前置鉴权 AccessDecisionManager
对象由 AbstractSecurityInterceptor
发起调用,其职责是给出资源是否能被访问的最终结果;
AccessDecisionManager
包含三个主要方法:boolean supports(ConfigAttribute attribute);
:判断配置属性是否可被访问;boolean supports(Class clazz);
:判断安全对象的类型是否支持被访问;void decide(Authentication authentication, Object secureObject,Collection<ConfigAttribute> attrs) throws AccessDeniedException;
:通过认证信息、安全对象、权限信息综合判断安全对象是否允许被访问。Spring Security
内置了以「投票」为判定方法的鉴权策略。Spring Security
的鉴权策略可以由用户自己实现。
投票策略下,AccessDecisionManager
控制着一系列的 AccessDecisionVoter
实例,判断权限是否满足,如果不满足抛出 AccessDeniedException
异常。
AccessDecisionVoter
也包含三个方法:boolean supports(ConfigAttribute attribute);
:判断配置属性是否支持;boolean supports(Class clazz);
:判断类型是否支持;int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attrs);
:根据认证信息对安全资源进行投票。投票鉴权分为三类:
RoleVoter
;AuthenticatedVoter
,主要区分认证用户、匿名用户等;Servlet
鉴权主要围绕着 FilterSecurityInterceptor
类展开,该类作为一个安全过滤器,被放置在 FilterChainProxy
中。
具体流程如下:
FilterSecurityInterceptor
从 SecurityContextHolder
中获取 Authentication
对象;FilterSecurityInterceptor
从 HttpServletRequest
、HttpServletREsponse
、 FilterChain
中创建 FilterInvocation
对象;FilterInvocation
对象传递给 SecurityMetadataSource
用来获取 ConfigAttribute
对象集合;Authentication
、FilterInvocation
和 ConfigAttribute
对象传递给 AccessDecisionManager
实例验证权限:AccessDeniedException
异常,并由 ExceptionTranslationFilter
接收并处理;FilterSecurityInterceptor
将控制权交还给 FilterChain
,使程序继续执行。Spring Security
是一个功能强大且高度可定制的身份验证和访问控制的安全框架。它是 Spring
应用程序在安全框架方面的公认标准。其核心特性包括:认证和授权、常规攻击防范、与 Servlet
接口集成、与 Spring MVC
集成等。
常规攻击防范在 Spring Security
安全框架中是默认开启的,常见的威胁抵御方式有:防止伪造跨站请求(CSRF
),安全响应头(HTTP Response headers
),HTTP
通讯安全等
新建 SpringBoot
项目,在 pom.xml
中增加 Spring Security
依赖:
1 | <dependency> |
只要加入依赖,项目的所有接口都会被自动保护起来。
创建一个 Controller:
1 |
|
导入spring-boot-starter-security
启动后,Spring Security
已经生效,默认拦截全部请求,如果用户没有登录,跳转到内置登录页面。
访问/hello
接口 ,需要登录之后才能访问。
默认配置下,会自动生成一个 user
用户,并分配其随机密码,密码可以从控制台的日志信息中找到:
Spring Boot
引入 Spring Security
启动后,将会自动开启如下配置项:
springSecurityFilterChain
的 Servlet
过滤器,包含了几乎所有的安全功能,例如:保护系统 URL
、验证用户名、密码表单、重定向到登录界面等;UserDetailsService
实例,并生成随机密码,用于获取登录用户的信息详情;除此之外,Spring Security
还有一些其他可配置的功能:
user
的可以通过表单认证的用户,并为其初始化密码;BCrypt
方式加密密码;CSRF
攻击;Servlet
接口,如:「getRemoteUser」、「getUserPrincipal」、「isUserInRole」、「login」和「logout」。也可以直接在 application.yml
配置文件中配置用户的基本信息:
1 | spring: |
配置完成后,重启项目,就可以使用这里配置的用户名/密码登录了。
如果需要自定义逻辑时,只需要实现UserDetailsService
接口即可。
1 |
|
返回值 UserDetails
是一个接口,要想返回 UserDetails
的实例就只能返回接口的实现类。
这里使用 Spring Security
中提供的 org.springframework.security.core.userdetails.User
。
Spring Security
要求:当进行自定义登录逻辑时容器内必须有 PasswordEncoder
实例。
客户端密码和数据库密码是否匹配是由 Spring Security
去完成的,但 Security
中没有默认密码解析器。所以当自定义登录逻辑时要求必须给容器注入PaswordEncoder
的bean
对象。
1 |
|
重启项目后,在浏览器中输入账号:admin
,密码:123
,登录后就可以访问接口了。
Sentinel 是阿里开源的项目,提供了流量控制、熔断降级、系统负载保护等多个维度来保障服务之间的稳定性。
Sentinel 分为两个部分:
Java
客户端)不依赖任何框架/库,能够运行于所有 Java
运行时环境,同时对 Dubbo / Spring Cloud
等框架也有较好的支持。Dashboard
)基于 Spring Boot
开发,打包后可以直接运行,不需要额外的 Tomcat
等应用容器。这里仅介绍Sentinel
核心库 与 Spring Cloud OpenFeign
整合使用。
基于上一篇OpenFeign服务间调用
pom.xml
文件中添加依赖1 | <dependency> |
YMAL
配置文件中添加如下配置1 | # 配置文件打开 Sentinel 对 Feign 的支持 |
Feign
对应的接口中的资源名策略定义:httpmethod:protocol://requesturl
。@FeignClient
注解中的所有属性,Sentinel
都做了兼容。
如:ToolsFeign 接口中方法 getSendSms 对应的资源名为 POST:http://XXX-CLOUD-TOOLS/tools/sms/send。
OpenFeign
调用远程服务,@FeignClient
属性中配置降级回调类@FeignClient
属性中的fallback
和fallbackFactory
fallback
:定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback
指定的类必须实现@FeignClient
标记的接口。fallbackFactory
:工厂类,用于生成fallback
类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码。注:同一个
@FeignClient
里,fallback
和fallbackFactory
不能同时使用。
Feign
的规则定义接口,使用fallback
属性:1 | /** |
ToolsFeignFallback.calss
1 |
|
Feign
的规则定义接口,使用fallbackFactory
属性:1 | /** |
ToolsFeignFallbackFactory.calss
1 |
|
ToolsFeignFallback.calss
1 | /** |
SLOW_REQUEST_RATIO
):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT
(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN
状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。ERROR_RATIO
):当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN
状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0]
,代表 0% - 100%
。ERROR_COUNT
):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN
状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。注意异常降级仅针对业务异常,对
Sentinel
限流降级本身的异常(BlockException
)不生效。为了统计异常比例或异常数,需要通过Tracer.trace(ex)
记录业务异常。
开源整合模块,如Sentinel Dubbo Adapter
,Sentinel Web Servlet Filter
或@SentinelResource
注解会自动统计业务异常,无需手动调用。
熔断降级规则(DegradeRule)包含下面几个重要的属性:
Field | 说明 | 默认值 |
---|---|---|
resource | 资源名,即规则的作用对象 | – |
grade | 熔断策略,支持 慢调用比例/异常比例/异常数策略 | 慢调用比例 |
count | 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值 | – |
timeWindow | 熔断时长,单位为 s | – |
minRequestAmount | 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) | 5 |
statIntervalMs | 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) | 1000 ms |
slowRatioThreshold | 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入) | – |
Sentinel 支持注册自定义的事件监听器监听熔断器状态变换事件(state change event)。示例:
1 | EventObserverRegistry.getInstance().addStateChangeObserver("logging", (prevState, newState, rule, snapshotValue) -> { |
OpenFeign
是SpringCloud
提供的一个声明式的伪Http
客户端,它使得调用远程服务就像调用本地服务一样简单,只需要创建一个接口并添加一个注解即可。OpenFeign
是SpringCloud
在Feign
的基础上支持了Spring MVC
的注解,并通过动态代理的方式产生实现类来做负载均衡并进行调用其他服务。
Spring Cloud OpenFeign
的依赖@EnableFeignCleints
Feign
的规则定义接口并添加@FeignClient
注解Feign
接口的类里注入,直接调用接口方法pom.xml
文件中添加依赖:1 | <dependency> |
@EnableFeignCleints
注解:1 | // basePackages 是Feign接口定义的路径 |
Feign
的规则定义接口:1 | // 括号内是远程调用微服务在注册中心的服务名 |
Feign
接口方法1 |
|
1 |
|
@EnableFeignCleints
触发Spring
应用程序对classpath
中@FeignClient
修饰类的扫描@FeignClient
修饰类后,Feign
框架通过扩展SpringBeanDeifinition
的注册逻辑,最终注册一个FeignClientFacotoryBean
进入Spring
容器Spring
容器在初始化其他用到@FeignClient
接口的类时,获得的是FeignClientFacotryBean
产生的一个代理对象Proxy
.java
原生的动态代理机制,针对Proxy
的调用,都会被统一转发给Feign
框架所定义的一个InvocationHandler
,由该Handler
完成后续的HTTP
转换,发送,接收,翻译HTTP
响应的工作Feign
和 RestTemplate
不一样 ,对请求细节封装的更加彻底,不管是请求还是请求的参数,还是响应的状态都看不到,想要看到请求的细节需要通过Feign
的日志,我们可以通过配置来调整日志级别,从而了解OpenFeign
中Http
请求的细节。即对OpenFeign
远程接口调用的情况进行监控和日志输出。
NONE
:默认级别,不显示日志BASIC
:仅记录请求方法、URL
、响应状态及执行时间HEADERS
:除了BASIC
中定义的信息之外,还有请求和响应头信息FULL
:除了HEADERS
中定义的信息之外,还有请求和响应正文及元数据信息1 |
|
在YMAL
配置文件中中指定监控的接口,以及日志级别
1 | logging: |
Spring Cloud Config
可以为微服务架构中的应用提供集中化的外部配置支持,它分为服务端和客户端两个部分。
服务端被称为分布式配置中心,它是个独立的应用,可以从配置仓库获取配置信息并提供给客户端使用。
客户端可以通过配置中心来获取配置信息,在启动时加载配置。Spring Cloud Config
默认采用Git
来存储配置信息,所以天然就支持配置信息的版本管理,并且可以使用Git
客户端来方便地管理和访问配置信息。
因为config server
是需要到git
上拉取配置文件的,所以还需要在远程的git
上新建一个存放配置文件的仓库,
如下仓库中存放客户端配置文件:
1 | application-beta.yml |
config-server
),在pom.xml
文件中添加依赖:1 | <!--配置服务--> |
application.yml
配置文件内容如下:1 | server: |
@EnableConfigServer
注解,声明这是一个config-server
。代码如下:1 | /** |
http://localhost:8009/master/application-dev.yml
,可以看到能够访问到客户端配置文件的内容。pom.xml
文件中添加依赖:1 | <dependency> |
bootstrap.yml
配置文件内容如下:1 | server: |
启动服务,配置中心读取的配置生效。
1 | # 获取配置信息 |
name
: 文件名,一般以服务名(spring.application.name
)来命名,如果配置了spring.cloud.config.name
,则为该名称.profiles
: 一般作为环境标识,对应配置文件中的spring.cloud.config.profile
lable
: 分支(branch
),指定访问某分支下的配置文件,对应配置文件中的spring.cloud.config.label
,默认值是在服务器上设置的(对于基于git
的服务器,通常是“master
”)Maven
提供了Profile
切换功能(多环境dev,beta,prod
), 如下pom.xml
:
1 | <profiles> |
配置文件bootstrap.yml
:
1 | spring: |
SpringBoot
一把情况下会遵从你选的环境将@activatedProperties@
替换掉。
但SpringCloud
比较特殊,使用配置中心后客户端不会再使用application.yml
,而是使用bootstrap.yml
。但是Maven
不认bootstrap.yml
里的@activatedProperties@
。
解决:在pom.xml
的build
标签里添加如下代码,用于过滤yml
文件:
1 | <build> |
因为config server
默认情况下只会搜索git
仓库根路径下的配置文件,所以我们还需要加上一个配置项:search-paths
,
该配置项用于指定config server
搜索哪些路径下的配置文件,需要注意的是这个路径是相对于git
仓库的,并非是项目的路径。
1 | spring: |
Spring Cloud Gateway 是基于 Spring5.0、SpringBoot2.0 和 Project Reactor 开发的网关,旨在提供一种简单而有效的方式来对API进行路由,基于过滤器链的方式提供:安全,监控/埋点,和限流。
Spring Cloud Gateway 基于 Spring Boot2.x、Spring WebFlux 和 Project Reactor构建,属于异步非阻塞模型。
路由(Route):路由是网关最基础的部分,路由信息由ID、目标URI、一组断言和一组过滤器组成。如果断言路由为真,则说明请求的 URI 和配置匹配。
断言(Predicate):Java8 中的断言函数。Spring Cloud Gateway 中的断言函数输入类型是 Spring 5.0 框架中的 ServerWebExchange。Spring Cloud Gateway 中的断言函数允许开发者去定义匹配来自于 Http Request 中的任何信息,比如请求头和参数等。
过滤器(Filter):使用特定工厂构建的 Spring Framework GatewayFilter 实例。过滤器将会对请求和响应进行处理。
客户端向 Spring Cloud Gateway 发出请求。 由网关处理程序 Gateway Handler Mapping 映射确定请求与路由匹配,则将其发送到网关 Web 处理程序 Gateway Web Handler。 Web 处理程序通过指定的过滤器链将请求发送到我们实际的服务执行业务逻辑,然后返回。 过滤器被虚线分隔的原因是过滤器可以在发送代理请求之前和之后运行逻辑。执行所有 pre 过滤器逻辑,然后发出代理请求;发出代理请求后,将运行 post 过滤器逻辑。
1 | <!-- 引入spring cloud gateway依赖 --> |
@EnableDiscoveryClient
注册到Eureka1 | // @EnableEurekaClient: 声明一个Eureka客户端,只能注册到Eureka Server |
1 | server: |
1 | spring: |
单个URI的地址的schema协议,一般为http或者https协议。 和注册中心相结合的路由配置的schema协议部分为自定义的lb:类型,表示从微服务注册中心(如Eureka)订阅服务,并且进行服务的路由。
1 | import org.springframework.boot.SpringApplication; |
Spring Cloud Gateway 是通过 Spring WebFlux 的 HandlerMapping 做为底层支持来匹配到转发路由,Spring Cloud Gateway 内置了很多 Predicates 工厂,这些 Predicates 工厂通过不同的 HTTP 请求参数来匹配,多个 Predicates 工厂可以组合使用。
路由谓词工厂(Route Predicate Factories)
类型 | 路由谓词 | 路由谓词工厂 | 描述 |
---|---|---|---|
时间相关 | After | AfterRoutePredicateFactory | 在某个时间之后的请求才会被转发,如:`- After=2017-01-20T17:42:47.789-07:00[America/Denver]` |
Before | BeforeRoutePredicateFactory | 在某个时间之前的请求才会被转发,如:`- Before=2017-01-20T17:42:47.789-07:00[America/Denver]` | |
Between | BetweenRoutePredicateFactory | 在某个时间段之间的才会被转发,如:`- Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]` | |
Cookie相关 | Cookie | CookieRoutePredicateFactory | `- Cookie=chocolate, ch.p`名为chocolate的表单或者满足正则ch.p的表单才会被匹配到进行请求转发 |
Header相关 | Header | HeaderRoutePredicateFactory | `- Header=X-Request-Id, \d+`携带参数X-Request-Id或者满足\d+的请求头才会匹配 |
Host | HostRoutePredicateFactory | `- Host=**.somehost.org,**.anotherhost.org`当主机名为somehost.org或anotherhost.org的时候才会被转发 | |
请求相关 | Method | MethodRoutePredicateFactory | `- Method=GET,POST`只有GET和POST方法才会匹配转发请求 |
Path | PathRoutePredicateFactory | `- Path=/red/{segment},/blue/{segment}`当请求的路径为/red/、/blue/开头的时才会被转发 | |
Query | QueryRoutePredicateFactory | `- Query=green`只要请求中包含green参数即可 | |
RemoteAddr | RemoteAddrRoutePredicateFactory | `- RemoteAddr=192.168.1.1/24`主机IP | |
Weight | WeightRoutePredicateFactory | `- Weight=group1, 2`权重是按组计算的, 两个参数:group 和 weight(int) |
1 | spring: |
Spring Cloud Gateway 除了具备请求路由功能之外,也支持对请求的过滤。
局部过滤器(GatewayFilter),是针对单个路由的过滤器。可以对访问的URL过滤,进行切面处理。Spring Cloud Gateway 包含许多内置的 GatewayFilter 工厂。
过滤器工厂 | 作用 | 参数 |
---|---|---|
AddRequestHeader | 为原始请求添加Header | Header的名称及值 |
AddRequestParameter | 为原始请求添加请求参数 | 参数名称及值 |
AddResponseHeader | 为原始响应添加Header | Header的名称及值 |
DedupeResponseHeader | 剔除响应头中重复的值 | 需要去重的Header名称及去重策略 |
CircuitBreaker | 为路由引入CircuitBreaker的断路器保护 | CircuitBreakerCommand的名称 |
MapRequestHeader | 将fromHeader的值更新到toHeader | fromHeader名称, toHeader名称 |
FallbackHeaders | 为fallbackUri的请求头中添加具体的异常信息 | Header的名称 |
PrefixPath | 为原始请求路径添加前缀 | 前缀路径 |
PreserveHostHeader | 为请求添加一个 preserveHostHeader=true的属 性,路由过滤器会检查该属性以 决定是否要发送原始的Host | 无 |
RequestRateLimiter | 用于对请求限流,限流算法为令 牌桶 | keyResolver、 rateLimiter、 statusCode、 denyEmptyKey、 emptyKeyStatus |
Redirect | 将原始请求重定向到指定的URL | http状态码及重定向的 url |
RemoveRequestHeader | 为原始请求删除某个Header | Header名称 |
RemoveResponseHeader | 为原始响应删除某个Header | Header名称 |
RemoveRequestParameter | 为原始请求删除请求参数 | 参数名称 |
RewritePath | 重写原始的请求路径 | 原始路径正则表达式以 及重写后路径的正则表 达式 |
RewriteLocationResponseHeader | 重写响应头中 Location 的值 | 输入四个参数:stripVersionMode、locationHeaderName、hostValue、protocolsRegex |
RewriteResponseHeader | 重写原始响应中的某个Header | Header名称,值的正 则表达式,重写后的值 |
SaveSession | 在转发请求之前,强制执行 WebSession::save操作 | 无 |
SecureHeaders | 为原始响应添加一系列起安全作 用的响应头 | 无,支持修改这些安全 响应头的值 |
SetPath | 修改原始的请求路径 | 修改后的路径 |
SetRequestHeader | 重置请求头的值 | Header的名称及值 |
SetResponseHeader | 修改原始响应中某个Header的值 | Header名称,修改后 的值 |
SetStatus | 修改原始响应的状态码 | HTTP 状态码,可以是 数字,也可以是字符串 |
StripPrefix | 用于截断原始请求的路径 | 使用数字表示要截断的 路径的数量 |
Retry | 针对不同的响应进行重试 | retries、statuses、 methods、series |
RequestSize | 设置允许接最大请求包的大小。如果请求包大小超过设置的 值,则返回 413 Payload Too Large | 请求包大小,单位为字 节,默认值为5M |
SetRequestHost | 用指定的值替换现有的host header | 指定的Host |
ModifyRequestBody | 在转发请求之前修改原始请求体内容 | 修改后的请求体内容 |
ModifyResponseBody | 修改原始响应体的内容 | 修改后的响应体内容 |
每个过滤器工厂都对应一个实体类,并且这些类的名称必须以GatewayFilterFactory结尾,这是Spring Cloud Gateway的一个约定,例如AddRequestHeader对一个的实体类为AddRequestHeaderGatewayFilterFactory。
4.1.1 限流过滤器RequestRateLimiter
Spring Cloud Gateway官方提供了基于令牌桶的限流支持。 基于其内置的过滤器工厂RequestRateLimiterGatewayFilterFactory实现。 在过滤器工厂中是通过Redis和Lua脚本结合的方式进行流量控制。
1 | <!-- reactive redis依赖包(包含Lettuce客户端) --> |
1 | spring: |
1 | import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; |
Spring Cloud Gateway目前提供的限流还是比较简单的,在实际开发中我们的限流策略会有很多种情况, 比如:对不同接口的限流,被限流后的友好提示,这些可以通过自定义RedisRateLimiter来实现自己的限流策略。
全局过滤器(GlobalFilter)作用于所有路由,Spring Cloud Gateway定义了Global Filter接口,用户可以自定义实现自己的Global Filter。 通过全局过滤器可以实现对权限的统一校验,安全性校验等功能。
过滤器工厂 | 描述 |
---|---|
ForwardRoutingFilter | 它会从exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);获取路由配置的URI,如果这个URI是forward模式,过滤器会将请求转发到DispatcherHandler,然后匹配到网关本的请求路径之中,原来请求的URI将被forward的URI覆盖,原始的请求URI被存储到exchange的ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR属性之中。 |
LoadBalancerClientFilter | 它是用来处理负载均衡的过滤器。在网关后面的服务可以启动多个服务实例,这个过滤器就是把请求根据均衡规则路由到某台服务实例上面。它从exchange的ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR属性中获取URI,如果这个URI的scheme是“lb”,如:lb://myserivce,它会使用spring cloud 的LoadBalancerClient解析myservice服务名,获取一个服务实例的host和port,并替换原来的客户端请求。原来请求的url会存储在exchange的ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR属性中。这个过滤器也会从exchange中获取ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR属性值,如果它的值也是“lb”,也会使用相同的规则路由。 |
NettyRoutingFilter | 这是一个优先级最低的过滤器,如果从exchange的ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR获取的URL的scheme是https或http,它将使用Netty的HttpClient创建向下执行的请求代理,请求返回的结果将存储在exchange的ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR属性中,过滤器链后面的过滤器可以从中获取返回的结果。(还有一个测试使用的过滤器,WebClientHttpRoutingFilter,它和NettyRoutingFilter的功能一样,但是不使用netty)。 |
NettyWriteResponseFilter | 它的优先级是最高的,它是“post”类型的过滤器。如果在exchange中ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR的属性存在HttpClientResponse,它会在所有的其它的过滤器执行完成之后运行,将响应的数据发送给网关的客户端。 |
RouteToRequestUrlFilter | 它的作用是把浏览器的URL请求的Path路径添加到路由的URI之中,比如浏览器请求网关的URL是:http://localhost:8080/app-a/app/balance ,路由的URI配置是:uri: lb://app-a ,那么添加之后的路由的URI是:lb://app-a/app/balance ,并将它存储在exchange的ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR属性之中。 |
WebsocketRoutingFilter | 它是用来路由WebScoket请求,在exchange的ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR的URI中,如果scheme是ws或wss,它会使用Spring Web Socket 模块转发WebSocket请求。WebSockets可以使用路由进行负载均衡,比如:lb:ws://serviceid。 |
GatewayMetricsFilter | 它用来统计一些网关的性能指标。需要添加spring-boot-starter-actuator的项目依赖。 |
在网关路由 ServerWebExchange 后,它将通过在 exchange 添加一个 gatewayAlreadyRouted 属性,从而将exchange标记为 routed 。一旦请求被标记为 routed ,其他路由过滤器将不会再次路由请求,而是直接跳过,防止重复的路由操。可以使用便捷方法将 exchange 标记为 routed ,或检查 exchange 是否是 routed。
1 | ServerWebExchangeUtils.isAlreadyRouted //检查是否已被路由 |
鉴权逻辑: ①当客户端第一次请求服务的时候,服务端对用户进行信息认证(登录)。 ②认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证。 ③以后每次请求,客户端都携带认证的token。 ④服务端对token进行解密,判断是否有效。
对于验证用户是否已经登录授权的过程可以在网关层统一校验。校验的标准就是请求中是否携带token凭证以及token的正确性。
这里代码实现仅判断是否携带token凭证:TokenFilter.java
1 | import org.apache.commons.lang.StringUtils; |
Eureka是一种RESTful服务,主要用于AWS云中间层服务器的发现、负载平衡和故障转移。
Eureka包含两个组件:服务注册中心Eureka Server 和 服务客户端Eureka Client。
Eureka Server提供注册服务,各个节点启动后,会在Eureka Server中进行注册,这样Eureka Server中的服务注册表中将会存储所有可用服务节点的信息。
Eureka Server通过Register、Get、Renew等接口提供服务的注册、发现和心跳检测等服务。
Eureka Client是一个java客户端,用于简化与Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。
在应用启动后,将会向Eureka Server发送心跳,默认周期为30秒,如果Eureka Server在多个心跳周期内没有收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除(默认90秒)。
Eureka Client分为两个角色,分别是:Service Provider(服务提供方)和Service Consumer(服务消费方)。
Eureka Server
:提供服务注册和发现,多个Eureka Server之间会同步数据,做到状态一致(最终一致性)Service Provider
:服务提供方,将自身服务注册到Eureka。Service Consumer
:服务消费方,通过Eureka Server发现服务,并消费。微服务在Eureka上注册后,会每30秒发送心跳包,Eureka通过心跳来判断服务时候健康,默认90s没有得到客户端的心跳,则注销该实例。
导致Eureka Server收不到心跳包的可能:一是微服务自身故障,二是微服务与Eureka之间的网络故障。
通常微服务的自身的故障只会导致个别服务出现故障,而网络故障通常会导致大面积服务出现故障。
Eureka设置了一个阀值,当判断挂掉的服务的数量超过阀值(心跳失败比例在15分钟之内低于85%)时,Eureka Server认为很大程度上出现了网络故障,将不再删除心跳过期的服务,这种服务保护算法叫做Eureka Server的服务保护模式。
当网络故障恢复后,Eureka Server会退出”自我保护模式”。
Eureka还有客户端缓存功能(也就是微服务的缓存功能)。即便Eureka Server集群中所有节点都宕机失效,微服务的Provider和Consumer都能正常通信。
只要Consumer不关闭,缓存始终有效,直到一个应用下的所有Provider访问都无效的时候,才会访问Eureka Server重新获取服务列表。
1 | eureka: |
1 | <!-- 引入SpringCloud Eureka server的依赖 --> |
@EnableEurekaServer
1 | // 声明当前项目为Eureka Server |
1 | server: |
http://localhost:18000
1 | <!-- 引入SpringCloud Eureka client的依赖 --> |
@EnableDiscoveryClient
1 | // @EnableEurekaClient: 声明一个Eureka客户端,只能注册到Eureka Server |
1 | server: |
通过以上四步 就完成了一个 Eureka客户端的搭建,直接启动项目, 访问Eureka的注册中心http://localhost:18000
查看当前服务。
指标 | 描述 |
---|---|
数据一致性 (Consistency) | 也叫做数据原子性系统在执行某项操作后仍然处于一致的状态。在分布式系统中,更新操作执行成功后所有的用户都应该读到最新的值,这样的系统被认为是具有强一致性的。等同于所有节点访问同一份最新的数据副本。 优点: 数据一致,没有数据错误可能。 缺点: 相对效率降低。 |
服务可用性 (Availablity) | 每一个操作总是能够在一定的时间内返回结果,这里需要注意的是”一定时间内”和”返回结果”。一定时间内指的是,在可以容忍的范围内返回结果,结果可以是成功或者是失败。 |
分区容错性 (Partition-torlerance) | 在网络分区的情况下,被分隔的节点仍能正常对外提供服务(分布式集群,数据被分布存储在不同的服务器上,无论什么情况,服务器都能正常被访问) |
CAP由Eric Brewer在2000年PODC会议上提出。该猜想在提出两年后被证明成立,成为我们熟知的CAP定理。
一般来说,分区容错无法避免,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们,剩下的 C 和 A 无法同时做到。 因为可能通信失败(即出现分区容错),所以,对于分布式系统,我们只能能考虑当发生分区错误时,如何选择一致性和可用性。
需要强调的是:C 和 A 的抉择是发生在有分区问题的时候,正常情况下系统就应该有完美的数据一致性和可用性。
而根据一致性和可用性的选择不同,开源的分布式系统往往又被分为 CP 系统和 AP 系统。
当一套系统在发生分区故障后,客户端的任何请求都被卡死或者超时,但是,系统的每个节点总是会返回一致的数据,则这套系统就是 CP 系统,经典的比如 Zookeeper。
如果一套系统发生分区故障后,客户端依然可以访问系统,但是获取的数据有的是新的数据,有的还是老数据,那么这套系统就是 AP 系统,经典的比如 Eureka。
很多时候一致性和可用性并不是二选一的问题,大部分的时候,系统设计会尽可能的实现两点,在二者之间做出妥协,当强调一致性的时候,并不表示可用性是完全不可用的状态,比如,Zookeeper 只是在 master 出现问题的时候,才可能出现几十秒的不可用状态,而别的时候,都会以各种方式保证系统的可用性。而强调可用性的时候,也往往会采用一些技术手段,去保证数据最终是一致的。
]]>rocketmq-spring-boot-starte
r可以快速的搭建RocketMQ
生产者和消费者服务。pom.xml
引入组件rocketmq-spring-boot-starter
依赖1 | <!-- https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-spring-boot-starter --> |
application.yml
,添加RocketMQ
相关配置1 | # 多个name-server(集群)使用英文;分割 |
使用RocketMQTemplate
实现消息的发送;
使用实现RocketMQListener
接口,并添加@RocketMQMessageListener
注解,声明消费主题,消费者分组等,且默认消费模式是集群消费。
发送消息测试接口:http://localhost:8080/send/common
1 |
|
普通消息监听消费
1 | /** |
发送消息测试接口:http://localhost:8080/send/tag
1 |
|
监听消费
1 | /** |
发送消息测试接口:http://localhost:8080/send/broadcast
1 |
|
监听消费
1 | /** |
发送消息测试接口:http://localhost:8080/send/random
1 |
|
监听消费
1 | /** |
发送消息测试接口:http://localhost:8080/send/order
1 |
|
监听消费
1 | /** |
producer
向broker
发送消息时指定消息发送成功及发送异常的回调方法,调用API
后立即返回,producer
发送消息线程不阻塞 ,消息发送成功或失败的回调任务在一个新的线程中执行。
发送消息测试接口:http://localhost:8080/send/async
1 |
|
监听消费
1 |
|
单向发送消息这种方式主要用在不特别关心发送结果的场景,例如日志发送。
发送消息测试接口:http://localhost:8080/send/oneway
1 |
|
监听消费
1 |
|
发送消息测试接口:http://localhost:8080/send/delay
1 |
|
监听消费
1 |
|
发送消息测试接口:http://localhost:8080/send/tx
1 |
|
生产者端需要实现RocketMQLocalTransactionListener
接口,重写执行本地事务的方法和检查本地事务方法;@RocketMQTransactionListener
注解表明这个一个生产端的消息监听器,需要配置监听的事务消息生产者组。
1 |
|
监听消费
1 | /** |
1 | 2021-06-11 17:25:02.861 INFO 13904 --- [MessageThread_3] com.demo.CommonListener : CommonListener收到消息:普通消息 |
]]>
Java
语言开发的一个分布式、队列模型的消息中间件,后开源给Apache
基金会成为了Apache
的顶级开源项目,具有高性能、高可靠、高实时、分布式特点。RocketMQ 主要由Producer
、Broker
、Consumer
、NameServer
组成;其中Producer
负责生产消息;Consumer
负责消费消息;Broker
是MQ
服务,负责接收、分发消息;NameServer
是路由中心,负责MQ
服务之间的协调。
RocketMQ
安装包1 | 进入自定义软件安装目录 |
JDK1.8
或以上)1 | 解压 |
1 | RocketMQ的默认内存占用非常高,是4×4g的,将4g调整为512m |
RocketMQ
的环境变量1 | 编辑/etc/profile |
RockerMQ
顺序1 | 先启动 NameServer,然后启动 Broker |
RockerMQ
顺序1 | 先关闭Broker,再关闭NameServer |
1 | 查看 Name Server 启动日志 |
IP
调试,关闭防火墙 或 开放防火墙端口9876,10911
1 | NameServer默认端口:9876 |
pom.xml
引入组件rocketmq-spring-boot-starter
依赖1 | <!-- https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-spring-boot-starter --> |
application.yml
,添加RocketMQ
相关配置1 | # 多个name-server(集群)使用英文;分割 |
1 |
|
1 | /** |
1 |
|
1 | 2021-06-10 14:56:25.180 INFO 17720 --- [ main] a.r.s.s.DefaultRocketMQListenerContainer : running container: DefaultRocketMQListenerContainer{consumerGroup='test-group', nameServer='192.168.2.100:9876', topic='test-topic', consumeMode=CONCURRENTLY, selectorType=TAG, selectorExpression='*', messageModel=CLUSTERING} |
事件经过:由于前端同学不小心把上传图片服务器地址写死了测试域名(指向测试服务器),然后项目上到正式环境,一段时间后,发现用户发布商品时的商品详情富文本中的图片全部指向测试图片服务器域名,然后图片又太多了,手动逐条修复数据不太现实。
解决思路:
Url
地址;Url
下载图片到本地服务器并保持图片存放路径与图片文件名和原本一致;rsync
远程同步命令同步到正式服务器;Url
地址指向正式服务器域名(因为路径和文件名与测试服务器一致,只需替换域名即可)。pom.xml
依赖1 | <dependency> |
1 | /** |
1 | <!-- 这里测试图片服务器域名为:img-test.abc.com --> |
1 |
|
1 | # 登录远程服务器,进入图片服务器目录 |
1 | update productInfo set detail = REPLACE(detail, 'img-test.abc.com%'', 'img.abc.com%'') where detail like like '%img-test.abc.com%' |
1 | location / { |
然后重启Nginx
。
Thymeleaf
手动渲染原理:当与SpringBoot
结合时,放入Model
的数据就会被放到上下文(Context
)中,并且此时模板解析器(TemplateResolver
)已经创建完成(默认模板存放位置:templates
,默认模板文件类型:html
);然后通过模板引擎(TemplateEngine
)结合上下文(Context
)与模板解析器(TemplateResolver
),利用内置的语法规则解析,从而输出解析后的文件到指定目录。
1 | /** |
相关依赖:
1 | <dependency> |
配置文件:
1 | server: |
接口和实现类:
1 |
|
html模板原型:
1 |
|
启动测试:浏览器访问:http://127.0.0.1:8080/generate/home
或编写测试类测试。
相关依赖:
1 | <dependency> |
接口和实现类:
1 | public interface GenerateHtml { |
可在项目启动后执行覆盖拷贝操作:
1 |
|
生成静态文件:
1 |
|
1 | # Java错误日志: |
Redis
在个默认情况下,如果在RDB snapshots
持久化过程中出现问题,Redis
不允许用户进行任何更新操作;即:stop-writes-on-bgsave-error yes
。
临时解决方案是通过命令:config set stop-writes-on-bgsave-error no
设置这个选项为false
,让程序忽略了这个异常,使得程序能够继续往下运行,但写硬盘仍然是失败的!
Redis
在进行持久化的时候,有的时候可以在日志中看到fork进程失败的提示,一般是系统可用的内存空间不够导致,这需要我们对fork
原理明白,才能更好的进行参数调整。
一般来说Redis
在进行RDB
的时候,会fork
出一个子进程,子进程和父进程会共享一个地址空间,在fork
子进程的时候,会检查当前机器可用的内存是否满足fork
出一个子进程的要求,一般由操作系统overcommit_memory
(系统内存分配策略)决定。
Redis
的数据回写机制分同步和异步两种,SAVE
命令,主进程直接向磁盘回写数据。在数据大的情况下会导致系统假死很长时间,所以一般不是推荐的。BGSAVE
命令,主进程fork
后,复制自身并通过这个新的进程回写磁盘,回写结束后新进程自行关闭。由于这样做不需要主进程阻塞,系统不会假死,一般默认会采用这个方法。Redis
默认采用异步回写,所以如果我们要将数据刷到硬盘上,这时Redis
分配内存不能太大,否则很容易发生内存不够用无法fork
的问题;
设置一个合理的写磁盘策略,否则写频繁的应用,也会导致频繁的fork
操作,对于占用了大内存的Redis
来说,fork
消耗资源的代价是很大的;
Linux
对大部分申请内存的请求都回复yes
,以便能跑更多更大的程序。
因为申请内存后,并不会马上使用内存,将这些不会使用的空闲内存分配给其它程序使用,以提高内存利用率,这种技术叫做Overcommit
。
一般情况下,当所有程序都不会用到自己申请的所有内存时,系统不会出问题,但是如果程序随着运行,需要的内存越来越大,在自己申请的大小范围内,不断占用更多内存,直到超出物理内存,当Linux
发现内存不足时,会发生OOM killer(OOM=out-of-memory)
。
OOM killer
会选择杀死一些进程,以便释放内存。当发生OOM killer
时,会记录在系统日志中/var/log/messages
。
用户态进程,非内核线程,占用内存越多和运行时间越短的进程越有可能被杀掉。
在Linux
下有个vm内核参数:CommitLimit
用于限制系统应用使用的内存资源;执行grep -i commit /proc/meminfo
,看到CommitLimit
和Committed_As
参数。
CommitLimit
是一个内存分配上限,CommitLimit = 物理内存 * overcommit_ratio(/proc/sys/vm/overcmmit_ratio,默认50,即50%) + swap大小
Committed_As
是已经分配的内存大小(应用程序要申请的内存 + 系统已经分配的内存)。vm.overcommit_memory
文件指定了内核针对内存分配的策略,其值可以是0、1、2
。
0
:启发策略(默认);表示内核将检查是否有足够的可用内存供应用进程使用;如果有足够的可用内存,内存申请允许;否则,内存申请失败,并把错误返回给应用进程。系统在为应用进程分配虚拟地址空间时,会判断当前申请的虚拟地址空间大小是否超过剩余内存大小,如果超过,则虚拟地址空间分配失败。因此,也就是如果进程本身占用的虚拟地址空间比较大或者剩余内存比较小时,fork
、malloc
等调用可能会失败。 0
即是启发式的overcommitting handle
,会尽量减少swap
交换分区的使用,root
可以分配比一般用户略多的内存。1
:允许overcommit
;表示内核允许分配所有的物理内存,而不管当前的内存状态如何,允许超过CommitLimit
,这种情况下,避免了fork
可能产生的失败,但由于malloc
是先分配虚拟地址空间,而后通过异常陷入内核分配真正的物理内存,在内存不足的情况下,这相当于完全屏蔽了应用进程对系统内存状态的感知,即malloc
总是能成功,一旦内存不足,会引起系统OOM
杀进程,应用程序对于这种后果是无法预测的。 直至内存用完为止。在数据库服务器上不建议设置为1,从而尽量避免使用swap
交换分区。2
:禁止overcommit
;表示不允许超过CommitLimit
值。由于很多情况下,进程的虚拟地址空间占用远大于其实际占用的物理内存,这样一旦内存使用量上去以后,对于一些动态产生的进程(需要复制父进程地址空间)则很容易创建失败,如果业务过程没有过多的这种动态申请内存或者创建子进程,则影响不大,否则会产生比较大的影响 。这种情况下系统所能分配的内存不会超过上面提到的CommitLimit
大小,如果这么多资源已经用光,那么后面任何尝试申请内存的行为都会返回错误,这通常意味着此时没法运行任何新程序。我们可以通过设置overcommit_memory=1
的优化,减少操作系统内存,提高Redis
的fork
成功率,因为fork
后的进程和父进程共享一个数据空间,持久化要新增的内存空间都会小于父进程已经使用的空间,具体有三种方式修改内核参数,但要有root
权限:
/etc/sysctl.conf
,改vm.overcommit_memory=1
,然后sysctl -p
使配置文件生效;sysctl vm.overcommit_memory=1
;echo 1 > /proc/sys/vm/overcommit_memory
;当Redis
持久化fork
子进程后,占用内存大小和父进程等同,由于Linux
在写时有copy-on-write
机制,父子进程共享相同的物理内存页,当父进程处理写请求的时候会把要修改的页创建副本,而子进程在fork
过程中共享整个父进程的内存快照。如果我们要减少创建的副本的大小,就涉及操作系统的另外一个概念Huge Pages
(大页)。
在Redhat Linux
中,内存都是以页的形式划分的,默认情况下每页是4K
,这就意味着如果物理内存很大,则映射表的条目将会非常多,会影响CPU
的检索效率。因为内存大小是固定的,为了减少映射表的条目,可采取的办法只有增加页的尺寸。Linux Kernel
在2.6.38
内核中增加了THP
(Transparent Huge Pages)的特性,支持大内存页(2MB
)分配,默认开启。当开启后可以加快fork
子进程的速度,但fork
操作之后,每个内存页从原来的4KB
变成了2MB
,会大幅增加重写期间父进程内存消耗,同时每次写命令引起的复制内存页单位放大了512
倍,会拖慢写操作的执行时间,因此在使用Redis
的时候Redis
建议关闭THP
,方法为:echo never > /sys/kernel/mm/transparent_hugepage/enabled
。为了让机器重启该参数仍然生效,建议在/etc/rc.local
中追加echo never > /sys/kernel/mm/transparent_hugepage/enabled
,避免失效。当大页被关闭后,可以看到同等操作下,RDB
备份时候的copy-on-write
变化内存空间会减少。
综上分析,我们可以操作系统物理内存和Redis
内存之间的一些关系,尤其Redis
在持久化的时候fork
进程会随操作系统的参数不同,需要的内存也有所不同,为了加快fork
子进程的速度以及主备之间的文件传输同步,一般我们建议一个Redis
节点的最大内存在10G-15G
左右,操作系统的内存适当冗余,尽量控制同一台机器的多个Redis
节点在同一个时间点进行RDB
备份(可以通过缓存中心定时备份),导致内存同一时刻增加避免内存空间不足导致的fork
失败,最安全保险的情况是内存为Redis
的2倍
,但是在vm.overcommit_memory=1和大页关闭的情况下,可以根据实际使用,降低操作系统的整个内存大小 。
https://www.jianshu.com/p/785ee3bea266
https://www.cnblogs.com/wjoyxt/p/3777042.html
Spring WebFlux
模块提供的一个非阻塞的基于响应式编程的进行HTTP
请求的客户端工具。引入WebFlux
依赖则可使用WebClient
:
1 | <dependency> |
WebClient
接口提供了三个不同的静态方法(create()
,create(String baseUrl)
,builder()
)和一个内部类(WebClient.Bulider
)来创建WebClient
实例:
1 | /** |
1 |
|
1 |
|
1 |
|
1 |
|
使用WebClient
发送请求时, 如果接口返回的不是200
状态(而是4xx
、5xx
这样的异常状态),则会抛出WebClientResponseException
异常。
1 |
|
前面我们都是使用retrieve()
方法是直接获取响应体的内容。
使用exchangeToMono()
和exchangeToFlux()
方法获取完整的代表响应结果的对象,通过该对象我们可以获取响应码、contentType
、contentLength
、响应消息体等。
1 |
|
引入依赖:
1 | <dependency> |
编写配置,创建WebClient.Bulider
类型的Bean
,加上@LoadBalaced
为WebClient
增加负载均衡的支持。
1 | import org.springframework.cloud.client.loadbalancer.LoadBalanced; |
编写Controller,客户端实现访问服务端资源,并对外提供访问接口:
1 |
|
Server-Sent Events
服务器推送事件,是一种仅发送文本消息的技术。SSE
基于HTTP
协议中的持久连接。SSE
是HTML5
标准协议中的一部分。客户端接收服务端异步更新的消息可以分为两类:客户端拉取和服务端推送。
客户端拉取:通过短轮询或者长轮询定期请求服务器进行更新。
服务端推送:SSE
和WebSocket
,SSE
是单向,WebSocket
是双向;SSE
基于HTTP
协议,WebSocket
基于WebSocket
协议(HTTP
以外的协议);
text/event-stream
。响应文本的内容是一个事件流,事件流是一个简单的文本流,仅支持UTF-8
格式的编码。\r\n
)来分隔。:
)进行分隔,冒号前的为类型,冒号后的为其对应的值。每个事件可以包含如下类型的行:data
,表示该行是事件所包含的数据。以data
开头的行可以出现多次。所有这些行都是该事件的数据。event
,表示该行用来声明事件的类型,即事件名称。浏览器在收到数据时,会产生对应名称的事件。id
,表示该行用来声明事件的标识符。retry
,表示该行用来声明浏览器在连接断开之后进行重连的等待时间。1 | data: china // 该事件仅包含数据 |
id
作用: 如果服务端发送的事件中包含事件标识id
,那么浏览器会将最近一次接收到的事件标识id
记录到HTTP
头的Last-Event-ID
属性中。如果浏览器与服务端的连接中断,当浏览器再次连接时,会将Last-Event-ID
记录的事件标识id
发送给服务端。服务器端通过浏览器端发送的事件标识id
来确定将继续连接哪个事件。订阅一个服务端推送事件(GET
请求),需要设置 包含如下请求头的Request
:
1 | Accept: text/event-stream # 指明MediaType是事件流 |
服务端需要提供 包含以下响应头的Response:
1 | Content-Type: text/event-stream;charset=UTF-8 # 告诉客户端响应是一个事件流 |
首先,在pom.xml
文件中,引入webflux
;
1 | <dependency> |
创建一个controller
类并用@RestController
注解标记;
创建一个接受Http GET
请求的方法,该方法返回一个Flux
对象,并配置produces=text/event-stream
;
1 |
|
浏览器访问/test/sse
就会看到每一秒推送一个数据:
1 | data: -> 0 |
Flux.just()
将消息列表里的消息一条条发送出去即可。Flux.inteval()
来轮询。Spring
的事件监听接口来实现,关键点在于要把监听消息的处理器和Flux的构造结合起来。SSE
只适合发送文本消息;尽管可以使用Base64
编码和gzip
压缩来发送二进制消息,但效率可能很低。Internet Explorer
不支持。Internet Explorer/Edge
和许多移动浏览器不支持SSE
;尽管可以使用polyfills
,但它们可能效率低下SSE
连接,通过事件来区分。因为浏览器对同时并发的连接数有限制,一般最大是6个。JavaScript
里用EventSource对象来接收服务器发送事件通知:
1 | if (typeof(EventSource)!=="undefined") { |
EventSource
是服务器推送的一个网络事件接口。一个EventSource
实例会对HTTP
服务开启一个持久化的连接,以text/event-stream
格式发送事件, 会一直保持开启直到被要求关闭。
EventSource
属性EventSource.onerror
:EventHandler,当发生错误时被调用,并且在此对象上派发error
事件。EventSource.onmessage
:EventHandler,当收到一个message
事件,当接收到消息时被调用。EventSource.onopen
:EventHandler,当收到一个open
事件,当连接刚打开时被调用。EventSource.readyState
(只读):unsigned short值,代表连接状态。可能值是CONNECTING(0), OPEN(1), 或者CLOSED(2)。EventSource.url
(只读):一个DOMString,代表事件源的URL。Spring Data Redis
中同时支持了Jedis
客户端和Lettuce
客户端。但是仅Lettuce
是支持Reactive
方式的操作;这里选择默认的Lettuce
客户端。创建Maven
项目,并在pom.xml
导入依赖:
1 | <!-- reactive redis依赖包(包含Lettuce客户端) --> |
配置文件application.yml
1 | spring: |
注入配置类:
1 |
|
简单的RedisService封装
1 |
|
测试
1 |
|
测试运行结果:
1 | true |
]]>本文使用Spring Boot版本:2.4.3