微服务开发

本次最佳实践主要对于传统开发的劣势及存在问题进行分析,及描述微服务开发带来的优势,指导传统服务开发到微服务的改造。也将详细描述微服务开发设计过程,对不同的层面进行字面和代码示例方式指导微服务开发。

最佳实践背景

对于软件来说,有一套开发规约绝不是消灭代码内容的创造性、优雅性,而是限制过度个性化,推行相对标准化,以一种普遍认可的方式做事。

在过去的几年里,大多数团队都会使用微服务架构来构建产品,他们使用微服务架构的意图都是正确的:更快的开发速度、更好的可扩展性、更小的独立团队、独立的部署、使用合适的技术来完成工作等等。但大多数时候,团队在使用微服务时都很不顺利。

由于历史隔阂与业务风格差异,导致工程结构差别很大,代码风格迥异,规范不一,沟通成本大,合作效率低,维护成本高。因此,应该需要专业化的迭代式、集约式发展,而不是动辄重复造论,真正专业化的团队一定会有统一的开发规约,这代表效率、共鸣、情怀和可持续。

最佳实践价值

本次最佳实践主要目标:

  • 码出高效:提升代码开发效率,提升沟通效率和研发效能。

  • 码出质量:防患于未然,提升质量意识和系统可维护性,降低故障率。

  • 码出情怀:工匠精神,追求极致的卓越精神,打磨精品代码。

微服务项目开发分析

传统应用程序成为了一个庞大、复杂的单体,开发组织可能会陷入了一个痛苦的境地,敏捷开发和交付的任何一次尝试都将原地徘徊。最终正确修复bug和实现新功能变得非常困难而耗时。此外,这种趋势就像是往下的螺旋。如果基本代码都令人难以理解,那么改变也不会变得正确,最终得到的将是一个巨大且不可思议的大泥球。

目前传统IT企业拥有大量的单体应用,不能快捷方便的管理应用,如果服务器停机,由于需要手工流程所以需要较长的时间来恢复,很难通过增加新的实例来进行横向扩展。而新的应用开始变得越来灵活,开发越来越迅捷,不停的敏捷迭代适应快速变化的市场,同时应用本身必须要保证服务的稳定性,可扩展性,增强用户体验。

为了适应新形势下的发展潮流,单个应用微服务架构改造,让系统的应用开发、部署、运维模式发生改变,最终应当实现在有限信息资源下应用建设效率提升、应用系统性能提升、应用架构水平整体提升,从而完成企业转型的关键战略升级。

微服务项目架构设计

微服务开发设计

应用分层:规范构建项目,便于后续开发及维护。

父子项目抽取:统一管理依赖,便于维护。

公共子模块抽取:共性代码提取并提供规范使用,便于后续运维和开发。

代码生成器:通用三层代码生成,方便快速开发项目。

日志封装:使用AOP统一日志处理,解耦控制层。

异常处理:简化代码,避免异常的遗漏及断言判断封装。

通用接口返回JSON封装:统一规范返回数据结构。

Model工厂模式设计:统一规范实体创建。

公共配置文件设计:简化配置,方便管理配置文件。

接口健康检查:提供服务健康检查接口。

动态多数据源设计:动态数据源,支持多主多从、纯粹多库、混合配置。

Swagger2接口文档:统一接口管理的动态界面化框架。

标准化RESTFUL风格接口:一套成熟的互联网API设计理论,标准化设计接口。

分布式锁设计:解决定时任务在多实例环境下重复执行以及表单重复提交。

Logback日志设计:统一日志打印及格式。

应用分层

应用分层须遵循:

  • 方便后续代码进行维护扩展。

  • 分层的效果让整个团队接受。

  • 各个层职责边界清晰。

