通过TDD或者用户用例来看一下这个功能如何被使用:
///////// 使用方式一: 在业务代码中处理幂等 //////////// // 接口调用方 Idempotence idempotence = new Idempotence(); String idempotenceId = idempotence.createId(); Order order = createOrderWithIdempotence(..., idempotenceId); // 接口实现方 public class OrderController { private Idempotence idempontence; // 依赖注入 public Order createOrderWithIdempotence(..., String idempotenceId) { // 前置操作 boolean existed = idempotence.check(idempotenceId); if (existed) { // 两种处理方式: // 1. 查询order,并且返回; // 2. 返回duplication operation Exception } idempotence.record(idempotenceId); //...执行正常业务逻辑 } public Order createOrder(...) { //... } } ///////// 使用方式二:在框架层面处理幂等 ////////////// // 接口调用方 Idempotence idempotence = new Idempotence(); String idempotenceId = idempotence.createId(); //...通过feign框架将幂等号添加到http header中... // 接口实现方 public class OrderController { @IdempotenceRequired public Order createOrder(...) { //... } } // 在AOP切面中处理幂等 @Aspect public class IdempotenceSupportAdvice { @Autowired private Idempotence idempotence; @Pointcut("@annotation(com.xzg.cd.idempotence.annotation.IdempotenceRequired)") public void controllerPointcut() { } @Around(value = "controllerPointcut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { // 从HTTP header中获取幂等号idempotenceId // 前置操作 boolean existed = idempotence.check(idempotenceId); if (existed) { // 两种处理方式: // 1. 查询order,并且返回; // 2. 返回duplication operation Exception } idempotence.record(idempotenceId) Object result = joinPoint.proceed(); return result; } }
我们可以得出接口调用方生成幂等号,随着请求传入框架。框架从HTTP header或者接口中取得这个然后查询。如果幂等号已经存在说明任务正在执行直接返回;否则记录幂等号并执行任务。
易用性:接入成本低,易于学习。松耦合
性能:低延迟,因为在请求处理前加入了幂等判断逻辑。
容错性:高容错。不能因为幂等框架的异常影响接口服务的本身可用性。
接口调用方接收到请求,解析幂等号,传递给幂等框架,框架查询这个幂等号是否存在。如果存在则不需要重复执行;不存在则存储这个幂等号并执行相应的业务逻辑。
public class Idempotence { // comment-1: 如果要替换存储方式,是不是很麻烦呢? private JedisCluster jedisCluster; // comment-2: 如果幂等框架要跟业务系统复用jedisCluster连接呢? // comment-3: 是不是应该注释说明一下redisClusterAddress的格式,以及config是否可以传递进null呢? public Idempotence(String redisClusterAddress, GenericObjectPoolConfig config) { // comment-4: 这段逻辑放到构造函数里,不容易写单元测试呢 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); } // comment-5: generateId()是不是比缩写要好点? // comment-6: 根据接口隔离原则,这个函数跟其他函数的使用场景完全不同,这个函数主要用在调用方,其他函数用在实现方,是不是应该分别放到两个类中? public String genId() { return UUID.randomUUID().toString(); } // comment-7: 返回值的意义是不是应该注释说明一下? public boolean saveIfAbsent(String idempotenceId) { Long success = jedisCluster.setnx(idempotenceId, "1"); return success == 1; } public void delete(String idempotenceId) { jedisCluster.del(idempotenceId); } }
// 代码目录结构
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 interface IdempotenceStorage { boolean saveIfAbsent(String idempotenceId); void delete(String idempotenceId); } public class RedisClusterIdempotenceStorage implements IdempotenceStorage { 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; } }
public class IdempotenceIdGenerator { public String generateId() { return UUID.randomUUID().toString(); } }