基于ShardingSphere的数据分片

1. 为什么要分片

sharding是为了提升系统响应能力。对于数据库来说,为什么sharding会改善性能?

数据库管理数据提供增删改查功能,耗时的部分主要就是CPU计算和IO访问。单表记录规模过大:

  • 会增加表索引的大小,进而影响索引的IO读取和内存交换。
  • 会增加表数据的文件大小,进而影响最终数据的读取与扫描。
  • 记录数过多,在表扫描时需要处理更多未命中的数据。
  • 业务上,会提升单表操作并发压力。如果有触发表级锁的操作,会加重影响性能。

服务器单机硬件性能有上限,分表的基础上可以将表再分到不同的物理设备上。

2. 分表设计

分表的设计主要从业务需求上出发,主要是根据数据的查询特点来选择,从表操作SQL中必须的条件字段中选取。

例如:对于用户表,更新与加载主要是按用户操作,所以用户表的分表可以直接按用户id进行hash随机,用户及其关子表,都要和用户表保持一致的分表逻辑,因为用户子表的查询必然也会带着用户id。

分表基础上经常还会带着分库,分库分表后表连接查询就可能不支持了,所以这种情况下,要根据主要的使用场景来进行分表设计。

例如:用户好友关系表,存储用户与其好友的uid映射关系,主要的使用场景是加载好友列表(高频),分表设计时要以所有人用户id来进行分表。这里有一个问题,映射表里只有好友的uid,分库分表情况下又不能直接和用户表进行表连接,那么在加载好友列表时只能遍历按uid去获取好友的名称了。一种改进的方式是,在好友关系表中冗余存储需要随好友列表一起频繁加载的信息(比如姓名),附加同步机制来保证数据及时性。

3. sharding算法

将一个大表的数据拆分到多个表,数据的均匀性是非常重要的,这样才能平摊负载。

在数据均匀分布的前提下,只需要按分表个数直接取模就可以了。

1
2
SELECT (gcid %9) as p_gcid, count(1) as c1
FROM crm.customer_crm_search group by p_gcid;
1
2
3
4
5
6
7
8
SELECT (gcid %9) as p_gcid, count(1) as c1
FROM crm.customer_crm_search group by p_gcid;

SELECT (gcid %9) &7 as p_gcid, count(1) as c2
FROM crm.customer_crm_search group by p_gcid;

SELECT (gcid %127) &7 as p_gcid, count(1) as c3
FROM crm.customer_crm_search group by p_gcid;
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
# p_gcid, c1
0, 1262531
1, 1259903
2, 1259860
3, 1261827
4, 1261015
5, 1260104
6, 1261028
7, 1262103
8, 1262400

# p_gcid, c2
0, 2524931
1, 1259903
2, 1259860
3, 1261827
4, 1261015
5, 1260104
6, 1261028
7, 1262103

# p_gcid, c3
0, 1430624
1, 1431112
2, 1430266
3, 1430061
4, 1429165
5, 1429653
6, 1430281
7, 1339609

6. 参考

  1. Java Bean Copy框架性能对比
  2. 为什么阿里要求避免使用 Apache BeanUtils 进行属性复制?
  3. Performance of Java Mapping Frameworks
  4. MicroServices – DTO to Entity & Entity to DTO mapping – Libraries Comparison
  5. MapStruct Installation
  6. JMapper Philosophy

微服务中VO、DTO、Entity间的相互转换处理

微服务架构下,服务拆分会产生VO、DTO、Entity三类pojo:

  • VO 用于前端接口参数传递,例如用于http接口接收请求参数。可以继承扩展DTO,或者直接使用DTO。
  • DTO 用于rpc接口参数传递。单独定义,或者继承扩展其他rpc接口的DTO。
  • Entity 用于orm映射处理,与表结构一一对应,只在服务内部使用,不能对外。

1. 问题

微服务架构面向不同场景的pojo定义,引入了一个问题:一个前端请求的处理,需要在这三者之间进行转换

  • 请求:VO => DTO => Entity
  • 返回:Entity => DTO => VO

Entity

1
2
3
4
5
6
7
8
9
10
11
@Data
@TableName(value = "orders", schema = "crazy1984")
public class Order {
@TableId(type = IdType.AUTO)
private int id;
private String orderId;
private String orderType;
private int orderStatus;
private Date createdAt;
private Date updatedAt;
}

DTO

1
2
3
4
5
6
7
@Data
public class OrderDTO {
private String orderId;
private String orderType;
private int orderStatus;
private Date createdAt;
}

VO

1
2
3
4
5
@Data
public class OrderVO extends OrderDTO{
private String orderTypeName;
private String orderStatusName;
}

2. 手动转换

直接的方法,是通过代码对pojo属性进行逐个拷贝:

1
2
3
4
5
OrderDTO dto = new OrderDTO();
dto.setOrderId(entity.getOrderId());
dto.setOrderType(entity.getOrderType());
dto.setOrderStatus(entity.getOrderStatus());
dto.setCreatedAt(entity.getCreatedAt());

这样的方式太低效,给开发人员增加许多低效的重复劳动,也不易维护(比如新增字段时,所有相关处都要同步修改)。借助IDE工具自动生成代码,可以减少重复工作。

3. 工具类辅助拷贝转换

改进一点的方法是,使用工具类进行同名属性的自动拷贝,例如使用spring的BeanUtils

1
2
OrderDTO dto = new OrderDTO();
BeanUtils.copyProperties(entity, dto);