应用分层图:应用分层图

  • 开发接口层:可直接封装Service方法暴露成RPC接口;通过Web封装成http接口;进行网关安全控制、流量控制等。

  • 终端显示层:各个端的模板渲染并执行显示的层。当前主要是velocity渲染,JS渲染,JSP渲染,移动端展示等。

  • 请求处理层(Web层):主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。

  • 业务逻辑层(Service层):相对具体的业务逻辑服务层。

  • 通用处理侧(Manager层):通用业务处理层,它有如下特征:

    • 对第三方平台封装的层,预处理返回结果及转化异常信息。

    • 对Service层通用能力的下沉,如缓存方案、中间件通用处理。

    • 与DAO层交互,对多个DAO的组合复用。

  • 数据持久层(DAO层):数据访问层,与底层MySQL、Oracle、Hbase等进行数据交互。

  • 外部接口或第三方平台:包括其它部门RPC开放接口,基础平台,其它公司的HTTP接口。

父子项目抽取

在微服务开发过程中,随业务的不断扩展微服务个数会随之增加。大规模的微服务组成了一个复杂的项目,而多模块开发在每个模块都对应着一个pom.xml。它们之间通过继承和聚合而相互关联,如不加以管理,随之依赖会杂乱冗余,父级项目目的就在于此。

父子项目中pom.xml编写规范

  1. Properties:统一管理所有JAR包版本,通过定义全局变量,在POM中依赖包通过${property_name}的形式引用变量的值。下面提供代码以供参考:

    <properties>
    	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    	<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    	<java.version>1.8</java.version>
    	<spring-cloud.version>Finchley.SR1</spring-cloud.version>
    </properties>
    
  2. Dependency Management:所有JAR包依赖管理,只是声明依赖,并不实现引入,因此子项目需要显示的声明需要用的依赖。如果不在子项目中声明依赖,是不会从父项目中继承下来的;只有在子项目中写了该依赖项,并且没有指定具体版本,才会从父项目中继承该项,并且version和scope都读取自父pom 另外如果子项目中指定了版本号,那么会使用子项目中指定的jar版本。下面提供代码以供参考:

    <dependencyManagement>
    	<dependencies>
    		<dependency>
    			<groupId>org.springframework.cloud</groupId>
    			<artifactId>spring-cloud-dependencies</artifactId>
    			<version>${spring-cloud.version}</version>
    			<type>pom</type>
    			<scope>import</scope>
    		</dependency>
    		<dependency>
    			<groupId>com.alibaba.cloud</groupId>
    			<artifactId>spring-cloud-alibaba-dependencies</artifactId>
    			<version>2.0.0.RELEASE</version>
    			<type>pom</type>
    			<scope>import</scope>
    		</dependency>
    	</dependencies>
    </dependencyManagement>
    
  3. Plugin Management:打包插件管理,与dependencyManagement非常类似,PluginManagement下的plugins下的plugin则仅仅是一种声明,子项目中可以PluginManagement下的plugin进行信息的选择、继承、覆盖等。下面提供代码以供参考:

    <pluginManagement>
      <plugins>
    	<plugin>
    	  <groupId>org.springframework.boot</groupId>
    	  <artifactId>spring-boot-maven-plugin</artifactId>
    	  <version>2.2.0.RELEASE</version>
    	</plugin>
      </plugins>
    </pluginManagement>
    

公共子模块抽取

微服务的思想:有多个服务,把一个项目拆分成多个独立的服务,多个服务是独立运行的,每个服务占用独立的进程。 所以能拆即拆,模块化开发的思想设计。

在实际开发过程中,服务会分为很多个模块,但是有些实体类或接口会在很多模块使用,这样可以将其单独放在一个模块中,其他模块要使用的时候,直接调用,可以大幅简化了=开发配置,以及提高开发效率。

公共子模块

共性代码提取并提供规范使用,便于后续运维和开发,示例图如下:共性代码提取

在其它需要使用,在pom.xml的dependencies下引入公共模块即可:

<dependency>
	<groupId>com.poc.springcloud</groupId>
	<artifactId>microservicecloud-api</artifactId>
	<version>${project.version}</version>
</dependency>

代码生成器

在平时开发过程中,常有机械的、重复的代码编写,因此自动完成这些重复性内容来节省时间,节省代码,保持理性。代码生成器用来生成有规律的代码,如dao、modelEntity、mapper、mapper.xml等。

代码生成器示例代码如下:

