谨防被删,以下做了备份
最佳实践
JDBC框架的选择
主流JDBC框架有Hibernate、MyBatis、Spring JDBC Template、Ebean、DBUtils等,Dew基于Spring Boot,所以对于这些框架都提供了很好的兼容。那么如何选择呢?
- 先说Hibernate,如果你的数据表结构与对象模型基本吻合,那么使用JPA会带来很大的便捷,推荐Hibernate
- 如果你的数据表结构与对象模型严重不吻合或是你希望对SQL有更多主动权(SQL优化、复杂查询等),那JPA就没什么优势了,这时:
- 如果你追求极简、不需要ORMPPING,那么DBUtils会是最佳选择
- 如果你喜欢敏捷开发,推崇充血模型,那么尝试一下Ebean吧,与Play!结合最合适不过
- 如果你既要有一定的ORMPPING能力,又希望自己写SQL,那么MyBatis会是不错的选择
- 如果你使用了Spring,希望框架简单些,可以接受自己写ORMPPING,未来无切换关系型数据库的计划,那么Spring JDBC Template将是个很好的选择
服务调用开发期使用
在 Spring Cloud
体系下,服务调用需要启动 Eureka
服务(对于 Dew
中的 Registry
组件),这对开发阶段并不友好:
- 开发期间会不断启停服务,
Eureka
保护机制会影响服务注册(当然这是可以关闭的) - 多人协作时可能会出现调用到他人服务的情况(同一服务多个实例)
- 需要启动
Eureka
服务,多了一个依赖
为解决上述问题,在使用 Spring Cloud
的 RestTemplate
时,增加 Ribbon
的服务配置.
# <client>为service-id
<client>.ribbon.listOfServers: <直接访问的IPs>
# 如
x-service.ribbon.listOfServers: 127.0.0.1:8812
@Validated
注解
- 在Spring Controller类里,
@Validated
注解初使用会比较不易上手,在此做下总结- 对于基本数据类型和String类型,要使校验的注解生效,需在该类上方加
@Validated
注解 - 对于抽象数据类型,需在形式参数前加
@Validated
注解
- 对于基本数据类型和String类型,要使校验的注解生效,需在该类上方加
Tip | Spring对抽象数据类型校验抛出异常为MethodArgumentNotValidException ,http状态码为400,对基本数据类型校验抛出异常为ConstraintViolationException ,http状态码为500,dew对这两种异常做了统一处理,http状态码均返回200,code为400 |
---|---|
jackson
对于 Java8
时间转换( SpringMVC
以 jackson
接收 json
数据)
- 对于
LocalDateTime
类型,需在参数上加@JsonFormat
注解,如下:@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
LocalDate,LocalTime,Instant
等,无需配置可自行转换
Tip | jackson 对于 LocalDateTime 类型的支持与其他三种类型不具有一致性,这是 jackson 需要优化的一个点 |
---|---|
Ribbon
负载均衡
example: service-dew.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
Note | service-dew 为服务名,配置时自行选取规则,类均在com.netflix.loadbalancer 包下 |
---|---|
若指定zone,默认会优先调用相同zone的服务,此优先级高于策略配置,配置如下
#指定属于哪个zone
eureka:
instance:
metadata-map:
zone: #zone 名称
#指定region(此处region为项目在不同区域的部署,为项目规范,不同region间能互相调用)
eureka:
client:
region: #region名称
Feign
配置特定方法超时时间
Hystrix 超时时间配置
# 配置默认的hystrix超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=10000
# 配置特定方法的超时时间,优于默认配置
hystrix.command.<hystrixcommandkey>.execution.isolation.thread.timeoutInMilliseconds=10000
# <hystrixcommandkey>的format为FeignClassName#methodSignature,下面是示例配置
hystrix.command.PressureService#getBalance(int).execution.isolation.thread.timeoutInMilliseconds=10000
Ribbon 超时时间配置
# 配置默认ribbon超时时间
ribbon.ReadTimeout=60000
# 配置特定服务超时时间,优于默认配置
<client>.ribbon.ReadTimeout=6000
# <client>为实际服务名,下面是示例配置
x-service.ribbon.ReadTimeout=5000
Hystrix 和 Ribbon 的超时时间配置相互独立,以低为准,使用时请根据实际情况进行配置
Tip | 如果要针对某个服务做超时设置,建议使用 Ribbon 的配置;在同时使用 Ribbon 和 Hystrix 时,请特别注意超时时间的配置。 |
---|---|
Feign
接口添加 Http
请求头信息
Tip | 在 @FeignClient 修饰类中的接口方法里添加新的形参,并加上 @RequestHeader 注解指定key值 |
---|---|
示例
@PostMapping(value = "ca/all", consumes = MediaType.APPLICATION_JSON_VALUE)
Resp<CustomerInfoVO> applyCA(@RequestBody CAIdentificationDTO params,
@RequestHeader Map<String, Object> headers);
Feign
文件上传实践
- 在
SDK
工程处,添加包依赖
pom
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
<version>3.0.1</version>
</dependency>
- 在
SDK
工程处,创建一个Configuration
MultipartSupportConfig
import feign.codec.Encoder;
import feign.form.spring.SpringFormEncoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.cloud.netflix.feign.support.SpringEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MultipartSupportConfig {
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
@Bean
public Encoder feignFormEncoder() {
return new SpringFormEncoder(new SpringEncoder(messageConverters));
}
}
- 修改接口
FeginExample
@FeignClient(name = "demo")
public interface FeginExample {
@PostMapping(value = "images", consumes = MULTIPART_FORM_DATA_VALUE)
Resp<String> uploadImage(
@RequestParam MultipartFile image,
@RequestParam("id") String id);
}
@RequestPart
与 @RequestParam
效果是一样的,大家就不用花时间在这上面了。
- 修改服务器接口
FeginServiceExample
@RestController
public class FeginServiceExample {
@PostMapping(value = "images", consumes = MULTIPART_FORM_DATA_VALUE)
public Resp<String> uploadImage(
@RequestParam("image") MultipartFile image,
@RequestParam("id") String id,
HttpServletRequest request) {
return Resp.success(null);
}
}
常见问题:
HTTP Status 400 - Required request part 'file' is not present
请求文件参数的名称与实际接口接受名称不一致
feign.codec.EncodeException: Could not write request: no suitable HttpMessageConverter found for request type [org.springframework.mock.web.MockMultipartFile] and content type [multipart/form-data]
转换器没有生效,检查一下MultipartSupportConfig
自定义降级方法
Note | 构建类继承HystrixCommand抽象类,重写run方法,getFallback方法,getFallback为run的降级,再执行excute方法即可 |
---|---|
Tip | 每个HystrixCommand的子类的实例只能execute一次。 |
---|---|
下面附上代码
public class HelloHystrixCommand extends HystrixCommand<HelloHystrixCommand.Model> {
public static final Logger logger = LoggerFactory.getLogger(HelloHystrixCommand.class);
private Model model;
protected HelloHystrixCommand(HystrixCommandGroupKey group) {
super(group);
}
protected HelloHystrixCommand(HystrixCommandGroupKey group, HystrixThreadPoolKey threadPool) {
super(group, threadPool);
}
protected HelloHystrixCommand(HystrixCommandGroupKey group, int executionIsolationThreadTimeoutInMilliseconds) {
super(group, executionIsolationThreadTimeoutInMilliseconds);
}
protected HelloHystrixCommand(HystrixCommandGroupKey group, HystrixThreadPoolKey threadPool, int executionIsolationThreadTimeoutInMilliseconds) {
super(group, threadPool, executionIsolationThreadTimeoutInMilliseconds);
}
protected HelloHystrixCommand(Setter setter) {
super(setter);
}
public static HelloHystrixCommand getInstance(String key){
return new HelloHystrixCommand(HystrixCommandGroupKey.Factory.asKey(key));
}
@Override
protected Model run() throws Exception {
int i = 1 / 0;
logger.info("run: thread id: " + Thread.currentThread().getId());
return model;
}
@Override
protected Model getFallback() {
return new Model("fallback");
}
public static void main(String[] args) throws Exception {
HelloHystrixCommand helloHystrixCommand = HelloHystrixCommand.getInstance("dew");
helloHystrixCommand.model = helloHystrixCommand.new Model("run");
logger.info("main: " + helloHystrixCommand.model + "thread id: " + Thread.currentThread().getId());
System.out.println(helloHystrixCommand.execute());
}
class Model {
public Model(String name) {
this.name = name;
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Model{" +
"name='" + name + '\'' +
'}';
}
}
}
断路保护
Hystrix配置
# 执行的隔离策略 THREAD, SEMAPHORE 默认THREAD
hystrix.command.default.execution.isolation.strategy=THREAD
# 执行hystrix command的超时时间,超时后会进入fallback方法 默认1000
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=1000
# 执行hystrix command是否限制超时,默认是true
hystrix.command.default.execution.timeout.enabled=true
# hystrix command 执行超时后是否中断 默认true
hystrix.command.default.execution.isolation.thread.interruptOnTimeout=true
# 使用信号量隔离时,信号量大小,默认10
hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests=10
# fallback方法最大并发请求数 默认是10
hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests=10
# 服务降级是否开启,默认为true
hystrix.command.default.fallback.enabled=true
# 是否使用断路器来跟踪健康指标和熔断请求
hystrix.command.default.circuitBreaker.enabled=true
# 熔断器的最小请求数,默认20. (这个不是很理解,欢迎补充)
hystrix.command.default.circuitBreaker.requestVolumeThreshold=20
# 断路器打开后的休眠时间,默认5000
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=5000
# 断路器打开的容错比,默认50
hystrix.command.default.circuitBreaker.errorThresholdPercentage=50
# 强制打开断路器,拒绝所有请求. 默认false, 优先级高于forceClosed
hystrix.command.default.circuitBreaker.forceOpen=false
# 强制关闭断路器,接收所有请求,默认false,优先级低于forceOpen
hystrix.command.default.circuitBreaker.forceClosed=false
# hystrix command 命令执行核心线程数,最大并发 默认10
hystrix.threadpool.default.coreSize=10
- 信息参见:
使用断路保护可有效果的防止系统雪崩,Spring Cloud
对 Hystrix
做了封装,详见: http://cloud.spring.io/spring-cloud-netflix/single/spring-cloud-netflix.html#_circuit_breaker_hystrix_clients
需要说明的是 Hystrix
使用新线程执行代码,导致 Threadlocal
数据不能同步,使用时需要将用到的数据做为参数传入,如果需要使用 Dew
框架的上下文(请求链路/用户等获取)需要先传入再设值,e.g.
Hystrix Command 示例,及Context处理
public class HystrixExampleService {
@HystrixCommand(fallbackMethod = "defaultFallback", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")
})
public String someMethod(Map<String, Object> parameters, DewContext context) {
// !!! Hystrix使用新线程执行代码,导致Threadlocal数据不能同步,
// 使用时需要将用到的数据做为参数传入,如果需要使用Dew框架的上下文需要先传入再设值
DewContext.setContext(context);
try {
Thread.sleep(new Random().nextInt(3000));
logger.info("Normal Service Token:" + Dew.context().getToken());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "ok";
}
// 降级处理方法定义
public String defaultFallback(Map<String, Object> parameters, DewContext context, Throwable e) {
DewContext.setContext(context);
logger.info("Error Service Token:" + Dew.context().getToken());
return "fail";
}
}
定时任务
使用 Spring Config
配置中心 refresh
时,在 @RefreshScope
注解的类中, @Scheduled
注解的自动任务会失效。 建议使用实现 SchedulingConfigurer
接口的方式添加自动任务。
自动任务添加
@Configuration
@EnableScheduling
public class SchedulingConfiguration implements SchedulingConfigurer {
private Logger logger = LoggerFactory.getLogger(SchedulingConfiguration.class);
@Autowired
private ConfigExampleConfig config;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addTriggerTask(() -> logger.info("task1: " + config.getVersion()), triggerContext -> {
Instant instant = Instant.now().plus(5, SECONDS);
return Date.from(instant);
});
taskRegistrar.addTriggerTask(() -> logger.info("task2: " + config.getVersion()), new CronTrigger("1/3 * * * * ?"));
}
}
主要性能影响参数
内置
Tomcat
参数调整效果并不大,如果需要调整,建议适当调大max-treads
和accept-count
# 最大等待请求数 默认100 server.tomcat.accept-count=1000
# 最大并发数 默认200
server.tomcat.max-threads=1000
# 最大连接数 默认BIO:200 NIO:10000 APR:8192
server.tomcat.max-connections=2000Zuul
性能参数说明# 连接池最大连接,默认是200 zuul.host.maxTotalConnections=1000
每个route可用的最大连接数,默认值是20
zuul.host.maxPerRouteConnections=1000
Hystrix最大的并发请求 默认值是100
zuul.semaphore.maxSemaphores=1000
Note | Zuul 的最大并发数主要调整 maxSemaphores 优先级高于 Hystrix 的最大线程数配置. |
---|---|
Ribbon
性能参数说明调整MaxTotalConnections
和MaxConnectionsPerHost
时建议同比调整Pool
相关的参数# ribbon 单主机最大连接数,默认50 ribbon.MaxConnectionsPerHost=500
# ribbon 总连接数,默认 200
ribbon.MaxTotalConnections=1000
# 默认200
ribbon.PoolMaxThreads=1000
# 默认1
ribbon.PoolMinThreads=500
Note | Zuul 和其它使用 Ribbon 的服务一样,TPS主要调整 Ribbon 的 MaxConnectionsPerHost 和 MaxTotalConnections |
---|---|
Hystrix
性能参数说明# 并发执行的最大线程数,默认10 hystrix.threadpool.default.coreSize=100
Note | 普通 Service 使用 Hystrix 时,最大并发主要调整 hystrix.threadpool.default.coreSize |
---|---|
Warning | Hystrix 的默认超时时间为1s,在高并发下容易出现超时,建议将默认超时时间适当调长, 特殊接口需要将时间调短或更长的,使用特定配置,见上面 Feign 配置特定方法超时时间. |
---|---|
Zuul
保护(隐藏)内部服务的 Http
接口
在yml配置文件里配置(ignored-patterns
,ignored-services
)这两项中的一项即可
配置示例
zuul: #配置一项即可!
ignored-patterns: /dew-example/** #排除此路径
ignored-services: dew-example #排除此服务
缓存处理
Spring Cache
提供了很好的注解式缓存,但默认没有超时,需要根据使用的缓存容器特殊配置
Redis缓存过期时间设置
@Bean
RedisCacheManager cacheManager() {
final RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate);
redisCacheManager.setUsePrefix(true);
redisCacheManager.setDefaultExpiration(<过期秒数>);
return redisCacheManager;
}
Spring Boot Admin 监控实践
在 Spring Boot Actuator
中提供很多像 health
、 metrics
等实时监控接口,可以方便我们随时跟踪服务的性能指标。Spring Boot
默认是开放这些接口提供调用的,那么就问题来了,如果这些接口公开在外网中,很容易被不法分子所利用,这肯定不是我们想要的结果。 在这里我们提供一种比较好的解决方案
- 被监控的服务配置
management:
security:
enabled: false # 关闭管理认证
context-path: /management //(1)
eureka:
instance:
metadata-map:
cluster: default (2)
- 管理前缀
集群名称
Zuul
网关配置
zuul:
ignoredPatterns: /*/management/** //(1)
同上文
management.context-path
, 这里之所以不是/management/**
,由于网关存在项目前缀,需要往前一级,大家可以具体场景具体配置
Spring Boot Admin
配置
spring:
application:
name: monitor
boot:
admin:
discovery:
converter:
management-context-path: ${management.context-path}
routes:
endpoints: env,metrics,dump,jolokia,info,configprops,trace,logfile,refresh,flyway,liquibase,heapdump,loggers,auditevents,hystrix.stream,turbine.stream (1)
turbine:
clusters: default (2)
location: ${spring.application.name}
turbine:
instanceUrlSuffix: ${management.context-path}/hystrix.stream
aggregator:
clusterConfig: default (2)
appConfig: monitor-example,hystrix-example (3)
clusterNameExpression: metadata['cluster']
security:
basic:
enabled: false
server:
port: ...
eureka:
instance:
metadata-map:
cluster: default (2)
client:
serviceUrl:
defaultZone: ...
management:
security:
enabled: false
context-path: /management (4)
- 要监控的内容
- 要监控的集群名称
- 添加需要被监控的应用
Service-Id
,以逗号分隔 - 同上文
management.context-path
jdbc 批量插入性能问题
如果不开启rewriteBatchedStatements=true,那么jdbc会把批量插入当做一行行的单条处理,也就没有达到批量插入的效果
jdbc配置示例
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/dew?useUnicode=true&characterEncoding=utf-8&rewriteBatchedStatements=true
username: root
password: 123456
- 对于一张七列的表,插入1500条数据,分别对mybatis和jdbctemplate进行测试,记录三次数据如下,可以看到,该配置对于jdbctemplate影响是极大的,而对于mybatis影响却不大,后续有时间再继续深入了解
Table 1. 测试数据
rewriteBatchedStatements | mybatis(ms) | jdbctemplate(ms) | dew(ms) |
---|---|---|---|
true | 401 | 88 | 174 |
true | 427 | 78 | 167 |
true | 422 | 75 | 176 |
false | 428 | 1967 | 2065 |
false | 410 | 2641 | 2744 |
false | 369 | 2299 | 2398 |
http请求并发数性能瓶颈
- 当策略为Thread时(默认是Thread),hystrix.threadpool.default.maximumSize为第一个性能瓶颈,默认值为10.
Tip | 修改值时,需要先设置hystrix.threadpool.default.allowMaximumSizeToDivergeFromCoreSize为true,默认为false |
---|---|
hystrix详细配置参见https://github.com/Netflix/Hystrix/wiki/configuration#allowMaximumSizeToDivergeFromCoreSize
- 第二个瓶颈为springboot内置的tomcat的最大连接数,参数为server.tomcat.maxThreads,默认值为200
日志中解析message,动态显示property
- 在启动类的main方法中注册converter,如下
PatternLayout.defaultConverterMap.put("dew", TestConverter.class.getName());
Note | 这个是解析%dew的内容 |
---|---|
- 自定义Converter继承DynamicConverter
,解析message,获取有效信息并返回解析后得到的字符串。
Converter代码示例
public class TestConverter extends DynamicConverter<ILoggingEvent> {
@Override
public String convert(ILoggingEvent event) {
// 这里未做解析,示例代码
return event.getMessage();
}
}