这样可以大量减少代码工作,但也有一些不足之处:

  1. 不支持属性名映射,属性名必须完全一致。
  2. 不支持自动类型转换,spring的BeanUtils要求源属性与目标属性的类型是相互assignable的。
  3. 属性拷贝中的属性名匹配、类型检查、写权限检查都是动态判断,有性能损耗。

4. Mapping框架

除了以上方法,还可以用开源的mapping框架,如:

这些框架都支持:

  • 不同属性名的映射
  • 自动类型转换
  • 递归mapping自定义pojo的属性

例如 ModelMapper:

1
2
ModelMapper mapper = new ModelMapper();
OrderDTO dto = modelMapper.map(entity, OrderDTO.class);

这些框架的实现原理,可以归结为以下几点:

  • 基于java反射机制,根据源和目标的class自动进行属性get、set的调用。
  • 基于注解、配置,来进行不同属性名的映射、类型转换。

以上这些框架的性能对比MapStructJMapper性能较好。原因是,它们都使用代码生成将map的过程静态化了,所以实际性能和自己手写get、set一样。在开发过程中,可以通过检查生成的代码来确保mapping转换没有错误,相比ModelMapper的黑盒实现更加可靠。对于grpc协议protobuf对象和entity的互相转换,也能很好的支持。

5. MapStruct的使用

maven安装

在项目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
<?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>com.crazy1984.mapper</groupId>
<artifactId>mapper-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>mapper-demo</artifactId>
<packaging>jar</packaging>
<properties>
<m2e.apt.activation>jdt_apt</m2e.apt.activation>
<org.mapstruct.version>1.3.1.Final</org.mapstruct.version>
<org.projectlombok.version>1.18.12</org.projectlombok.version>
</properties>
<!-- provided -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<scope>provided</scope>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Amapstruct.suppressGeneratorTimestamp=true</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>

上述配置,能让lombok和mapstruct一起工作。

示例:Entity和DTO的双向mapping

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
/**
* demo: entity
*
* @author zhangjy
* @date 2019-10-24
*/
@Data
@TableName(value = "transfer_flow", schema = "sale_fund")
public class TransferFlow {

@TableId(type = IdType.AUTO)
private int id;
@TableField(updateStrategy = FieldStrategy.NEVER)
private String flowId;// 插入后不可更新
@TableField(updateStrategy = FieldStrategy.NEVER)
private String orderId;// 插入后不可更新
/**
* 转移状态:0未开始,1成功,2失败,3跳过
*
* @see TransferResultEnum
*/
private int transferStatus;
@TableField(updateStrategy = FieldStrategy.NEVER)
private Date createdAt;// 插入后不可更新
private Date updatedAt;
/**
* 非数据库字段:demo注释
*/
@TableField(exist = false)
private String comment;
}

/**
* demo: dto
*
* @author zhangjy
* @date 2019-10-24
*/
@Data
public class TransferFlowDTO implements Serializable {

private static final long serialVersionUID = -1256479071103445488L;
private int id;
private String flowId;
private String orderId;
/**
* 转移状态:0未开始,1成功,2失败,3跳过
*
* @see TransferResultEnum
*/
private int transferStatus;
private LocalDate createdAt;
private Date updatedAt;
/**
* demo注释
*/
private String comment;
}

/**
* @author zhangjy
* @date 2020-03-18
*/
@Mapper
public interface TransferFlowConverter {

TransferFlowConverter INSTANCE = Mappers.getMapper(TransferFlowConverter.class);

TransferFlowDTO toDTO(TransferFlow entity);

List<TransferFlowDTO> toDTO(List<TransferFlow> lst);

TransferFlow fromDTO(TransferFlowDTO dto);

}

生成的代码是:

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
public class TransferFlowConverterImpl implements TransferFlowConverter {

@Override
public TransferFlowDTO toDTO(TransferFlow entity) {
if ( entity == null ) {
return null;
}

TransferFlowDTO transferFlowDTO = new TransferFlowDTO();

transferFlowDTO.setId( entity.getId() );
transferFlowDTO.setFlowId( entity.getFlowId() );
transferFlowDTO.setOrderId( entity.getOrderId() );
transferFlowDTO.setTransferStatus( entity.getTransferStatus() );
if ( entity.getCreatedAt() != null ) {
// Date to LocalDate
transferFlowDTO.setCreatedAt( LocalDateTime.ofInstant( entity.getCreatedAt().toInstant(), ZoneOffset.UTC ).toLocalDate() );
}
transferFlowDTO.setUpdatedAt( entity.getUpdatedAt() );
transferFlowDTO.setComment( entity.getComment() );

return transferFlowDTO;
}

@Override
public List<TransferFlowDTO> toDTO(List<TransferFlow> lst) {
if ( lst == null ) {
return null;
}

List<TransferFlowDTO> list = new ArrayList<TransferFlowDTO>( lst.size() );
for ( TransferFlow transferFlow : lst ) {
list.add( toDTO( transferFlow ) );
}

return list;
}

@Override
public TransferFlow fromDTO(TransferFlowDTO dto) {
if ( dto == null ) {
return null;
}

TransferFlow transferFlow = new TransferFlow();

transferFlow.setId( dto.getId() );
transferFlow.setFlowId( dto.getFlowId() );
transferFlow.setOrderId( dto.getOrderId() );
transferFlow.setTransferStatus( dto.getTransferStatus() );
if ( dto.getCreatedAt() != null ) {
// LocalDate to Date
transferFlow.setCreatedAt( Date.from( dto.getCreatedAt().atStartOfDay( ZoneOffset.UTC ).toInstant() ) );
}
transferFlow.setUpdatedAt( dto.getUpdatedAt() );
transferFlow.setComment( dto.getComment() );

return transferFlow;
}
}