public class CodeGenerator {
	public static void generator(String moduleName, String author, String packageParent, String tablePrefix,
	String[] include, String driverName, String username, String password, String url) {
		//代码生成器
		AutoGenerator mpg = new AutoGenerator().setGlobalConfig();
	}
}

public class CodeGeneratorTest {
	public static void main(String[] args) {
		CodeGenerator.generator("micro-demo", "lhh", "com.micro.demo", "sin_", new String[]{"sin_test"}, "oracle.jdbc.OracleDriver", "****", "****",
		"jdbc:oracle:thin****");
	}
}

日志封装

在开发过程中,随处可见日志的处理,为提高开发效率及规范日志处理,非常有必要提供全局日志处理。

不建议使用System.out,因为大量的使用会增加资源的消耗。使用System.out是在当前线程执行的,写入文件也是写入完毕之后才继续执行下面的程序。而使用Log工具不但可以控制日志是否输出,怎么输出,它的处理机制也是通知写日志,继续执行后面的代码不必等日志写完。

下面介绍微服务开发过程中最常用日志打印方式,使用slf4j+logback实现日志记录:

  1. 在已构建好的maven工程中的pom.xml引入下面依赖。

    <dependency>
    	<groupId>org.projectlombok</groupId>
    	<artifactId>lombok</artifactId>
    	<optional>true</optional>
    </dependency>
    
  2. 在Maven工程resources目录下新建logback-spring.xml文件。

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration scan="true" scanPeriod="60 seconds" debug="false">
        <contextName>logs</contextName>
        <!--  日志位置  -->
        <property name="log.path" value="log" />
        <!--  日志保留时长  -->
        <property name="log.maxHistory" value="15" />
        <property name="log.colorPattern" value="%magenta(%d{yyyy-MM-dd HH:mm:ss}) %highlight(%-5level) %yellow(%thread) %green(%logger) %msg%n"/>
        <property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} %-5level %thread %logger %msg%n"/>
    
        <!--输出到控制台-->
        <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>${log.colorPattern}</pattern>
            </encoder>
        </appender>
    
        <!--输出到文件-->
        <appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <fileNamePattern>${log.path}/info/info.%d{yyyy-MM-dd}.log</fileNamePattern>
                <MaxHistory>${log.maxHistory}</MaxHistory>
            </rollingPolicy>
            <encoder>
                <pattern>${log.pattern}</pattern>
            </encoder>
            <filter class="ch.qos.logback.classic.filter.LevelFilter">
                <level>INFO</level>
                <onMatch>ACCEPT</onMatch>
                <onMismatch>DENY</onMismatch>
            </filter>
        </appender>
    
        <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <fileNamePattern>${log.path}/error/error.%d{yyyy-MM-dd}.log</fileNamePattern>
            </rollingPolicy>
            <encoder>
                <pattern>${log.pattern}</pattern>
            </encoder>
            <filter class="ch.qos.logback.classic.filter.LevelFilter">
                <level>ERROR</level>
                <onMatch>ACCEPT</onMatch>
                <onMismatch>DENY</onMismatch>
            </filter>
        </appender>
    
        <!--  日志类型为debug时,输出到控制台  -->
        <root level="debug">
            <appender-ref ref="console" />
        </root>
        <!--  日志类型为info时,输出到配置好的文件  -->
        <root level="info">
            <appender-ref ref="file_info" />
            <appender-ref ref="file_error" />
        </root>
    </configuration>
    
  3. 记录日志,使用slf4j注解,然后调用log方法就可以直接生成日志文件。

    package springboot.demo.log.controller;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * @author Administrator on 2019/10/26.
     * @version 1.0
     */
    @Slf4j
    @RestController
    public class TestController {
        @GetMapping("/index")
        public void index() {
            log.info("我记录日志了");
        }
    }
    

异常处理

在项目的开发中,不管是对底层的数据库操作、业务层的处理过程、控制层的处理过程,都不可避免会遇到各种可预知的、不可预知的异常需要处理。每个过程都单独处理异常,系统的代码耦合度高,工作量大且不好统一,维护的工作量也很大。

解耦异常处理,这样既保证了相关处理过程的功能较单一,也实现了异常信息的统一处理和维护。

统一异常处理示例代码如下:

