我们讲解了幂等框架的设计实现思路,正常情况下,幂等框架的处理流程已经被捋清楚了,先生成一个幂等号,传递给实现方,实现方记录幂等号或者使用幂等号去进行判重,但是幂等框架要处理的异常情况很多,这就是设计的复杂和难点,业务系统宕机,幂等框架异常
虽然幂等框架处理的异常,考虑到开发成本和易用性,对某些异常进行了妥协,交给人工介入
今天,针对幂等框架的设计,我们会讲解编码的实现,和限流框架的讲解相同,对于幂等框架,还原整个开发过程,从最小原型代码将起,讲解如何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的切面编程,让使用人员使用注解就可以进行相关的幂等性保证