示例:带递归和额外信息补充的mapping

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
@Data
@AllArgsConstructor(access = AccessLevel.PACKAGE)
@NoArgsConstructor
@Builder
public class TaskListDTO implements Serializable {
private static final long serialVersionUID = -2125356351064981209L;

private String taskId;
private String taskName;
private Integer taskState;
private String taskStateText;
private List<ApproverlDTO> approverlDTOList;

}

@Data
public class TaskList {
private String taskId;
private String taskName;
private Integer taskState;
private Integer rejectFlag;
private List<ApproverList> approverList;
}

/**
* 带递归和额外信息补充的mapping
* @author zhangjy
* @date 2020-03-18
*/
@Mapper
public abstract class TaskListConverter {

public static TaskListConverter INSTANCE = Mappers.getMapper(TaskListConverter.class);

@Mapping(source = "approverList", target = "approverlDTOList")
public abstract TaskListDTO toDTO(TaskList task);

public abstract List<TaskListDTO> toDTO(List<TaskList> lst);

// 递归转换主pojo内的
public abstract ApproverlDTO toDTO(ApproverList approver);

// 主pojo枚举名称补充
@AfterMapping
protected void calledWithSourceAndTarget(TaskList task,
@MappingTarget TaskListDTO.TaskListDTOBuilder target) {
if (task == null || target == null) {
return;
}
// 审批状态,1:未处理,2:已处理
if (task.getTaskState() != null) {
target.taskStateText(task.getTaskState() == 1 ? "未处理" : "已处理");
}
}

// 内部pojo枚举名称补充
@AfterMapping
protected void calledWithSourceAndTarget(ApproverList approver,
@MappingTarget ApproverlDTO.ApproverlDTOBuilder target) {
if (approver == null || target == null) {
return;
}
// 审批结果 1:同意 2:驳回
if (approver.getApproveResult() != null) {
target.approveResultText(approver.getApproveResult() == 1 ? "同意" : "驳回");
}
}

}

示例:字符串List自动转json

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
@Data
public class CustomerNoteDTO implements Serializable {
private static final long serialVersionUID = 1783984956317260001L;
private Long id;
private Long customerId;
private List<String> images;
}

@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
@TableName(value = "customer_notes", schema = "crm")
public class CustomerNotes {

@TableId(type = IdType.AUTO)
private Long id;
private Long customerId;
private String images;
}

/**
* @author zhangjy
* @date 2020-03-25
*/
@ApplicationScope
@Component
@Slf4j
public class SpringMapper {
@Autowired
private Gson gson;

String map(List<String> values) {
try {
return gson.toJson(values);
} catch (Exception e) {
log.warn("list to json", e);
return null;
}
}

List<String> map(String json) {
try {
final TypeToken<?> tt = TypeToken.getParameterized(List.class, String.class);
return gson.fromJson(json, tt.getType());
} catch (Exception e) {
log.warn("json to list: {}", json, e);
return null;
}
}

}

/**
* DTO vs entity 转换器
*
* @author zhangjy
* @date 2020-03-23
*/
@Mapper(componentModel = "spring", uses = SpringMapper.class)
public interface CustomerNoteConverter {
CustomerNoteConverter INSTANCE = Mappers.getMapper(CustomerNoteConverter.class);

CustomerNotes fromCDTO(CustomerNoteDTO noteDTO);

CustomerNoteDTO toDTO(CustomerNotes note);

}

生成的代码:

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
@Component
public class CustomerNoteConverterImpl implements CustomerNoteConverter {

@Autowired
private SpringMapper springMapper;

@Override
public CustomerNotes fromCDTO(CustomerNoteDTO noteDTO) {
if ( noteDTO == null ) {
return null;
}

CustomerNotes customerNotes = new CustomerNotes();

customerNotes.setId( noteDTO.getId() );
customerNotes.setCustomerId( noteDTO.getCustomerId() );
customerNotes.setImages( springMapper.map( noteDTO.getImages() ) );

return customerNotes;
}

@Override
public CustomerNoteDTO toDTO(CustomerNotes note) {
if ( note == null ) {
return null;
}

CustomerNoteDTO customerNoteDTO = new CustomerNoteDTO();

customerNoteDTO.setId( note.getId() );
customerNoteDTO.setCustomerId( note.getCustomerId() );
customerNoteDTO.setImages( springMapper.map( note.getImages() ) );

return customerNoteDTO;
}
}

6. 参考

  1. Java Bean Copy框架性能对比
  2. 为什么阿里要求避免使用 Apache BeanUtils 进行属性复制?
  3. Performance of Java Mapping Frameworks
  4. MicroServices – DTO to Entity & Entity to DTO mapping – Libraries Comparison
  5. MapStruct Installation
  6. JMapper Philosophy

自建邮件服务器

相关知识

邮件服务器包含两大核心功能:发邮件和收邮件。

邮件投递的过程是:

  1. 发送方邮件客户端和邮件服务器通信,发送邮件。
  2. 邮件服务器收到邮件后,对邮件进行投递:如果是不同服务器的邮件地址,需要转投。
  3. 接收方邮件客户端和邮件服务器通信,收取邮件。

以上三步过程中,端与端之间都需要通信,就需要通信协议:

  1. 发送时,邮件客户端和邮件服务器间,使用的是SMTP
    • SMTP=Simple Mail Transfer Protocol,简单邮件传输协议
    • SMTP是一组用于从源地址到目的地址传输邮件的规范,通过它来控制邮件的中转方式。
  2. 邮件服务器间转投邮件时,使用的也是SMTP
  3. 接收时,邮件客户端和邮件服务器间,使用的是POP3或IMAP
    • POP3=Post Office Protocol 3,邮局协议的第3个版本
    • IMAP=Internet Mail Access Protocol,交互式邮件存取协议