public class SinopeHandlerExceptionResolver extends AbstractHandlerExceptionResolver {

	private static final ModelAndView MODEL_VIEW_INSTANCE = new ModelAndView();
	
	protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
		if (ex instanceof ApiException) {
			handleApi((ApiException) ex, request, response);
		} else if (ex instanceof HttpRequestMethodNotSupportException) {
			handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportException) ex, request, response);
		} else if (ex instanceof HttpMediaTypeNotSupportException) {
			handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportException) ex, request, response);
		} else if (ex instanceof HttpMediaTypeNotAcceptableException) {
			handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, request, response);
		} else if (ex instanceof MissingPathVariableException) {
			handleMissingPathVariable((MissingPathVariableException) ex, request, response);
		} else if (ex instanceof MissingServletRequestParameterException) {
			handleMissingServletRequestParameter((MissingServletRequestParameterException) ex, request, response);
		} else if (ex instanceof ServletRequestBindingException) {
			handleServletRequestBindingException((ServletRequestBindingException) ex, request, response);
		} else if (ex instanceof ConversionNotSupportedException) {
			handleConversionNotSupported((ConversionNotSupportedException) ex, request, response);
		}
	}
}

public void handleApi(ApiException ex, HttpServletRequest request, HttpServletResponse response) {
	ResponseUtil.sendFail(request, response, ex.getErrorCode());
}

通用接口返回JSON封装

在项目开发中,如果将每个接口都要封装的方式,直接封装成统一格式,就可以避免不同人开发,返回格式不统一的问题,而灵活快速又易懂的返回数据是非常关键的。

JSON封装示例代码如下:

public class SuccessResponse<T> extends ApiResponse<T> {

	private Integer status;
	
	private T result;
}

public class FailedResponse extends ApiResponse {
	
	private Integer status;
	
	private String error;
	
	private String msg;
	
	private String exception;
	
	private LocalDateTime time;
}

返回JSON报文示例:

public static ApiResponse<Void> success(HttpServletResponse response, HttpStatus status) {
	response.setStatus(status.value());
	return SuccessResponse.builder().status(status.value()).build();
}

public static <T> FailedResponse failure(ErrorCode errorCode, Exception exception) {
	
	return ResponseUtil.exception(FailedResponse.builder().msg(errorCode.getMsg()), exception).error(errorCode.getError())
	.show(errorCode.isShow()).time(LocalDateTime.now()).status(errorCode.getHttpCode).build();
}

GetMapping("/{id}")
public ApiResponse<User> get(@PathVariable("id") Long id) {
	
	User user = userService.getById(id);
	ApiAssert.notNull(ErrorCodeEnum.USER_NOT_FOUNT, user);
	return success(user);
}

客户端超时配置

  1. API形式配置HSF服务,配置HSFApiConsumerBean的clientTimeout属性,单位是ms,我们把接口的超时配置为1000ms,方法queryOrder配置为100ms,代码如下:

    HSFApiConsumerBean consumerBean = new HSFApiConsumerBean();
    //接口级别超时配置
    consumerBean.setClientTimeout(1000);
    //xxx
    MethodSpecial methodSpecial = new MethodSpecial();
    methodSpecial.setMethodName("queryOrder");
    //方法级别超时配置,优先于接口超时配置
    methodSpecial.setClientTimeout(100);
    consumerBean.setMethodSpecials(new MethodSpecial[]{methodSpecial});
    
  2. Spring配置HSF服务,上述例子中的API配置等同于如下XML配置:

    <bean id="CallHelloWorld" class="com.taobao.hsf.app.spring.util.HSFSpringConsumerBean">
        ...
        <property name="clientTimeout" value="1000" />
        <property name="methodSpecials">
          <list>
            <bean class="com.taobao.hsf.model.metadata.MethodSpecial">
              <property name="methodName" value="queryOrder" />
              <property name="clientTimeout" value="100" />
            </bean>
          </list>
        </property>
        ...
    </bean>
    

