我们讲解了幂等框架的设计实现思路,正常情况下,幂等框架的处理流程已经被捋清楚了,先生成一个幂等号,传递给实现方,实现方记录幂等号或者使用幂等号去进行判重,但是幂等框架要处理的异常情况很多,这就是设计的复杂和难点,业务系统宕机,幂等框架异常

虽然幂等框架处理的异常,考虑到开发成本和易用性,对某些异常进行了妥协,交给人工介入

今天,针对幂等框架的设计,我们会讲解编码的实现,和限流框架的讲解相同,对于幂等框架,还原整个开发过程,从最小原型代码将起,讲解如何review发现问题,重构代码解决问题,逐步迭代,获得一个易读,易扩展,灵活的框架

1.0版本的功能雪球

我们这一节,主要是为了得到最初的最简原型版本

主要包含的功能由

实现生成幂等号的功能

实现存储,查询,删除幂等号的功能

我们先来看看,如何生成幂等号

幂等号的主要作用,在于标识两个接口请求是否是同一个业务请求,换句话说,两个接口请求是否是重试的关系,而非独立的两个请求,接口调用方需要在发送接口请求的时候,将幂等号一并发给接口实现方,从而判断幂等,那么如何生成幂等号呢?一般有两种生成方式,可以选择集中生成并且分派给调用方,或者是交给调用方自己生成

对于第一种的生成方式,需要服务提供方,提供一套幂等号的生成系统,并且提供对应的远程接口,调用方通过远程接口获取幂等号,然后,完全隐藏了幂等号的实现细节,当我们改动幂等号的生成算法的时候,调用方不用改动任何的代码

第二种方式,调用方按照和接口实现方预先商量好的算法,自己生成幂等号,然后传递给服务端,但是一旦需要修改幂等号的生成算法,就需要修改每个调用方的代码

如果,是交给调用方自己实现幂等号的生成算法,会有问题,一方面,会重复开发,违反DRY原则,另一方面,工程师的开发水平不齐,bug很多

所以,综合考虑幂等号的生成效率,和代码维护的成本,我们选择了第一种的实现方式,在此基础上进行改进,由幂等框架来统一提供幂等号生成算法的代码实现,并封装为开发类库,提供给各个调用方复用,最好还能不依赖于外部系统

幂等号的生成算法其实可以有很多,比如全局唯一的ID生成算法,简单点有UUID,复杂点可以将应用名拼接在UUID上,方便做排查

第二个问题,如何实现幂等号的分布式存储,删除,查询呢?

幂等号只是为了判重,数据库中,只需要存储一个幂等号就可以,不需要太复杂的数据结构,所以简单的键值型数据库就可以,比如Reids

在幂等判重逻辑中,先检查幂等号是否存在,没有存在,在将幂等号塞进去,多个线程,或者多个进程进行执行上面的逻辑时候,就会存在着竞争的关系

这就需要加锁去处理,让同一个时间只能有一个线程能够执行的下去,除此外,因为存在着进程之间的竞争,所以,还必须是分布式锁

分布式锁会增加开发的难度,但是Reids本身就是单线程的,Reids的检查-设置操作本身可以作为原子操作的,比如setnx(key,value)先检查key是否存在,如果存在,则返回结果0,不存在,则将key保存下来,将值设置为value,返回结果1,因为Reids本身是单线程执行命令的,所以不存在刚刚的并发问题

最小原型的实现

V1版本要实现的代码和思路,已经明确下来了,具体的代码实现如下,首先是和限流框架相同的实现方法,写出了MVP代码,然后基于这个版本做优化重构

V1版本的功能非常的简单,基本代码如下

public class Idempotence {

private JedisCluster jedisCluster;

public Idempotence(String redisClusterAddress, GenericObjectPoolConfig config) {

String[] addressArray= redisClusterAddress.split(“;”);

Set<HostAndPort> redisNodes = new HashSet<>();

for (String address : addressArray) {

String[] hostAndPort = address.split(“:”);

redisNodes.add(new HostAndPort(hostAndPort[0], Integer.valueOf(hostAndPort[1])));

}

this.jedisCluster = new JedisCluster(redisNodes, config);

}

public String genId() {

return UUID.randomUUID().toString();

}

public boolean saveIfAbsent(String idempotenceId) {

Long success = jedisCluster.setnx(idempotenceId, “1”);

return success == 1;

}

public void delete(String idempotenceId) {

jedisCluster.del(idempotenceId);

}

}

然后针对这个最小原型的代码,进行相关的MVP的重构