所以,搭建一个完整的邮件服务器,要包括两部分服务:

  • 发送邮件用的SMTP服务
  • 接收邮件用的POP3、IMAP服务(可选择只用一种,推荐IMAP安全性高)

部署方案

SMTP

在网络上路由和传输电子邮件的服务叫MTA(Mail Transfer Agent,邮件传输代理)。Linux下流行的开源MTA有:

  1. Sendmail 现在也叫proofpoint(被Proofpoint, Inc 收购后,已商业化),是Linux平台上最流行也是最早的MTA。配置复杂,安全机制相对较弱。
  2. Postfix 是一款流行的跨平台MTA。它的初衷是替代Sendmail,所以借鉴了Sendmail的很多功能特性,但配置更简单、性能更好。还支持一些其他特性,包括:
    • 容器支持
    • 垃圾邮件控制
    • 支持多种协议
    • 支持数据库
    • 支持信箱
    • 邮件地址操纵
  3. Exim 是一款免费的MTA,支持类Unix系统。
  4. Qmail 简单、可靠 、高效,支持安全扩展。安装简单快速,支持邮件组,支持分用户管理邮件地址列表。支持SMTP和POP3。

POP3/IMAP

用于给最终的接收方投递邮件的服务叫MDU(Mail Delivery Agent,邮件投递代理)。Linux下流行的开源MDU有:

  1. Dovecot 是类Unix平台下一款开源的IMAP和POP3服务。 它的特点是轻量级、速度快且易于安装。
  2. UW IMAP 是一款IMAP协议服务器实现参考范例,由华盛顿大学开发。

主流方案

开源邮件服务器的主流方案是Postfix+Dovecot,其中 Postfix支持SMTP协议负责发送邮件,Dovecot支持POP3+IMAP协议负责接收邮件。

部署过程可参考:在Centos7下搭建邮件服务器

容器方案

现代的邮件服务器,除了基本的收发邮件还支持许多其他功能,如:日历、web邮件、垃圾邮件过滤等。要手动一个个集成部署这些组件服务比较麻烦。在容器化技术流行的今天,有许多大神提供了容器化的解决方案:

  • Mail-in-a-Box a mail server in a box. 只有一个镜像,集成了以下组件:
  • Mailu is a simple yet full-featured mail server as a set of Docker images. 开源免费,包含多个镜像,主要特性包括:
    • Standard email server, IMAP and IMAP+, SMTP and Submission
    • Advanced email features, aliases, domain aliases, custom routing
    • Web access, multiple Webmails and administration interface
    • User features, aliases, auto-reply, auto-forward, fetched accounts
    • Admin features, global admins, announcements, per-domain delegation, quotas
    • Security, enforced TLS, Letsencrypt!, outgoing DKIM, anti-virus scanner
    • Antispam, auto-learn, greylisting, DMARC and SPF
    • Freedom, all FOSS components, no tracker included
  • docker-mailserver A fullstack but simple mail server (smtp, imap, antispam, antivirus…). 只有配置文件,没有数据库,易于部署和升级。

DNS配置

邮件服务器部署好之后,还需要配置DNS,这样其他邮件地址服务器投递邮件时才能找到本邮件服务器。

例如:我的邮件地址是 zjy@crazy1984.com,发送方的邮件地址是fighter@163.com。那么要让163.com邮件服务器能找到crazy1984.com邮件服务器,就需要进行DNS配置。

MX

DNS配置邮件域名,要添加MX记录,可参考:How to Setup an Email Server on CentOS 7

我的MX配置:

1
2
3
记录类型:MX
主机记录:@.crazy1984.com
记录值:mx01.dm.aliyun.com

SPF

SMTP协议是个非常简单的协议,没有很好的考虑安全措施,发件人可以假冒任意地址,使得垃圾邮件和诈骗邮件横行。

SPF,全称为 Sender Policy Framework,即发件人策略框架。SPF通过查询DNS记录的方式,来检查和认证邮件发送方,因为DNS解析是不能任意假冒的。详细内容可参考:

我的SPF配置:

1
2
3
记录类型:TXT
主机记录:@.crazy1984.com
记录值:v=spf1 mx mx:mail.crazy1984.com include:spf1.dm.aliyun.com -all

以上是2018年11月前后,在研究自建邮件服务器过程中学习到的知识的一点记录,最终因为阿里云服务器端口限制问题(邮件必须relay过手审核),没有配置成功。以后找个其他云服务商试试。

命令行下批量清理redis中的错误数据

问题

程序开发过程中,因为错误代码导致在redis中写入了错误的数据,可能使得读取的时候报错。如果没有设置ttl自动过期,将长期留存在redis中。

例如,想在set的同时指定ttl,但是少给了一个TimeUnit参数:

1
redisTpl.opsForValue().set(key, siteId, expire);

使得调用方法:

1
2
3
4
// 从
void set(K key, V value, long timeout, TimeUnit unit);
// 变成
void set(K key, V value, long offset);

从而产生了垃圾数据,还不能自动过期。

KEY前缀

项目中使用redis的时候,需要严格规划KEY的前缀,这样:

  • 一来可以避免KEY冲突。
  • 二来可以按业务范围查询维护相关的KEY。

上边代码中,我们错误设置的KEY的prefix=org_site_。

清理