服务端超时配置

  1. API形式配置HSF服务,配置HSFApiProviderBean的clientTimeout属性,单位是ms,代码如下:

    HSFApiProviderBean providerBean = new HSFApiProviderBean();
    //接口级别超时配置
    providerBean.setClientTimeout(1000);
    //xxx
    MethodSpecial methodSpecial = new MethodSpecial();
    methodSpecial.setMethodName("queryOrder");
    //方法级别超时配置,优先于接口超时配置
    methodSpecial.setClientTimeout(100);
    providerBean.setMethodSpecials(new MethodSpecial[]{methodSpecial});
    
  2. Spring配置HSF服务,上述例子中的API配置等同于如下XML配置:

    <bean class="com.taobao.hsf.app.spring.util.HSFSpringProviderBean" init-method="init">
        ...
        <property name="clientTimeout" value="1000" />
        <property name="methodSpecials">
          <list>
            <bean class="com.taobao.hsf.model.metadata.MethodSpecial">
              <property name="methodName" value="queryOrder" />
              <property name="clientTimeout" value="2000" />
            </bean>
          </list>
        </property>
        ...
    </bean>
    

Model工厂模式设计

创建使用工厂模式的目的在于,在每新创建一个实体时,就要去修改匹配的构造函数,而修改代码时非常谨慎的,为避免准备构造方法的参数以及new对象(new对象其实也是一种硬编码的表现),所以就需要引入工厂方法模式。

示例代码如下:

public class EntityFactoryBean implements FactoryBean<BaseEntity> {
	
	@Nullable
	private static BaseEntity baseEntity;
	
	public static BaseEntity getInstance(Class<? extends BaseEntity> clazz) {
		try {
			baseEntity = (BaseEntity)Class.forName(clazz.getName()).newInstance();
		} catch(Exception e) {
			log.error("EntityFactoryBean create Entity failed:" + e.getMessage(), e);
			e.printStackTrace();
		}
		return baseEntity;
	}
}

@GetMapping("/{id}")
public ApiResponse<Void> update(@PathVariable("id") Long id, @RequestBody @Validated(UserParm.Update.class) UserParm userPARM) {
	
	User user = (User)EntityFactoryBean.getInstance(User.class);
	BeanUtils.copyProperties(userPARM, user);
	user.setUserId(id);
	userService.updateById(user);
	return success();
}

公共配置文件设计

把公共的配置文件独立出来,方便统一管理,项目部署,避免在使用时因为配置文件杂乱无章而导致的错误引用。

公共配置文件application-common.yaml抽取:

