幂等定义

在计算机科学中,一个操作如果多次执行产生的影响与一次执行的影响相同,这样的操作即符合幂等性。在分布式系统中,服务消费方调用服务提供方的接口,多次调用的结果应该与一次调用的结果一样,这正是分布式环境下幂等性的语义。

需求背景

跨中心微服务操作需要进行幂等验证,分布式微服务架构服务间频繁使用网络通信,由于网络不可靠网络震荡、客户端重试都导致产生重复请求,传统方式根据流水查询日志表无法保证高并发情况下的众多分布式微服务幂等。同时mq消费服务多实例情况下也经常出现重复消费的情况,为保证消费端幂等重复创建日志表进行冗余存储造成资源浪费。
基于此需要提供一种安全可靠、统一的分布式服务幂等组件。

幂等性常用解决方案

全局唯一ID
根据业务生成一个全局唯一ID,在调用接口时会传入该ID,接口提供方会从相应的存储系统比如Redis中去检索这个全局ID是否存在,如果存在则说明该操作已经执行过了,将拒绝本次服务请求;否则将相应该服务请求并将全局ID存入存储系统中,之后包含相同业务ID参数的请求将被拒绝。

去重表
这种方法适用于在业务中有唯一标识的插入场景。比如在支付场景中,一个订单只会支付一次,可以建立一张去重表,将订单ID作为唯一索引。把支付并且写入支付单据到去重表放入一个事务中,这样当出现重复支付时,数据库就会抛出唯一约束异常,操作就会回滚。这样保证了订单只会被支付一次。

组件需具备功能点

  • 提供redis幂等切面(减少代码侵入)
  • 提供redis分布式锁幂等验证api 
  • 提供jdbc幂等表验证api
  • 提供双重幂等验证api
  • 提供服务幂等参数获取及修改接口(方便幂等验证方式有问题进行切换或关闭幂等)
  • 提供幂等表数据定时清理失效记录功能

组件功能列表

  1. 服务启动幂等参数加载方法:服务启动时调用幂等参数查询服务加载当前服务的幂等参数
  2. redis幂等验证类及方法:提供幂等验证方法,输入幂等流水,根据服务及流水查询redis是否存在,如果不存在记录服务及流水返回成功,如果存在则抛出幂等校验异常。
  3. redis幂等切面及注解类:切面类先执行幂等校验方法,后执行注解的业务方法,如果业务方法执行异常则删除对应幂等流水,便于业务侧重试
  4. 数据库幂等验证类及方法:支持基于数据库的幂等验证,输入幂等流水根据服务幂等参数插入幂等表,如果主键冲突则说明操作已被执行,抛出幂等校验异常。
  5. 数据库幂等保障类及方法:判断redis集群、redis幂等是否正常,服务幂等参数是否配置了redis与幂等表同时校验,如果配置了同时校验或者redis异常则执行方法内幂等表插入操作。保证redis异常时服务数据依然可以保证幂等性。
/**
* 幂等校验主要逻辑
*/
public DIdemResult idemCheck(String idemkey, String dbPartitionId, String dbType, String routeTag, DIdem dIdem) {
		//双重幂等校验
        if (DIdemTypes.DIDEM_MODE_DOUBLE.equals(dIdemParam.getImplementMode())) {
            DIdemResult idemResult;
            try {
                String key = getIdemKey(dIdem.funcId(), idemkey, dbPartitionId);
                idemResult = redisDIdem.idemCheck(key, dIdem);
            } catch (Exception ex) { 
                logger.error("幂等分布式锁执行异常,降级使用jdbc幂等表验证", ex);
                idemResult = jdbcDIdem.isExistIdem(dIdem.svcId(), dIdem.funcId(), idemkey, dbPartitionId, dbType, routeTag);
                if (idemResult.isSuccess()) {
                    CacheHandler.put(idemkey, String.valueOf(System.currentTimeMillis()));
                }
                return idemResult;
            }

            if (idemResult.getExecPosition() != DIdemTypes.DIDEM_EXEC_REDIS) {
                idemResult = jdbcDIdem.isExistIdem(dIdem.svcId(), dIdem.funcId(), idemkey, dbPartitionId, dbType, routeTag);
                if (idemResult.isSuccess()) {
                //redis验证失败,执行jdbc验证
                    CacheHandler.put(idemkey, String.valueOf(System.currentTimeMillis()));
                }
            }
            return idemResult;
        } else if (DIdemTypes.DIDEM_MODE_REDIS.equals(dIdemParam.getImplementMode())) {
        	//只配置了redis校验
            String key = getIdemKey(dIdem.funcId(), idemkey, dbPartitionId);
            return redisDIdem.idemCheck(key, dIdem);
        } else if (DIdemTypes.DIDEM_MODE_JDBC.equals(dIdemParam.getImplementMode())) {
        	//只配置了jdbc校验
            return jdbcDIdem.isExistIdem(dIdem.svcId(), dIdem.funcId(), idemkey, dbPartitionId, dbType, routeTag);
        } else if (DIdemTypes.DIDEM_MODE_CLOSE.equals(dIdemParam.getImplementMode())) {
            //关闭幂等验证
            return new DIdemResult(true, idemkey, DIdemTypes.DIDEM_EXEC_DOUBLE_SKIP);
        } else {
            return new DIdemResult(false, idemkey, DIdemTypes.DIDEM_EXEC_DOUBLE_SKIP);
        }
    }