有了统一的KEY浅醉,我们可以用keys命令查询出所有相关的key。

1
2
3
4
5
6
7
8
redis.crazy1984.com:6379[2]> keys org_site_*
1) "org_site_12_0"
2) "org_site_12_174"
3) "org_site_12_1"
4) "org_site_344_174"
5) "org_site_344_3182"
6) "org_site_345_0"
7) "org_site_344_3178"

暴力清理

对于查出的key,如果可以全部清理,那么可以用以前介绍过的awk命令,自动生成del命令(将下边的get替换成del即可),然后执行。

1
2
3
4
5
6
7
8
9
10
$ echo "keys org_site_*" |redis-cli -h redis.crazy1984.com -n 2 --raw | awk '{print "get "$1}' | redis-cli -h redis.crazy1984.com -n 2 --raw 
12
12
1
12
12
344
1003181
345
344

条件清理

如果线上环境不允许我们暴力清理,只能定向清理错误的数据。那么如何处理呢?这时可以用上redis支持执行lua脚本的功能了。

以我们的情况为例,我们的value本想存入一个int,但是因为错误的设置,使得value的数据变长了。

比如:原来的value=123,因为错误的set把ttl作为了offset,使得实际的数据可能变成

1
2
3
4
redis.crazy1984.com:6379[2]> setrange org_site_123 100 123
(integer) 103
redis.crazy1984.com:6379[2]> get org_site_123
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00123"

正常情况下:value应该是个int,长度不会超过32 bytes。

错误情况下:因为ttl(本处使用的单位是秒)至少都是60以上,所以可以通过判断数据的长度来判断是否是错误的数据。lua脚本的参数以KEYS全局变量方式传入。

The arguments can be accessed by Lua using the KEYS global variable in the form of a one-based array (so KEYS[1], KEYS[2], …).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
local del_keys = {};
local used_keys = {};
-- lua脚本的参数以KEYS传入,是个map
for i,k in ipairs(KEYS) do
-- 按字符串处理获取value的长度
local ktest = redis.call("STRLEN", k)
local kval = redis.call("GET", k)
-- 正常情况下value是个int,长度不会超过10(max=2,147,483,647)
if ktest > 10 then
-- 删除错误的数据
redis.call("DEL", k)
local stest = string.format("DEL %s %d", k, ktest)
del_keys[#del_keys+1] = {stest, kval};
else
local stest = string.format("GET %s %d", k, ktest)
used_keys[#used_keys+1] = {stest, kval};
end
end
return del_keys;

把这段脚本保存为文件 redis_clean.lua

然后再结合redis的keys命令,把所有以org_site_开头的key作为参数传给lua处理。

1
2
3
$ echo "keys org_site_*" |redis-cli -h redis.crazy1984.com -n 2 --raw | xargs redis-cli -h redis.crazy1984.comm -n 2 --eval redis_clean.lua
1) 1) "DEL org_site_123 103"
2) "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00123"

问题解决。

logback过滤日志

问题

logback通过配置日志level可以关闭指定class或者package输出的日志。那么,有没有办法只过滤满足某些特征的部分日志,而不是全关呢?

方法

Logback用户手册 Chapter 7: Filters

  • 普通类型的Filter都通过添加到Appender来对日志进行过滤。
  • TurboFilter绑定到logging context,所以可以对所有的log事件进行过滤,它的处理范围更广。

本文主要介绍普通类型的Filter使用。

按日志级别过滤

1
2
3
4
5
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>

按内容进行过滤

dubbo的ZookeeperRegistry会info输出订阅provider的更新信息,若想过滤掉这个日志不输出,可以这么配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<appender name="RF" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<!-- defaults to type JaninoEventEvaluator -->
<evaluator>
<expression><![CDATA[return level>=INFO
&& logger.startsWith("org.apache.dubbo.registry.zookeeper.ZookeeperRegistry")
&& message.contains("[DUBBO] Notify urls for subscribe url");
]]>
</expression>
</evaluator>
<OnMismatch>NEUTRAL</OnMismatch>
<OnMatch>DENY</OnMatch>
</filter>
</appender>

按marker进行过滤

日志输出方式:

1
2
3
4
5
6
7
8
public class LogDemo {
static final Marker CF = MarkerFactory.getMarker("cf");
static final Logger logger = LoggerFactory.getLogger(LogDemo.class);

public void doSmth(){
logger.info(CF, "hello logback");
}
}

对所有符合 marker=cf 的日志,在appender=RFbyCF 中统一输出:

1
2
3
4
5
6
7
8
9
10
<appender name="RFbyCF" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<!-- defaults to type JaninoEventEvaluator -->
<evaluator>
<expression><![CDATA[return marker.contains("cf");]]></expression>
</evaluator>
<OnMismatch>DENY</OnMismatch>
<OnMatch>NEUTRAL</OnMatch>
</filter>
</appender>

通过marker可以对日志进行分类,然后统一输出到一个文件,方便使用处理。例如:进行统计分析。

mvn如何发布SNAPSHOT项目

问题

项目开发过程中,版本号是 x.y.x-SNAPSHOT,如何能够自动以release版本号发布项目,并自动更新成新的SNAPSHOT版本号?

步骤

使用插件:Maven Release Plugin

配置pom.xml

需要同时配置scm(本文用git)和distributionManagement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<scm>
<connection>scm:git:http://git.crazy1984.com/sale_java/sale-boot.git</connection>
<tag>HEAD</tag>
</scm>
<distributionManagement>
<repository>
<id>releases</id>
<url>http://repo.crazy1984.com/nexus/content/repositories/releases</url>
</repository>
<snapshotRepository>
<id>snapshots</id>
<url>http://repo.crazy1984.com/nexus/content/repositories/snapshots</url>
</snapshotRepository>
</distributionManagement>