#mybatis-plus配置:
mybatis-plus:
    mapper-locations: classpath:dao/*Mapper.xml
	global-config:
		banner: false
		supper-mapper-class: com.micro.common.base.BaseMapper

spring:
	profiles:
	  include: common
	  active:

接口健康检查

在每一个服务中都应该提供一个健康检查接口,此接口须执行简单的数据查询,用来提供给Kubernetes做自动服务健康检查。

示例代码如下:

@RestController
@RequestMapping(value = "/health_check", produces = "applicatioin/json")
public class HealthCheckController extends BaseController {
	
	@Autowired
	private IUserService userService;
	
	public ApiResponse<User> get(@PathVariable("id") Long id) {
		User user = userService.getById(id);
		ApiAssert.notNull(ErrorCodeEnum.USER_NOT_ROUNT, user);
		return success(user);
	}
}

动态多数据源设计

随着业务的发展,数据库压力的增大,如何分割数据库的读写压力时我们需要考虑的问题,而能够动态的切换数据源就是我们的首要目标。

在原有项目里使用多数据源时显得非常冗余,为此封装多数据源的使用非常关键,这里使用注解和面向切面来实现动态的数据源切换。

注:在使用注解切换数据源时,把@DS用到Mapper接口层上。

项目引入JAR依赖:

<dependency>
	<groupId>com.baomidu</groupId>
	<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
</denpendency>

spring: 
    datasorce: 
	  dynamic: 
	    hikari: 
		  connection-timeout: 30000
		  idle-timeout: 600000
		  max-lifetime: 1800000
		  max-pool-size: 20
		primary: sinopec
		datasorce: 
		  sinopec:
			username: ****
			password: ****
			driver-class-name: com.mysql.cj.jdbc.Driver
			url: ****
		  hypergraph:
		    username: ****
			password: ****
			url: ****
			driver-class-name: oracle.jdbc.OracleDriver

@DS("hypergraph")			
public interface TestMapper extends BaseMapper<Test> {

}

Swagger2接口文档

在各个细分组织人员共同完成项目,共同完成产品的全周期工作。如何进行组织架构内的有效高效沟通就显得尤其重要。其中,如何构建一份合理高效的接口文档更显重要。

而Swagger的出现可以完美解决以上传统接口管理方式存在的痛点,动态界面化接口管理。

项目引入JAR包依赖:

<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-swagger-ui</artifactId>
	<version>2.6.1</version>
</dependency>
<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-swagger2</artifactId>
	<version>2.6.1</version>
</dependency>
获取Swagger ApiInfo:
@Configuration
@ComponentScan(basePackages = { "com.aliware.edas.controller" })
@EnableSwagger2
public class SwaggerConfiguration {

    @Bean
    public Docket swaggerSpringfoxDocket() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .paths(Predicates.or( //这里添加你需要展示的接口
                        PathSelectors.ant("/poc/**")
                        )
                )
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("POC功能API接口测试文档")
                .description("description")
                .contact("liuhuihui")
                .version("1.0")
                .build();
    }
}
代码中通过注解使用Swagger功能:
@RestController
@RequestMapping(value = "/poc")
@RefreshScope
@Api(description = "Spring Cloud Alibaba-POC功能测试接口")
public class ConsumerController {

	@Value("${useLocalCache:false}")
    private String useLocalCache;

    InetAddress ip;
	
	@ApiOperation(value = "全局参数配置")
    @RequestMapping(value = "/echo-acm", method = RequestMethod.GET)
    public String echoAcm() {
        return "配置文件中的value值:" + useLocalCache + "\r\n";
    }

    @ApiOperation(value = "获取版本信息")
    @RequestMapping(value = "/echo-version", method = RequestMethod.GET)
    public String getVersion() throws UnknownHostException {
        ip = InetAddress.getLocalHost();
        String localName=ip.getHostName();
        String localIp = ip.getHostAddress();
        System.out.println("主机名称:" + localName + "ip地址:" + localIp);
        return "This version is V1" + "\r\n";
    }
}

标准化RESTFUL风格接口

网络应用程序,分为前端和后端两个部分。当前的发展趋势,就是前端设备层出不穷。因此,必须有一种统一的机制,方便不同的前端设备与后端进行通信。RESTful API是目前比较成熟的一套互联网应用程序的API设计理论。

常用HTTP动词及使用例子:

GET(SELECT):从服务器取出资源(一项或多项)。

POST(CREATE):在服务器新建一个资源。

PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。

PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。

DELETE(DELETE):从服务器删除资源。

分布式锁设计

现在大多数应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。保证分布式部署的应用集群具有以下特点:

  • 同一个方法在同一时间只能被一台机器的一个线程执行。

  • 使用重入锁(避免死锁)。

  • 高可用的获取锁和释放锁功能。

  • 获取锁和释放锁的性能要好。

因此采用高可用的Redis来实现分布式锁是可行的。Demo的分布式锁主要是防此多实例定时任务重复执行及表单重复提交。

示例代码如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public interface CacheLock {
	
	String prefix() default "";
	
	int expire() default 5;
	
	TimeUnit timeUnit() default TimeUnit.SECONDS: String delimiter() default ":";
}

@Target(ElementType.PARAMETER, ElementType.METHOD, ElementType.FIBLD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public interface CacheParm {

	String name() default "";
}

@Aspect
@Configuration
public class LockMethodAspect {
	
	private RedisLockHelper redisLockHelper;
	
	@Around("execution(public * *(..) && @annotation(com.micro.demo.common.annotation.CacheLock)"))
	public Object interceptor(ProceedingJoinPoint pjp) {
		MethodSignature signature = (MethodSignature)pjp.getSignature();
		Method method = signature.getMethod();
		CacheLock lock = method.getAnnotation(CacheLock.class);
		if (StringUtils.isEmpty(lock.prefix())) {
			throw new RuntimeException("lock key don't null...");
		}
		final String lockKey = AbstractLockKeyGenerator.getLock(pjp);
		String value = UUID.randomUUID().toString();
		try {
			final boolean = redisLockHelper.lock(lockKey.value, lock.expire(), lock.timeUnit());
			if (!success) {
				throw new RuntimeException("重复提交");
			}
			try {
				return pjp.proceed();
			} catch(Throwable thowable) {
				throw new RuntimeException("系统异常");
			}
		} finally {
			redisLockHelper.unlock(lockKey, value);
		}
	}
	
}

Logback日志设计

相比其它日志框架logback配置更加简单和效率,项目中日志记录的完整性能够帮助我们更好的分析,解决线上出现的各种问题,方便问题的快速定位。项目中用到日志的几个场景:记录后台的SQL输出,记录主要业务的执行,报警系统需要对不同的日志进行监控,需要做日志的分离等。

日志输出配置文件代码如下:

<?xml version="1.0
" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <property name="LOG_FILE_NAME_PATTERN" value="logs/auth.%d{yyyy-MM-dd}.%i.log"/>
    <!-- 日志格式 -->
    <property name="CONSOLE_LOG_PATTERN"
              value="%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%c){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
    <property name="FILE_LOG_PATTERN"
              value="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %c : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
    <!--输出到控制台-->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
        </encoder>
    </appender>
    <!--输出到文件-->
    <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_FILE_NAME_PATTERN}</fileNamePattern>
            <!-- 日志保留天数 -->
            <maxHistory>366</maxHistory>
            <!-- 日志文件上限大小,达到指定大小后删除旧的日志文件 -->
            <totalSizeCap>2GB</totalSizeCap>
            <!-- 每个日志文件的最大值 -->
            <timeBasedFileNamingAndTriggeringPolicy
                    class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>10MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
    </appender>
    <!-- (多环境配置日志级别)根据不同的环境设置不同的日志输出级别 -->
    <springProfile name="default,local">
        <root level="info">
            <appender-ref ref="console"/>
        </root>
        <logger name="com.zhl" level="debug"/>
    </springProfile>
    <springProfile name="dev,test">
        <root level="info">
            <appender-ref ref="console"/>
            <appender-ref ref="file"/>
        </root>
        <logger name="com.zhl" level="debug"/>
    </springProfile>
    <springProfile name="product,pre">
        <root level="info">
            <appender-ref ref="console"/>
            <appender-ref ref="file"/>
        </root>
        <logger name="com.zhl" level="debug"/>
    </springProfile>
</configuration>

不同日志信息输出到不同文件:

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <!-- 应用名称-->
    <property name="appName" value="zhlrm-ppt-service"/>
    <!-- 日志的存放目录-->
    <!-- debug-->
    <property name="DEBUG_LOG_FILE_NAME_PATTERN" value="logs/${appName}-debug.%d{yyyy-MM-dd}.%i.log"/>
    <property name="INFO_LOG_FILE_NAME_PATTERN" value="logs/${appName}-info.%d{yyyy-MM-dd}.%i.log"/>
    <property name="WARN_LOG_FILE_NAME_PATTERN" value="errlogs/${appName}-warn.%d{yyyy-MM-dd}.%i.log"/>
    <property name="ERROR_LOG_FILE_NAME_PATTERN" value="errlogs/${appName}-error.%d{yyyy-MM-dd}.%i.log"/>
    <!-- 日志格式 -->
    <property name="CONSOLE_LOG_PATTERN"
              value="%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%c){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
    <property name="FILE_LOG_PATTERN"
              value="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %c : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
    <!--输出到控制台-->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
        </encoder>
    </appender>
    <!--输出到DEBUG文件-->
    <appender name="debug_file" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
   <fileNamePattern>${DEBUG_LOG_FILE_NAME_PATTERN}</fileNamePattern>
            <!-- 日志保留天数 -->
            <maxHistory>30</maxHistory>
            <!-- 日志文件上限大小,达到指定大小后删除旧的日志文件 -->
            <totalSizeCap>2GB</totalSizeCap>
            <!-- 每个日志文件的最大值 -->
            <timeBasedFileNamingAndTriggeringPolicy
                 class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>50MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
        <!-- 此日志文件只记录debug级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>debug</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!--输出到INFO文件-->
    <appender name="info_file" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
     <fileNamePattern>${INFO_LOG_FILE_NAME_PATTERN}</fileNamePattern>
            <!-- 日志保留天数 -->
            <maxHistory>7</maxHistory>
            <!-- 日志文件上限大小,达到指定大小后删除旧的日志文件 -->
            <totalSizeCap>1GB</totalSizeCap>
            <!-- 每个日志文件的最大值 -->
            <timeBasedFileNamingAndTriggeringPolicy
                 class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>50MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
        <!-- 此日志文件只记录info级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>info</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!--输出到WARN文件-->
    <appender name="warn_file" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
     <fileNamePattern>${WARN_LOG_FILE_NAME_PATTERN}</fileNamePattern>
            <!-- 日志保留天数 -->
            <maxHistory>30</maxHistory>
            <!-- 日志文件上限大小,达到指定大小后删除旧的日志文件 -->
            <totalSizeCap>1GB</totalSizeCap>
            <!-- 每个日志文件的最大值 -->
            <timeBasedFileNamingAndTriggeringPolicy
                    class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>10MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
        <!-- 此日志文件只记录warn级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>warn</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!--输出到ERROR文件-->
    <appender name="error_file" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
   <fileNamePattern>${ERROR_LOG_FILE_NAME_PATTERN}</fileNamePattern>
            <!-- 日志保留天数 -->
            <maxHistory>30</maxHistory>
            <!-- 日志文件上限大小,达到指定大小后删除旧的日志文件 -->
            <totalSizeCap>1GB</totalSizeCap>
            <!-- 每个日志文件的最大值 -->
            <timeBasedFileNamingAndTriggeringPolicy
                    class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>10MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
        <!-- 此日志文件只记录error级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>error</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!-- region 根据不同的环境设置不同的日志输出级别 -->
    <springProfile name="default,local,dev">
        <root level="info">
            <appender-ref ref="console"/>
        </root>
        <logger name="com.zhl.rm" level="debug"/>
    </springProfile>
    <springProfile name="prod,pre,test">
        <root level="info">
            <appender-ref ref="console"/>
            <appender-ref ref="debug_file"/>
            <appender-ref ref="info_file"/>
            <appender-ref ref="warn_file"/>
            <appender-ref ref="error_file"/>
        </root>
        <logger name="com.zhl.rm" level="debug"/>
    </springProfile>
    <!-- endregion -->
</configuration>

微服务开发选用SpringBoot

Spring Boot 是一个真正的游戏改变者。Spring Boot是一个构建在Spring 框架顶部的项目,它提供了一种更简单、更快捷的方式来设置、配置和运行简单的基于Web的应用程序。

在过去Spring框架中,我们需要为应用配置所有的内容,会有许多配置文件,例如XML或元注释,这是Spring Boot解决的主要问题之一,基本无需XML配置了,都使用@注释。

Spring boot巧妙地根据我们选择的依赖配置,可以自动启动我们想要的所有功能,并且只需单击一下即可启动应用程序。此外,它还简化了应用程序的部署过程。

Spring Boot还有很多功能特点:

  • 自动配置:在启动时检测到框架将自动配置,如JDBC。

  • Starter:帮助项目启动,自动添加启动项目依赖项。

  • 实现微服务:提供REST风格API暴露微服务与客户端交互。

  • 自定义配置:通过配置文件来实现各个组件的基本配置。

  • 模块化:一个Spring Boot应用是一个微服务,相当于一个模块,多模块开发后使用Spring Cloud实现动态访问与监控再使用分布式运行,累计扩大规模。

  • 独立打包:一个Spring Boot应用独立设计,生产级质量的应用,通过简单的配置和部署嵌入式Web服务器。

  • 内嵌服务器:默认是Tomcat,可支持Jetty、undertow。

  • Spring Cloud基础:Spring Cloud是实现分布式微服务的组件,其基础就是多个Spring Boot 微服务,如服务发现就是嵌入了Eureka 组件的Spring Boot。