在代码可读性方面,我们针对构造函数,saveIfAbsense()函数的参数和返回值做了注释,将genId()函数改为全拼 generateId(),对于这个函数来说,全写更好点

在代码可扩展的方面,基于接口而非实现的编程,将幂等号的读写进行了独立,设计出了IdempotenceStorage接口和ReidsClusterIdempotenceStorage实现类,并且实现类实现了基于Reids Cluster的幂等号读写,如果需要替换新的幂等号读写方式,直接实现一个新的IdempotenceStorage接口实现类就可以了

除此外,按照接口隔离原则,我们将生成的幂等号的代码进行抽离,放在IdempotenceIdGenerator中,然后直接调用这个类,既可以了,将幂等号生成和幂等号存储剥离开来

在代码测试方面,我们将原本的构造函数中逻辑放出来,放在了parseHostAndPorts()函数,这个函数是private访问权限的,但是为了编写单元测试,设置为了Protected访问权限,并且,给与了对应的注解@VisibleForTesting

代码灵活性,我们为了方便传递,提供了一个新的构造函数,支持业务系统直接传递jedisCluster创建Idempotence对象

本章重点

我们使用了30行代码,实现了幂等框架,体现了思从深行从简,对于不到30行代码,还进行了相对应的优化,这说明,再小的代码都有优化的空间

// 代码目录结构

com.xzg.cd.idempotence

–Idempotence

–IdempotenceIdGenerator(幂等号生成类)

–IdempotenceStorage(接口:用来读写幂等号)

–RedisClusterIdempotenceStorage(IdempotenceStorage的实现类)

// 每个类的代码实现

public class Idempotence {

private IdempotenceStorage storage;

public Idempotence(IdempotenceStorage storage) {

this.storage = storage;

}

public boolean saveIfAbsent(String idempotenceId) {

return storage.saveIfAbsent(idempotenceId);

}

public void delete(String idempotenceId) {

storage.delete(idempotenceId);

}

}

public class IdempotenceIdGenerator {

public String generateId() {

return UUID.randomUUID().toString();

}

}

public interface IdempotenceStorage {

boolean saveIfAbsent(String idempotenceId);

void delete(String idempotenceId);

}

public class RedisClusterIdempotenceStorage {

private JedisCluster jedisCluster;

/**

* Constructor

* @param redisClusterAddress the format is 128.91.12.1:3455;128.91.12.2:3452;289.13.2.12:8978

* @param config should not be null

*/

public RedisIdempotenceStorage(String redisClusterAddress, GenericObjectPoolConfig config) {

Set<HostAndPort> redisNodes = parseHostAndPorts(redisClusterAddress);

this.jedisCluster = new JedisCluster(redisNodes, config);

}

public RedisIdempotenceStorage(JedisCluster jedisCluster) {

this.jedisCluster = jedisCluster;

}

/**

* Save {@idempotenceId} into storage if it does not exist.

* @param idempotenceId the idempotence ID

* @return true if the {@idempotenceId} is saved, otherwise return false

*/

public boolean saveIfAbsent(String idempotenceId) {

Long success = jedisCluster.setnx(idempotenceId, “1”);

return success == 1;

}

public void delete(String idempotenceId) {

jedisCluster.del(idempotenceId);

}

@VisibleForTesting

protected Set<HostAndPort> parseHostAndPorts(String redisClusterAddress) {

String[] addressArray= redisClusterAddress.split(“;”);

Set<HostAndPort> redisNodes = new HashSet<>();

for (String address : addressArray) {

String[] hostAndPort = address.split(“:”);

redisNodes.add(new HostAndPort(hostAndPort[0], Integer.valueOf(hostAndPort[1])));

}

return redisNodes;

}

}

课后思考

1.针对MVP代码,两个问题进行思考,其中一个问题,delete是返回void还是boolean,应不应该为幂等号生成算法抽象出一个接口

2.如果有接下来的版本,需要继续扩展哪些功能?

1.在框架里,使用删除操作的时候,应该是人工介入时候使用的吧,这时候返回一个boolean方便盘查,对于幂等号生成算法来说,我们是抽取出来作为一个类库去放在客户端调用的,那么客户端传递给我们的格式最好一致,所以不应该抽象出一个接口,只能有一个实现的话,接口没有甚意义

2.在新版本,支持配置文件中,配置不同的名称来选择不同的幂等读写方法,并且,利用注解来进行AOP的切面编程,让使用人员使用注解就可以进行相关的幂等性保证

发表评论

邮箱地址不会被公开。 必填项已用*标注