mvn release:prepare

发布前准备:变更pom.xml中的版本为release版本号,并打scm tag。

会顺序执行以下步骤:

  • Check that there are no uncommitted changes in the sources
  • Check that there are no SNAPSHOT dependencies
  • Change the version in the POMs from x-SNAPSHOT to a new version (you will be prompted for the versions to use)
  • Transform the SCM information in the POM to include the final destination of the tag
  • Run the project tests against the modified POMs to confirm everything is in working order
  • Commit the modified POMs (git commit)
  • Tag the code in the SCM with a version name (this will be prompted for)
  • Bump the version in the POMs to a new value y-SNAPSHOT (these values will also be prompted for)
  • Commit the modified POMs (git commit)

mvn release:perform

发布:scm检出最新release版本的tag,执行mvn deploy。

示例

  • 原开发版本:1.1.0-SNAPSHOT
  • 发布版本:1.1.0
  • 新开发版本:1.2.0-SNAPSHOT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ mvn release:prepare
[INFO] Scanning for projects...
[INFO]
[INFO] -------------------< com.crazy1984:xfund-archetype >-------------------
[INFO] Building xfund-archetype 1.1.0-SNAPSHOT
[INFO] --------------------------[ maven-archetype ]---------------------------
[INFO]
[INFO] --- maven-release-plugin:2.5.3:prepare (default-cli) @ xfund-archetype ---
[INFO] Verifying that there are no local modifications...
[INFO] ignoring changes on: **/pom.xml.releaseBackup, **/pom.xml.next, **/pom.xml.tag, **/pom.xml.branch, **/release.properties, **/pom.xml.backup
[INFO] Executing: /bin/sh -c cd /Users/zhangjy/git/git.crazy1984.com/sale_java/sale-boot/archetype && git rev-parse --show-toplevel
[INFO] Working directory: /Users/zhangjy/git/git.crazy1984.com/sale_java/sale-boot/archetype
[INFO] Executing: /bin/sh -c cd /Users/zhangjy/git/git.crazy1984.com/sale_java/sale-boot/archetype && git status --porcelain .
[INFO] Working directory: /Users/zhangjy/git/git.crazy1984.com/sale_java/sale-boot/archetype
[WARNING] Ignoring unrecognized line: ?? archetype/release.properties
[INFO] Checking dependencies and plugins for snapshots ...
What is the release version for "xfund-archetype"? (com.crazy1984.sale:xfund-archetype) 1.1.0: :
What is SCM release tag or label for "xfund-archetype"? (com.crazy1984.sale:xfund-archetype) xfund-archetype-1.1.0: :
What is the new development version for "xfund-archetype"? (com.crazy1984.sale:xfund-archetype) 1.1.1-SNAPSHOT: : 1.2.0-SNAPSHOT
...

$ mvn release:perform
$ mvn release:clean

Checked vs Unchecked Exceptions

区别

检测异常和非检测异常的主要区别,是在编译时是否会进行检查
检测异常必须进行捕获处理或者抛出,否则编译时会报错无法通过。

用法

检测异常也是interface的一部分,是对接口方法的描述:接口调用时可能发生哪些异常,调用方需要关注如何处理这些异常,可以是try catch捕获后处理,也可以往外再抛出由上层调用方处理。检测异常在编程过程中就给出相应的提示,让调用方能够预见和安排如何处理相应的异常

非检测异常主要包括的是编程问题,无法在程序运行过程中恢复或者处理的(因为编程时预见不到)。例如:空制针、内存溢出等,是需要通过修改代码或者调整环境配置来解决的。

非检测异常虽然可以让接口调用起来更干净和方便,不需要麻烦的try catch处理,但这样会使调用方疏于处理应处理的异常。一般来说,不要为了避免麻烦而使用RuntimeException。

Java官方的建议是

  • If a client can reasonably be expected to recover from an exception, make it a checked exception.
    • 如果调用方可以从异常中恢复进行后续处理,那么使用检测异常。
  • If a client cannot do anything to recover from the exception, make it an unchecked exception.
    • 如果调用方不能够从异常中恢复,那么使用非检测异常。

简单概括:需要调用方在运行时关注并处理的异常,声明为检测性异常。

其他参考

https://stackoverflow.com/questions/499437/in-java-when-should-i-create-a-checked-exception-and-when-should-it-be-a-runti

At my last job, we ran into some real issues with Runtime exceptions being forgotten until they showed up in production (on agedwards.com), so we resolved to use checked exceptions exclusively.

在我的上一份工作中,我们遇到了一些真实案例,因为运行时异常被忽略而导致问题在生产环境才出现,所以后来我们只使用检测异常。

https://stackoverflow.com/questions/27578/when-to-choose-checked-and-unchecked-exceptions

Checked Exceptions should be used for predictable, but unpreventable errors that are reasonable to recover from.

Unchecked Exceptions should be used for everything else.

工利其器:shell使用

0. 环境要求

  • shell环境:mac 或 linux 或 cygwin

1. 按目录结构拷贝部分文件

想结合着通配符(ls命令支持的那种),将指定目录下的部分文件带着目录结构筛选出来,拷贝到另一个目录下。

