前言 在上个月的25号,我成功搭建了Nexus,在接下来的一个月中,我做了这些事情:
封装各个starter的jar包,上传至Nexus
拆分单体项目为多个微服务
引入ProtoBuf,多个微服务之间采用grpc通信
rancher创建项目,构建测试环境、生产环境
测试环境回归测试,生产环境回归测试
至此,我成功将单体服务拆分,共耗时一个月的时间,代码逻辑没有变化,拆分工作比较费心,但万事开头难,后面的开发会更加简单和高效,下面,我将详细论述这些操作的难点和细节,偏简单口语点,勿对号入座直接照搬。目前暂未考虑开源,仅提供一些思路,取之精华,弃之糟粕
starter leopold-spring-boot-starter 我的项目都是基于SpringBoot的,因此一定需要一个starter来封装有关SpringBoot的内容,而且微服务之间的通讯基本是grpc,所以也会涵盖grpc的内容
pom.xml
内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.7.9</version > <relativePath /> </parent > <groupId > com.leopold</groupId > <artifactId > leopold-spring-boot-starter</artifactId > <version > 1.0.3-SNAPSHOT</version > <properties > <java.version > 17</java.version > </properties > <dependencies > <dependency > <groupId > net.devh</groupId > <artifactId > grpc-server-spring-boot-starter</artifactId > <version > 2.14.0.RELEASE</version > </dependency > <dependency > <groupId > net.devh</groupId > <artifactId > grpc-client-spring-boot-starter</artifactId > <version > 2.14.0.RELEASE</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-properties-migrator</artifactId > <scope > runtime</scope > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-configuration-processor</artifactId > <optional > true</optional > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-aop</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-thymeleaf</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.13.2</version > </dependency > <dependency > <groupId > com.leopold</groupId > <artifactId > leopold-common</artifactId > <version > 1.0.6-SNAPSHOT</version > </dependency > </dependencies > <build > <finalName > ${project.artifactId}</finalName > <resources > <resource > <directory > src\main\resources\lib</directory > <targetPath > BOOT-INF\lib</targetPath > <includes > <include > **/*.jar</include > </includes > </resource > <resource > <directory > src/main/resources</directory > </resource > </resources > </build > </project >
不难发现,基本上都是一些基础的SpringBoot依赖和Grpc依赖,这里的leopold-common
是我平常爱用的工具类,因此单独封装的一个jar包,因为目前都是Java项目,所以基本上都会用得到,就直接引在这个starter里了,所以他的scope
没有配置
此外,我会编写一些配置类,来作为通用配置,包含:
MVC相关所涉及的序列化、RFC配置、MVC配置以及Filter
一些自定义注解,用于MVC中AOP方面的一些操作
可以看到,基本上都是MVC相关的配置,对于GRPC的Fiter等一系列配置,在这里并没有设置,原因是我的大部分微服务端口不会对外发布,节点通讯依靠wireguard,除了考虑qos的影响外目前还不用考虑其他因素,如果需要对外发布,则应该配置grpc相关的filter更为妥当
leopold-jpa-starter 在原先的单体服务中,是用到了jpa相关内容,所以我需要单独封装一个starter,它的作用就是,只要引了这个jar包,就能使用jpa相关的功能
抽象 pom.xml
内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.7.9</version > <relativePath /> </parent > <groupId > com.leopold</groupId > <artifactId > leopold-jpa-starter</artifactId > <version > 1.0.4-SNAPSHOT</version > <name > leopold-jpa-starter</name > <description > leopold-jpa-starter</description > <properties > <maven.compiler.source > 17</maven.compiler.source > <maven.compiler.target > 17</maven.compiler.target > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > </properties > <dependencies > <dependency > <groupId > com.leopold</groupId > <artifactId > leopold-spring-boot-starter</artifactId > <version > 1.0.3-SNAPSHOT</version > <scope > provided</scope > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-jpa</artifactId > </dependency > <dependency > <groupId > com.querydsl</groupId > <artifactId > querydsl-apt</artifactId > <version > 5.0.0</version > <scope > provided</scope > </dependency > <dependency > <groupId > com.querydsl</groupId > <artifactId > querydsl-jpa</artifactId > <version > 5.0.0</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 8.0.32</version > <scope > runtime</scope > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-jdbc</artifactId > </dependency > </dependencies > <build > <finalName > ${project.artifactId}</finalName > <resources > <resource > <directory > src\main\resources\lib</directory > <targetPath > BOOT-INF\lib</targetPath > <includes > <include > **/*.jar</include > </includes > </resource > <resource > <directory > src/main/resources</directory > </resource > </resources > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <version > 3.8.1</version > <configuration > <source > 17</source > <target > 17</target > </configuration > </plugin > </plugins > </build > </project >
可以看到,我引入了上面一章节封装的leopold-spring-boot-stater
,但作用域scope
是provided
,因为这个starer并不需要作为服务启动,而是作为jar包引入,对于Spring相关的依赖,在其他项目中应该提供,而不是这个jar包提供,所以打包的时候也不会打入 leopold-spring-boot-stater
,而是仅打包跟jpa
相关的内容,包体积更小,更内聚
在本starter中,我封装了这些内容:
SnowId(后续会用etcd替换)
BaseJpaEntity,包含创建时间、创建人、修改时间等基础元素,所以让其他entity继承此类即可,因此把它封装在了此starter里
jpa bean相关配置
jpa exception相关配置
jpa 通用crud操作
可以看到,内容都是和jpa相关的,这里我简单分享下代码,酌情参考
BaseJpaEntity.class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import lombok.Getter;import lombok.Setter;import org.springframework.data.annotation.CreatedBy;import org.springframework.data.annotation.CreatedDate;import org.springframework.data.annotation.LastModifiedBy;import org.springframework.data.annotation.LastModifiedDate;import javax.persistence.Column;import javax.persistence.MappedSuperclass;import javax.persistence.Transient;import java.io.Serial;import java.io.Serializable;@Getter @Setter @MappedSuperclass public abstract class BaseJpaEntity implements Serializable { @Serial @Transient private static final long serialVersionUID = 7054150882445633369L ; @Column(insertable = false, columnDefinition = "TINYINT(1) DEFAULT 0") private Integer isDeleted = 0 ; @Column(updatable = false) @CreatedBy private Long creator; @LastModifiedBy private Long updator; @CreatedDate @Column(updatable = false) private Long createDate; @LastModifiedDate private Long updateDate; }
每张表继承此类,就可以含有这些基础元素,如果表不需要这些,就不用继承
接下来,我要分享一个自己内聚的一个通用查询,它包含了crud的操作,不同于jpa驼峰查询,在引入了grpc的概念后,驼峰命名方法虽然快速,但对外暴露的grpc接口错综复杂,且没有共性。细看proto定义的方法,各种根据字段的查询揉成一团,极度类似面向数据编程,因此我封装了一个简单的crud,用于处理各种因字段不同、字段多少的问题
IBaseService.class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import com.leopold.jpa.dto.SmartDTO;import com.leopold.jpa.dto.SmartDeleteDTO;import com.leopold.jpa.dto.SmartSaveDTO;import org.springframework.data.domain.Page;import org.springframework.data.jpa.repository.JpaSpecificationExecutor;import java.util.List;public interface IBaseService <T, C extends JpaSpecificationExecutor <T>> { void initRepository (C repository) ; Page<T> smartFind (SmartDTO smartDTO) ; long smartCount (SmartDTO smartDTO) ; void smartDelete (SmartDeleteDTO smartDeleteDTO) ; List<T> smartSave (SmartSaveDTO smartSaveDTO, Class<T> clazz) ; }
这个接口描述了我们常用的crud操作,以及一个init方法,用于传递jpa的repository对象
这里的各种DTO可能你会一头雾水,别急,我们来看proto中是如何定义的
查询一般分为如下类型:
外层查询相关:分页查询、按照字段排序
时间查询相关:时间范围查询,时间范围段查询
字段查询相关:等值查询、模糊查询、忽略大小写查询
字段查询连接符:and、or
BaseService.class
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 import cn.hutool.json.JSONUtil;import com.leopold.jpa.dto.*;import com.leopold.jpa.enums.EqEnum;import com.leopold.jpa.enums.OrderEnum;import com.leopold.jpa.enums.TimeEnum;import com.leopold.jpa.service.IBaseService;import org.springframework.data.domain.Page;import org.springframework.data.domain.PageRequest;import org.springframework.data.domain.Pageable;import org.springframework.data.domain.Sort;import org.springframework.data.jpa.domain.Specification;import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.data.jpa.repository.JpaSpecificationExecutor;import org.springframework.util.ObjectUtils;import javax.persistence.criteria.Predicate;import java.util.ArrayList;import java.util.List;import java.util.stream.Collectors;public class BaseService <T, C extends JpaSpecificationExecutor <T> & JpaRepository<T, Long>> implements IBaseService <T, C> { private C repository; public static final int MAX_LIMIT = 10000 ; @Override public void initRepository (C repository) { this .repository = repository; } @Override public Page<T> smartFind (SmartDTO smartDTO) { return null ; } @Override public long smartCount (SmartDTO smartDTO) { return 0 ; } @Override public void smartDelete (SmartDeleteDTO smartDeleteDTO) throws CustomException { } @Override public List<T> smartSave (SmartSaveDTO smartSaveDTO, Class<T> clazz) { return null ; } public C getRepository () { return repository; } public void setRepository (C repository) { this .repository = repository; } }
因为jpa查询的关系,表都是简单而且单调的,不会涉及多表联查,多表联查也不适用于jpa查询
对应的proto定义如下:
1 2 3 4 5 6 message SmartReq { QuerySetting querySetting = 99 ; repeated TimeSetting timeSetting = 98 ; repeated EqSetting eqSetting = 97 ; bool eqOr = 96 ; }
可以看到,它对应了上面的四个查询类型所需要的参数SmartDTO
QuerySetting
用于外层的分页查询,所以需要定义分页page和每页个数limit。排序一般按照某个字段排序、默认ASC,而且我们要用到protobuf中,枚举默认值是第一个的特性,即便我们没有set排序的类型,它依旧默认ASC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 message QuerySetting { int32 page = 1 ; int32 limit = 2 ; repeated OrderBy orderBy = 3 ; } message OrderBy { string field = 1 ; OrderEnum orderEnum = 2 ; } enum OrderEnum { ASC = 0 ; DESC = 1 ; }
TimeSetting
用于时间查询,包含开始时间、结束时间、时间查询字段和时间查询范围类型,注意,TimeSetting
这里我默认repeated
、也就是可以是多个。默认是and连接符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 message TimeSetting { TimeEnum timeEnum = 1 ; int64 startTime = 2 ; int64 endTime = 3 ; string key = 4 ; } enum TimeEnum { LT = 0 ; GT = 1 ; BETWEEN = 2 ; LTE = 3 ; GTE = 4 ; }
EqSetting
用于等值查询,包含查询的字段、字段值、字段的类型、是否模糊查询、是否忽略大小写,bool类型也是默认false的,在微服务调用时可不set此字段,通常我们都是查询字符串类型的字段,所以我把查询字段类型的枚举中,第一个设为STRING,保持默认值的特性。EqSetting
也是 repeated
的,因此可以多个等值查询,因为有or的查询,所以单独设定了这个等值查询的连接符,也就是eqOr
字段,bool类型,默认false,就是默认and连接符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 message EqSetting { string key = 1 ; string val = 2 ; EqEnum eqEnum = 3 ; bool isLike = 4 ; bool isLower = 5 ; } enum EqEnum { STRING = 0 ; LONG = 1 ; INTEGER = 2 ; BOOL = 3 ; }
接下来,我将详细描述Service层查询的具体实现思路:
jpa的分页查询我使用的是JpaSpecificationExecutor
的Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);
方法,所以我们先构造所需要的Specification
因为查询的类型不同,我们需要区分是时间查询还是等值查询,所以我们先构造一个初始化的查询init
,等同于where 1=1 and
的效果
1 2 3 public Specification<T> init () { return (root, criteriaQuery, criteriaBuilder) -> criteriaBuilder.and(); }
实现初始化一个Specification
1 2 3 4 public Specification<T> spec (SmartDTO smartDTO) { Specification<T> findSpecification = init(); return findSpecification; }
接下来,只需要按需判断,哪个类型的查询有值,我们就按照哪个查询的原则,比如等值查询:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 if (!ObjectUtils.isEmpty(smartDTO.getEqSettingDTOList())) { findSpecification = findSpecification.and(eq(smartDTO.getEqSettingDTOList(), smartDTO.getEqOr())); } ... public Specification<T> eq (List<EqSettingDTO> eqSettingDTOList, Boolean eqOr) { return (root, criteriaQuery, criteriaBuilder) -> { List<Predicate> predicates = new ArrayList <>(); for (EqSettingDTO eqSettingDTO : eqSettingDTOList) { String v = eqSettingDTO.getVal(); String k = eqSettingDTO.getKey(); Boolean isLike = eqSettingDTO.getIsLike(); EqEnum eqEnum = eqSettingDTO.getEqEnum(); Boolean isLower = eqSettingDTO.getIsLower(); switch (eqEnum) { case BOOL -> predicates.add(criteriaBuilder.equal(root.get(k).as(Boolean.class), Boolean.parseBoolean(v))); case LONG -> predicates.add(criteriaBuilder.equal(root.get(k).as(Long.class), Long.parseLong(v))); case INTEGER -> predicates.add(criteriaBuilder.equal(root.get(k).as(Integer.class), Integer.parseInt(v))); case STRING -> predicates.add(isLike ? criteriaBuilder.like( isLower ? criteriaBuilder.lower(root.get(k).as(String.class)) : root.get(k).as(String.class) , String.format("%%%s%%" , v)) : criteriaBuilder.equal( isLower ? criteriaBuilder.lower(root.get(k).as(String.class)) : root.get(k).as(String.class) , v)); default -> throw new IllegalArgumentException ("Invalid EqEnum value: " + eqEnum); } } return ObjectUtils.isEmpty(predicates) ? null : eqOr ? criteriaBuilder.or(predicates.toArray(new Predicate [0 ])) : criteriaBuilder.and(predicates.toArray(new Predicate [0 ])) ; }; }
时间查询:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 if (!ObjectUtils.isEmpty(smartDTO.getTimeSettingDTOList())) { findSpecification = findSpecification.and(time(smartDTO.getTimeSettingDTOList())); } ... public Specification<T> time (List<TimeSettingDTO> timeSettingList) { return (root, criteriaQuery, criteriaBuilder) -> { List<Predicate> predicates = new ArrayList <>(); for (TimeSettingDTO timeSetting : timeSettingList) { String key = timeSetting.getKey(); long startTime = timeSetting.getStartTime(); long endTime = timeSetting.getEndTime(); TimeEnum timeEnum = timeSetting.getTimeEnum(); switch (timeEnum) { case BETWEEN -> predicates.add(criteriaBuilder.between(root.get(key), startTime, endTime)); case LT -> predicates.add(criteriaBuilder.lt(root.get(key), startTime)); case GT -> predicates.add(criteriaBuilder.gt(root.get(key), startTime)); case LTE -> predicates.add(criteriaBuilder.lessThanOrEqualTo(root.get(key), startTime)); case GTE -> predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.get(key), startTime)); default -> throw new IllegalArgumentException ("Invalid timeEnum value: " + timeEnum); } } return criteriaBuilder.and(predicates.toArray(new Predicate [0 ])); }; }
最终拼接的spec
方法如下:
1 2 3 4 5 6 7 8 9 10 public Specification<T> spec (SmartDTO smartDTO) { Specification<T> findSpecification = init(); if (!ObjectUtils.isEmpty(smartDTO.getEqSettingDTOList())) { findSpecification = findSpecification.and(eq(smartDTO.getEqSettingDTOList(), smartDTO.getEqOr())); } if (!ObjectUtils.isEmpty(smartDTO.getTimeSettingDTOList())) { findSpecification = findSpecification.and(time(smartDTO.getTimeSettingDTOList())); } return findSpecification; }
对应的 smartFind
方法里:
1 2 3 4 5 6 @Override public Page<T> smartFind (SmartDTO smartDTO) { QuerySettingDTO querySettingDTO = smartDTO.getQuerySettingDTO(); Specification<T> findSpecification = spec(smartDTO); ... }
拼接好了Specification
,我们需要拼接分页字段,返回findAll所需要的Pageable
对象,我们首先构建一个List,用于存储排序字段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public List<Sort> sort (List<OrderByDTO> orderByDTOList) { List<Sort> sortList = new ArrayList <>(); if (!ObjectUtils.isEmpty(orderByDTOList)) { for (OrderByDTO orderByDTO : orderByDTOList) { String field = orderByDTO.getField(); OrderEnum orderEnum = orderByDTO.getOrderEnum(); switch (orderEnum) { case ASC -> sortList.add(Sort.by(Sort.Direction.ASC, field)); case DESC -> sortList.add(Sort.by(Sort.Direction.DESC, field)); default -> throw new IllegalArgumentException ("Invalid orderEnum value: " + orderEnum); } } } return sortList; }
对应的 smartFind
方法里:
1 2 3 4 5 6 7 @Override public Page<T> smartFind (SmartDTO smartDTO) { QuerySettingDTO querySettingDTO = smartDTO.getQuerySettingDTO(); Specification<T> findSpecification = spec(smartDTO); List<Sort> sortList = sort(smartDTO.getQuerySettingDTO().getOrderByDTOList()); ... }
接下来构建Pageable
对象,这里我们默认只查前10000行,如果不传page,但传了limit,就等同于jpa的top
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public Pageable getPage (int page, int limit, List<Sort> sortList) { PageRequest pageRequest = null ; if (page == 0 && limit == 0 ) { page = 1 ; limit = MAX_LIMIT; } if (limit > 0 ) { if (page > 0 ) { pageRequest = PageRequest.of(page - 1 , limit); } else { pageRequest = PageRequest.ofSize(limit); } } if (null != pageRequest && !ObjectUtils.isEmpty(sortList)) { for (Sort orders : sortList) { pageRequest = pageRequest.withSort(orders); } } return pageRequest; }
对应的 smartFind
方法里:
1 2 3 4 5 6 7 8 9 @Override public Page<T> smartFind (SmartDTO smartDTO) { QuerySettingDTO querySettingDTO = smartDTO.getQuerySettingDTO(); List<Sort> sortList = sort(smartDTO.getQuerySettingDTO().getOrderByDTOList()); Specification<T> findSpecification = spec(smartDTO); int limit = querySettingDTO.getLimit(); int page = querySettingDTO.getPage(); return this .repository.findAll(findSpecification, getPage(page, limit, sortList)); }
至此,smartFind
方法已经实现了,这个方法很简单,但它构造了我们很多公共的方法,后续只要有拼接 where
或者order by
、limit
等,都可以共用,比如接下来,我们着手 smartCount
方法:
1 2 3 4 @Override public long smartCount (SmartDTO smartDTO) { return this .repository.count(spec(smartDTO)); }
你会看到,查询总数的方法不会涉及排序和分页、所以只需要构造一个specification即可,而且可共用spec()
这个逻辑
接下来我们封装smartSave
方法
proto定义如下:
1 2 3 message SmartSaveReq { repeated string entity = 1 ; }
也许你会奇怪,为什么是一个string类型,并且可以传递多个,因为每个实体的字段实在是不同,所以这里并不会考虑protobuf字段定义所带来的字节数降低的优点,而是序列化成字符串传递,虽然会增大一个消息的字节数,但在封装上省下不少时间
对应的smartSave
实现如下:
1 2 3 4 5 6 7 8 9 10 @Override public List<T> smartSave (SmartSaveDTO smartSaveDTO, Class<T> clazz) { List<String> entityList = smartSaveDTO.getEntityList(); if (!ObjectUtils.isEmpty(entityList)) { List<T> collectList = entityList.stream().map(entity -> JSONUtil.toBean(entity, clazz)).toList(); return this .repository.saveAll(collectList); } else { throw new CustomException ("保存对象为空,禁止保存" ); } }
因为是string类型,所以我们在传递的过程中需要序列化为json str,再反序列化为泛型对象
smartDelete
的定义和smartFind
大同小异,只不过不用排序和分页
proto定义如下:
1 2 3 4 5 message SmartDeleteReq { repeated EqSetting eqSetting = 97 ; bool eqOr = 96 ; repeated string id = 1 ; }
这里的 EqSetting
和 smartFind
里的 EqSetting
是一样的,eqOr
用于and()
还是or()
判断,id
用于主键删除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Override public void smartDelete (SmartDeleteDTO smartDeleteDTO) throws CustomException { List<String> idList = smartDeleteDTO.getIdList(); if (!ObjectUtils.isEmpty(idList)) { this .repository.deleteAllById(idList.stream().map(Long::parseLong).collect(Collectors.toList())); } else { Specification<T> findSpecification = init(); List<EqSettingDTO> eqSettingDTOList = smartDeleteDTO.getEqSettingDTOList(); Specification<T> eqList = eq(eqSettingDTOList, smartDeleteDTO.getEqOr()); if (ObjectUtils.isEmpty(eqList)) { throw new CustomException ("删除条件为空,禁止删除" ); } findSpecification = findSpecification.and(eqList); List<T> repositoryDeleteAll = this .repository.findAll(findSpecification); this .repository.deleteAll(repositoryDeleteAll); } }
P.S. 请使用 CrudRepository
下的删除方法,因为在实体里,我们的删除是假删除,执行删除操作其实是update了is_delete这个字段,所以我会在实体上添加如下注解:
1 2 3 4 5 6 ... @SQLDelete(sql = "update xxxEntity set is_deleted = 1 where id = ?") @Where(clause = "is_deleted != 1") public class xxxEntity extends BaseJpaEntity { ... }
如果使用其他类的删除方法,这个注解就失效了
服务端 接口代码如下:
1 2 public interface IResourceService extends IBaseService <ResourceEntity, ResourceRepository> {}
实现类代码如下:
1 2 3 4 5 6 7 8 9 public class ResourceService extends BaseService <ResourceEntity, ResourceRepository> implements IResourceService { @Resource private ResourceRepository resourceRepository; @PostConstruct public void initService () { super .initRepository(resourceRepository); } }
grpc代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 @Override public void smartCount (SmartReq request, StreamObserver<ResourceSmartCountResp> responseObserver) { try { ResourceSmartCountResp.Builder builder = ResourceSmartCountResp.newBuilder(); builder.setCount(resourceService.smartCount(SmartFactory.buildSmartDTO(request))); responseObserver.onNext(builder.build()); responseObserver.onCompleted(); } catch (Exception e) { log.error(e.getMessage(), e); responseObserver.onError(Status.INTERNAL.withDescription(e.getMessage()).asRuntimeException()); } } @Override public void smartFind (SmartReq request, StreamObserver<ResourceSmartFindResp> responseObserver) { try { log.debug("smartFind request -> {}" , request); long queryStart = System.currentTimeMillis(); ResourceSmartFindResp.Builder builder = ResourceSmartFindResp.newBuilder(); Page<ResourceEntity> resourceEntities = resourceService.smartFind(SmartFactory.buildSmartDTO(request)); List<Resource> resources = resourceFactory.buildResourceProtoList(resourceEntities.getContent()); builder.addAllResource(resources); builder.setCount(resourceEntities.getTotalElements()); responseObserver.onNext(builder.build()); responseObserver.onCompleted(); log.debug("smartFind response -> {}" , resources); log.debug("smartFind cost {}ms" , System.currentTimeMillis() - queryStart); } catch (Exception e) { log.error(e.getMessage(), e); responseObserver.onError(Status.INTERNAL.withDescription(e.getMessage()).asRuntimeException()); } } @Override @Transactional(rollbackFor = Exception.class) public void smartSave (SmartSaveReqProto.SmartSaveReq request, StreamObserver<SmartSaveRespProto.SmartSaveResp> responseObserver) { try { List<ResourceEntity> resourceEntityList = resourceService.smartSave(SmartSaveFactory.buildSmartSaveDTO(request), ResourceEntity.class); responseObserver.onNext(smartSaveFactory.buildSmartSaveResp(resourceEntityList)); responseObserver.onCompleted(); } catch (Exception e) { log.error(e.getMessage(), e); responseObserver.onError(Status.INTERNAL.withDescription(e.getMessage()).asRuntimeException()); } } @Override @Transactional(rollbackFor = Exception.class) public void smartDelete (SmartDeleteReqProto.SmartDeleteReq request, StreamObserver<SmartDeleteRespProto.SmartDeleteResp> responseObserver) { try { resourceService.smartDelete(SmartDeleteFactory.buildSmartDeleteDTO(request)); responseObserver.onNext(SmartDeleteRespProto.SmartDeleteResp.newBuilder().build()); responseObserver.onCompleted(); } catch (Exception e) { log.error(e.getMessage(), e); responseObserver.onError(Status.INTERNAL.withDescription(e.getMessage()).asRuntimeException()); } }
客户端 下面我将测试crud,伪码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 @Test public void smartFind () throws Exception { ... SmartReqProto.SmartReq.Builder builder = SmartReqProto.SmartReq.newBuilder(); builder.setQuerySetting(QuerySettingProto.QuerySetting.newBuilder() .setPage(1 ) .setLimit(5 ) .addOrderBy(QuerySettingProto.OrderBy.newBuilder() .setField("subscribeTime" ) .setOrderEnum(QuerySettingProto.OrderEnum.DESC) .build()) .build()); builder.addEqSetting(QuerySettingProto.EqSetting.newBuilder() .setKey("isSubscribe" ) .setVal("true" ) .setEqEnum(QuerySettingProto.EqEnum.BOOL) .build()) ; SmartFindResp smartFindResp = stub.smartFind(builder.build()); ... } @Test public void smartCount () throws Exception { SmartReqProto.SmartReq.Builder builder = SmartReqProto.SmartReq.newBuilder(); builder.addEqSetting(QuerySettingProto.EqSetting.newBuilder() .setKey("isSubscribe" ) .setVal("true" ) .setEqEnum(QuerySettingProto.EqEnum.BOOL) .build()) ; SmartCountResp smartCountResp = stub.smartCount(builder.build()); long count = smartCountResp.getCount(); System.out.println("count: " + count); } @Test public void smartSave () throws Exception { SmartSaveReqProto.SmartSaveReq.Builder builder = SmartSaveReq.newBuilder(); builder.addAllEntity(List.of( JSONUtil.toJsonStr(Map.of( "password" , "456" , "name" , "1" )), JSONUtil.toJsonStr(Map.of( "password" , "456" , "name" , "2" )), JSONUtil.toJsonStr(Map.of( "password" , "456" , "name" , "3" )) )); SmartSaveResp smartSaveResp = stub.smartSave(builder.build()); ProtocolStringList entity = smartSaveResp.getEntityList(); System.out.println("entity: " + entity); } @Test public void smartDelete () throws Exception { SmartDeleteReq.Builder builder = SmartDeleteReq.newBuilder(); builder.addEqSetting(QuerySettingProto.EqSetting.newBuilder().build()); builder.addAllId(List.of( "1660565474116767744" , "1660565474120962048" , "1660565474120962049" )); SmartDeleteResp smartDeleteResp = stub.smartDelete(builder.build()); long count = smartDeleteResp.getCount(); System.out.println("count: " + count); }
leopold-redis-starter 这个starter主要用于redis 的连接以及一些基础的redis操作
pom.xml
内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.7.9</version > <relativePath /> </parent > <groupId > com.leopold</groupId > <artifactId > leopold-redis-starter</artifactId > <version > 1.0.2-SNAPSHOT</version > <name > leopold-redis-starter</name > <description > leopold-redis-starter</description > <properties > <maven.compiler.source > 17</maven.compiler.source > <maven.compiler.target > 17</maven.compiler.target > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > </properties > <dependencies > <dependency > <groupId > com.leopold</groupId > <artifactId > leopold-spring-boot-starter</artifactId > <version > 1.0.3-SNAPSHOT</version > <scope > provided</scope > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-pool2</artifactId > <version > 2.11.0</version > <type > jar</type > <scope > compile</scope > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency > </dependencies > <build > <finalName > ${project.artifactId}</finalName > <resources > <resource > <directory > src\main\resources\lib</directory > <targetPath > BOOT-INF\lib</targetPath > <includes > <include > **/*.jar</include > </includes > </resource > <resource > <directory > src/main/resources</directory > </resource > </resources > </build > </project >
关于redis的配置,我已经写过了,详细可以查看这篇文章 Rancher部署Redis HA
leopold-security-starter 这个starter用于配置token相关,常用于web项目
pom.xml
内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.7.9</version > <relativePath /> </parent > <groupId > com.leopold</groupId > <artifactId > leopold-security-starter</artifactId > <version > 1.0.3-SNAPSHOT</version > <properties > <maven.compiler.source > 17</maven.compiler.source > <maven.compiler.target > 17</maven.compiler.target > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > </properties > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency > <dependency > <groupId > com.leopold</groupId > <artifactId > leopold-spring-boot-starter</artifactId > <version > 1.0.3-SNAPSHOT</version > <scope > provided</scope > </dependency > <dependency > <groupId > com.leopold</groupId > <artifactId > leopold-redis-starter</artifactId > <version > 1.0.2-SNAPSHOT</version > <scope > provided</scope > </dependency > <dependency > <groupId > com.leopold</groupId > <artifactId > leopold-jpa-starter</artifactId > <version > 1.0.4-SNAPSHOT</version > <scope > provided</scope > </dependency > </dependencies > <build > <finalName > ${project.artifactId}</finalName > <resources > <resource > <directory > src\main\resources\lib</directory > <targetPath > BOOT-INF\lib</targetPath > <includes > <include > **/*.jar</include > </includes > </resource > <resource > <directory > src/main/resources</directory > </resource > </resources > </build > </project >
你会好奇,为什么security的starter会引入jpa的starter,其实是因为在设计用户表和密码的校验中,是需要涉及到UserEntity实体的,也就需要jpa操作实体,后续这个starter会适配单点登录、Oauth2,所以它会涉及到用户表的概念。
在这个starter中,我做了如下操作:
JWT配置、跨域配置、白名单配置
角色配置、密码校验配置
这里的代码比较复杂,我自定义的也比较灵活,就不贴代码了
leopold-websocket-starter 这个starer用于处理websocket连接,由于目前websocket连接需要token验证,所以也包含了leopold-security-starter
pom.xml
内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.7.9</version > <relativePath /> </parent > <groupId > com.leopold</groupId > <artifactId > leopold-websocket-starter</artifactId > <version > 1.0.2-SNAPSHOT</version > <properties > <maven.compiler.source > 17</maven.compiler.source > <maven.compiler.target > 17</maven.compiler.target > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > </properties > <dependencies > <dependency > <groupId > com.leopold</groupId > <artifactId > leopold-spring-boot-starter</artifactId > <version > 1.0.3-SNAPSHOT</version > <scope > provided</scope > </dependency > <dependency > <groupId > com.leopold</groupId > <artifactId > leopold-security-starter</artifactId > <version > 1.0.3-SNAPSHOT</version > <scope > provided</scope > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-websocket</artifactId > </dependency > </dependencies > <build > <finalName > ${project.artifactId}</finalName > <resources > <resource > <directory > src\main\resources\lib</directory > <targetPath > BOOT-INF\lib</targetPath > <includes > <include > **/*.jar</include > </includes > </resource > <resource > <directory > src/main/resources</directory > </resource > </resources > </build > </project >
部分核心实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 package com.leopold.websocket;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import org.springframework.util.ObjectUtils;import javax.websocket.*;import javax.websocket.server.HandshakeRequest;import javax.websocket.server.PathParam;import javax.websocket.server.ServerEndpoint;import javax.websocket.server.ServerEndpointConfig;import java.io.IOException;import java.util.List;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.atomic.AtomicInteger;@ServerEndpoint(value = "/websocket/{userId}", subprotocols = {"xxx"}, configurator = WebSocketServer.CustomWebSocketConfigurator.class) @Component @Slf4j public class WebSocketServer { private static TokenUtil tokenUtil; @Autowired public void setTokenUtil (TokenUtil tokenUtil) { WebSocketServer.tokenUtil = tokenUtil; } private static final AtomicInteger ONLINE_COUNT = new AtomicInteger (0 ); private static final ConcurrentHashMap<String, Session> CLIENTS_MAP = new ConcurrentHashMap <>(); private static final ExecutorService TASK_POOL = Executors.newFixedThreadPool(3 ); private volatile String userId; public static class CustomWebSocketConfigurator extends ServerEndpointConfig .Configurator { @Override public void modifyHandshake (ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) { super .modifyHandshake(config, request, response); List<String> tokenList = request.getHeaders().get("sec-websocket-protocol" ); if (tokenList != null && tokenList.size() > 0 ) { String token = tokenList.get(0 ); config.getUserProperties().put("token" , token.split("," )[0 ].trim()); } } } @OnOpen public void onOpen (@PathParam("userId") String userId, Session session) throws Exception { Map<String, Object> userProperties = session.getUserProperties(); Object tokenO = userProperties.get("token" ); if (ObjectUtils.isEmpty(tokenO)) { throw new Exception ("token not found" ); } String token = tokenO.toString(); if (!tokenUtil.check(token, userId)) { throw new Exception ("auth failed" ); } this .userId = userId; CLIENTS_MAP.put(userId, session); log.info("A new webSocket connected, userId -> [{}], all connected num -> [{}]" , userId, ONLINE_COUNT.incrementAndGet()); } @OnClose public void onClose () { if (!ObjectUtils.isEmpty(userId)) { CLIENTS_MAP.remove(userId); log.info("A webSocket closed, userId -> [{}], current connected num -> [{}]" , userId, ONLINE_COUNT.decrementAndGet()); } } @OnMessage public void onMessage (@PathParam("userId") String userId, String message, Session session) throws IOException { if (!ObjectUtils.isEmpty(message) && WebSocketMessageTypeEnum.HEART.name().equals(message)) { sendMessage(message, session); } else { log.info("get message from client -> [{}]" , message); } } @OnError public void onError (Session session, Throwable error) { if (error instanceof java.io.EOFException) { log.warn("ws normally disconnect with max connect timeout, session id -> [{}]" , session.getId()); } else { log.error("ws error -> {} session id -> [{}]" , error.getMessage(), session.getId(), error); } try { session.close(); } catch (IOException e) { log.error("ws close error -> {} session id -> [{}]" , error.getMessage(), session.getId(), error); } } public static void broadcastMessage (String message) { TASK_POOL.submit(() -> { for (Session session : CLIENTS_MAP.values()) { try { sendMessage(message, session); } catch (IOException e) { log.error("broadcast message error -> {}" , e.getMessage(), e); } } }); } public static void sendMessage (String message, String userId) { for (String clientUserId : CLIENTS_MAP.keySet()) { if (userId.equals(clientUserId)) { TASK_POOL.submit(() -> { try { sendMessage(message, CLIENTS_MAP.get(clientUserId)); } catch (IOException e) { log.error("send message error, userId -> [{}], message -> [{}], because -> [{}]" , userId, message, e.getMessage(), e); } }); break ; } } } public static void sendMessage (String message, List<String> userIdList) { TASK_POOL.submit(() -> { for (String clientUserId : CLIENTS_MAP.keySet()) { if (userIdList.contains(clientUserId)) { try { sendMessage(message, CLIENTS_MAP.get(clientUserId)); } catch (IOException e) { log.error("send message error, userId -> [{}], message -> [{}], because -> [{}]" , clientUserId, message, e.getMessage(), e); } } } }); } private static void sendMessage (String message, Session session) throws IOException { if (session.isOpen()) { session.getBasicRemote().sendText(message); } else { log.warn("session [{}] is closed, cannot send message" , session.getId()); } } }
编译 在封装好了starter后,我们需要把jar包上传至Nexus,流程如下:
代码上传至Git仓库
触发WebHook,通知Jenkins
Jenkins执行 mvm deploy
,打成jar包,并上传至Nexus
其他项目引用jar包
下面我简单贴一些配置仅供参考
Jenkins Pipeline
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 pipeline { agent any environment { JAVA_HOME = "你的JAVA_HOME" PATH = "${JAVA_HOME}/bin:${PATH}" NEXUS_REPO = "my-maven::default::你的Nexus_repo地址" } stages { stage('初始化代码环境' ) { steps { deleteDir() dir("${workspace}@tmp" ) { deleteDir() } dir("${workspace}@script" ) { deleteDir() } } } stage('拉取Git仓库代码' ) { steps { ... } } stage('打包至Nexus' ) { steps { sh 'mvn -Dmaven.compiler.executable=${JAVA_HOME}/bin/javac clean deploy -DskipTests -DaltDeploymentRepository=${NEXUS_REPO}' } } } }
如果你想relead每个jar,让tag作为当前release版本的version,可以使用 mvn versions:set -DnewVersion=${tag} -DartifactId=${ARTIFACT_ID}
更改。
Maven配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 ... <mirrors > <mirror > <id > nexus-aliyun</id > <mirrorOf > central</mirrorOf > <name > Nexus aliyun</name > <url > http://maven.aliyun.com/nexus/content/groups/public</url > </mirror > <mirror > <id > my-maven</id > <mirrorOf > *</mirrorOf > <name > my-mirror</name > <url > http://xxx/repository/xxx-group/</url > </mirror > </mirrors > ... <profiles > <profile > <id > nexus</id > <repositories > <repository > <id > my-maven</id > <name > Nexus</name > <url > http://xxx/repository/xxx-group/</url > <releases > <enabled > true</enabled > </releases > <snapshots > <enabled > true</enabled > <updatePolicy > always</updatePolicy > </snapshots > </repository > </repositories > </profile > </profiles > <activeProfiles > <activeProfile > nexus</activeProfile > </activeProfiles >
在需要编译和拉取的主机上添加到maven的settings.xml
配置文件中即可
服务拆分 在拆服务之前,我想先简单描述一下,原先的单体服务都包含了哪些功能
后台管理系统(登录、表的crud)
微信公众号(接收消息、处理消息、表的crud)
爬虫服务(线程)
所以我暂时将单体服务抽象为4个微服务
api:应用层服务。它是所有暴露的api应用层入口,具有security校验能力,用于组织其他微服务执行的结果
service:实体服务。它是所有表crud的入口
wx:微信公众号服务。它是微信对接消息的服务,所有跟微信通信的消息都会发送至这个微服务
spider:爬虫服务。当收到爬虫任务时,需要执行的爬虫服务
这样分的好处很明显:
假如爬虫服务崩溃,它不会影响微信公众号服务的响应。爬虫需求大,可暂时提升pod数量,增大爬虫效率
服务之间的边界明显,高内聚了核心的功能,低耦合互不影响。
它的缺点也是有的:
如何处理事务
如何保证幂等性
实体过多,各种重复的封装操作几乎都在每个微服务重新上演
没有写单体服务来的那么爽快
是的,上升了微服务,得到了稳定与隔离,一定会失去一些,而且目前的稳定是极其脆弱的,那该如何解决呢?
如何处理事务、保证幂等性:我更倾向于使用锁来处理,分布式的服务我们通常使用redis锁,但k3s中,我更想使用etcd锁来与pod紧密结合,哪怕回滚,也要一步一步回滚、多余的数据要进行删除和补回
实体错综复杂:使用文档型的数据库MongoDB、使用DDD聚合实体,分清聚合根,使每个实体都不再面向数据驱动,不受前端的影响,聚合自己的服务。这个过程是极易犯错的,很有可能聚合的实体越来越变得有局限性,所以刚开始一定要想清楚,所以才会使用文档型数据库,弥补行数据库的死板
没有写单体服务来的那么爽快:这个的确无法避免,随着人数的增多,它会越来越要求后端人员的前瞻能力,每个人的聚合能力不同,就会导致各个服务面目全非,甚至过度区分微服务,导致devops和编码时间大大增加,在时间效率上反而远远不足于单体服务,这是一把双刃剑,小公司很难驾驭、老板有时候也不会有前瞻性,也不肯愿意多支付微服务带来的高额费用。大公司不愿意改型,
其他 在这个月里,我基本上思维都是以代码的模式,基本没有devops思维的跳跃,起初是有点不适应,且枯燥无味的,因为crud很无聊,核心逻辑也没有变动,所以经常摆烂地写-,-
在下个月中,我将加入devops的思维,并分步,与编程的思维结合,主要有:
MongoDB HA的搭建
java重构service服务,将实体数据写入MongoDB中
搭建go环境
go重构爬虫服务,优化爬虫性能
这一部分大概也需要一个月的时间,届时,这个服务将逐渐由数据驱动逐步转型为领域驱动设计,目前主要是先将数据底层转型,再从DDD切入,我们下个月再见~