例如:一个多module到mvn项目,将所有子项目src/main/resources目录下的yml文件拷贝出来备份。

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
$ ls -1 */src/main/resources/*.yml
crm-collector-xxx/src/main/resources/application.yml
crm-collector-yyy/src/main/resources/application.yml
crm-main/src/main/resources/application.yml
crm-service/src/main/resources/application-service.yml
$ ls -1 */src/main/resources/*.yml |cpio -pdmuv copy2zjy
copy2zjy/crm-collector-xxx/src/main/resources/application.yml
copy2zjy/crm-collector-yyy/src/main/resources/application.yml
copy2zjy/crm-main/src/main/resources/application.yml
copy2zjy/crm-service/src/main/resources/application-service.yml
20 blocks
$ tree copy2zjy
copy2zjy
├── crm-collector-xxx
│   └── src
│   └── main
│   └── resources
│   └── application.yml
├── crm-collector-yyy
│   └── src
│   └── main
│   └── resources
│   └── application.yml
├── crm-main
│   └── src
│   └── main
│   └── resources
│   └── application.yml
└── crm-service
└── src
└── main
└── resources
└── application-service.yml

16 directories, 4 files

相关命令

  • ls – list directory contents
  • cpio – copy files to and from archives
  • tree - list contents of directories in a tree-like format

2. 生成批量操作脚本

对于给定的参数列表,生成批量操作脚本。例如:给定一个文件或目录列表,批量删除

1
2
3
4
5
6
7
$ ls -d1 *service*/target
crm-service-api/target
crm-service/target

$ ls -d1 *service*/target | awk '{print "rm -rfv "$1}'
rm -rfv crm-service-api/target
rm -rfv crm-service/target

对于生成的批量脚本,可以拷贝粘贴执行,也可以直接执行:

1
$ ls -d1 *service*/target | awk '{print "rm -rfv "$1}' | sh

docker 批量删除镜像

1
$ docker images |grep {your-image-name} | awk '{print "docker rmi "$3}'

3. shell下的时间转换

时间转epoch seconds

1
2
3
4
5
6
$ date "+%s"
1567568345
$ date -v-3m "+%s" #3个月前
1559620329
$ date -v-3m -v-3d -v0H -v0M -v0S "+%s" #3个月3天前,时分秒设置为0
1559318400

epoch seconds 转时间

1
2
3
4
5
6
$ date -r 1567568345
Wed Sep 4 11:39:05 CST 2019
$ date -j -r 1567568345 "+%Y-%m-%d %H:%M:%S"
2019-09-04 11:39:05
$ date -j -r 1559318400 "+%Y-%m-%d %H:%M:%S"
2019-06-01 00:00:00

工利其器:mac篇

0. 基础安装

  • java >= 1.8
  • xcode
1
$ xcode-select --install

1. Homebrew https://brew.sh/

The missing package manager for macOS (or Linux)

1
2
3
4
5
6
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
$ brew install calc curl fd fzf httpie imagemagick jq rename tree wget zsh \
git-extras git-flow-avh pandoc snappy unar xz z zplug \
ant etcd maven go grpc protobuf pyenv \
mysql redis sqlite libressl openssl
$ brew upgrade libressl openssl

brew install 安装的程序:

  • calc 命令行下的计算器
  • curl http网页内容请求
  • wget The non-interactive network downloader. 支持续传
  • rename renames multiple files
  • unar 命令行下解压缩。可指定字符集解压zip内文件名带中文。

关闭brew的自动更新,设置环境变量

1
HOMEBREW_NO_AUTO_UPDATE=1

这样可以避免每次brew install时都执行update,太慢。
需要自己定期执行brew update获得最新的程序列表及版本。

2. iTerm2 https://www.iterm2.com/

iTerm2 is a replacement for Terminal and the successor to iTerm. It works on Macs with macOS 10.12 or newer. iTerm2 brings the terminal into the modern age with features you never knew you always wanted.

自定义设置

  • Preferences -> Profiles -> Keys
    • Load Preset…
    • 选择:Nextural Text Editing
    • 这样就可以支持用 CTRL + <CTRL + >跳转字符串了
  • Preferences -> Keys -> Hotkey
    • Show/hide iTerm2 with a system-wide hotkey 快捷键设置为:”command+.”

3. zsh

zsh可以作为bash的替代品。相比mac默认的bash,zsh有很多更强大更方便的功能。

1
2
3
4
$ brew install zsh zplug zsh-completions
$ vi /etc/shells
#设置zsh为默认,在文件末尾添加:/usr/local/bin/zsh
$ chsh -s /usr/local/bin/zsh

oh-my-zsh https://github.com/robbyrussell/oh-my-zsh

Oh My Zsh is an open source, community-driven framework for managing your zsh configuration.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ sh -c "$(wget -O- https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"
$ git clone git@github.com:igoradamenko/jira.plugin.zsh.git ~/.oh-my-zsh/custom/plugins/jira
$ git clone git@github.com:changyuheng/fz.git ~/.oh-my-zsh/custom/plugins/fz
$ vi ~/.zshrc
#修改plguins配置
plugins=(adb brew copydir copyfile cp dotenv docker docker-machine fd fzf osx urltools
git git-extras git-flow-avh golang httpie history jira mvn npm nvm spring
pip pyenv rsync vscode web-search xcode z zsh_reload)

# config by cf
export GOPATH=$(go env GOPATH)
PATH=$PATH:$GOPATH/bin

export JAVA_HOME=$(/usr/libexec/java_home)
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
PATH=$JAVA_HOME/bin:$PATH

export JIRA_URL=http://jira.crazy1984.com
export JIRA_NAME=zhangjy
# extract JIRA issue name [ZNKFN-1234] from: feature/ZNKFN-1234_xyz
export JIRA_BRANCH_REGEX="s/[a-zA-Z]+\/([A-Z]+-[0-9]+)([^0-9].*)?/\1/p"

上边启用了一些非常有用的插件:

  • adb、brew、docker、mvn 命令的提示与补全(按TAB TAB)
  • git、git-flow-avh、jira(git与jira的自动关联) 命令的提示与补全
  • jira:注意要修改上述的JIRA_NAME配置,改成你的账号

4. git

修改 ~/.gitconfig 配置:一些UI颜色的设置,一些别名的配置。

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
[user]
name = "Your Name Here"
email = "your.email@crazy1984.com"
[color]
ui = auto
branch = auto
diff = auto
status = auto
[color "branch"]
current = green
local = yellow
remote = red
[color "diff"]
meta = yellow bold
frag = magenta bold
old = red bold
new = green bold
[color "status"]
added = yellow
changed = green
untracked = cyan
[alias]
st = status
di = diff
ci = commit
co = checkout
br = branch
hist = log --graph --pretty=format:'%Cred%h%Creset %s%C(yellow)%d%Creset %Cgreen(%cr)%Creset [%an]' --abbrev-commit --date=relative
[core]
autocrlf = input
# display unicode file names
quotePath = false
[credential "https://git.yours.com"]
username = zhangjy
[http "https://git.yours.com"]
sslVerify = false

配置git的SSH Key

1
2
$ ssh-keygen -t rsa -C "your.email@crazy1984.com" -b 4096
$ pbcopy < ~/.ssh/id_rsa.pub

打开git web控制台:

  • 个人设置 => SSH Keys ,添加SSH Key,粘贴、保存。
  • 配置好SSH Key后,git clone使用ssh地址,pull和push的时候就不需要输入账户密码了

5. SDKMAN https://sdkman.io

SDKMAN! is a tool for managing parallel versions of multiple Software Development Kits on most Unix based systems. It provides a convenient Command Line Interface (CLI) and API for installing, switching, removing and listing Candidates. Formerly known as GVM the Groovy enVironment Manager, it was inspired by the very useful RVM and rbenv tools, used at large by the Ruby community.

https://sdkman.io/install

1
2
3
$ curl -s "https://get.sdkman.io" | bash
$ source "$HOME/.sdkman/bin/sdkman-init.sh"
$ sdk version

openjdk-alpine容器中的jvm如何dump

openjdk-alpine容器中的jvm如何dump

微服务架构之后,应用的jvm运行在容器之内,如果系统出现问题,如何对jvm进行dump?

有两种方法:

  1. 开启jmx,使用jvisualvm通过jmx连接jvm,生成dump。
  2. 使用docker命令,进入容器的shell环境,使用jmap命令生成dump。

本文主要介绍第二种方法。在之前的文章中说过,我们的项目使用Google的jib打包成镜像,使用的基础镜像是8-jdk-alpine。容器启动时第一个进程就是java:

1
2
3
4
5
6
[root@VM_16_16_centos pp]# docker exec -it pp_eureka_1 sh
/ # ps
PID USER TIME COMMAND
1 root 0:50 java -Xms256m -Xmx512m ......
94 root 0:00 sh
99 root 0:00 ps

使用alpine镜像会有个问题,如果java进程的pid=1,那么无法执行jdk的各种连接java进程的命令,会报如下错误:

Unable to get pid of LinuxThreads manager thread

其他os镜像未验证,相关issue:https://github.com/docker-library/openjdk/issues/76

解决的方法是:启动一个init进程(pid=1)来接收docker stop and docker kill的信号,它会转发信号给其他进程,负责关闭僵尸进程。java进程由init进程启动。

具体有以下两种做法。

docker run –init

在docker 1.13 之后的版本,可以在docker run时加上 --init 参数来实现。

1
2
3
4
5
6
7
$ docker run --rm -it --init openjdk:8-jdk-alpine
/ # ps
PID USER TIME COMMAND
1 root 0:00 /dev/init -- /bin/sh
7 root 0:00 /bin/sh
8 root 0:00 ps
/ #

如果是docker compse,可以在docker-compose中配置上init参数。

1
2
3
4
5
6
7
8
9
10
11
12
version: '2.2'
services:
web:
image: alpine:latest
init: true


version: '2.2'
services:
web:
image: alpine:latest
init: /usr/libexec/docker-init

需要注意的是,init参数对docker-compose.yml的文件格式版本有要求:

  1. v2版本,version必须配置为2.2或以上版本。
  2. v3版本,version必须配置为3.7或以上版本。
  3. 不同版本的compose文件,有docker版本兼容性要求

krallin/tini

安装Tini,使用tini作为入口进程,配置启动java进程。

1
2
3
RUN apk add --no-cache tini
# Tini is now available at /sbin/tini
ENTRYPOINT ["/sbin/tini", "--", "java", "-Xms256m", "-Xmx512m", ......]

实际上,docker的--init参数也是通过集成Tini实现的。

其他事项

上述两种方式,那种更好?关于这个问题,github上有相关的讨论。个人觉得,各有利弊:

  1. 使用docker --init 参数简单,但是需要运维人员注意,部署时存在人为操作遗漏的风险。
  2. 使用 Tini 不用担心人为失误,但用jib打包镜像会增加配置工作,需要为每个项目配置entrypoint参数。

另外需要注意的是,在centos下,docker必须是docker-1.13.1-88.git07f3374.el7.centos.x86_64之后的发行版本,之前版本有bug。这个问题耽误了我一天时间,在本地windows下都好使,部署到docker不好使,试了好久,最后发现是centos下docker版本bug,更新后就好了。