为什么需要幂等

何谓幂等性

官方的说法叫做:一次或者N次请求同一个资源地址的返回结果应该是一致的 通俗的说幂等就是说要么对资源的就没有副作用比如查询操作,每次返回的结果一致(忽略数据的局部字段的不一致),要么是对请求的资源有副作用比如更新操作,但是需要做到只能更新一次,在一次更新需要提示已经更新成功,直接返回。由于网络的超时,或者远程服务宕机不包含在幂等性概念讨论考虑之内。幂等性在需要一致性场景强的业务中是默认的存在,在普通场景中也是减少业务数据库脏数据的利器。说白了这个幂等性是一个Web服务对外提供服务的承诺。

如何保证幂等

一般使用如下方式实现Web服务的幂等,数据库唯一索引,数据库乐观锁,分布式锁,令牌等

数据库唯一索引

数据库唯一索引这个实现最为简单,针对更新不频繁的业务表可以在其插入字段上增加唯一索引,然后在业务代码中增加捕获DuplicateKeyException,然后返回重复插入提示。如果说更新频繁的业务表则不可以使用唯一索引来保证数据不重复插入。因为对于有唯一索引的加锁MySQL会加两次,一次Xlock 一次是INSERT_INTENTION lock,如果此时争抢频繁导致了锁等待,那么很容易导致死锁。然后让业务无法使用。所以最简单的方式是启用一张新表来做这个幂等表(类似哨兵的概念)当这个表没有数据可以插入,如果已存在,则禁止后续业务操作。

 try{ 
            remoteService.insertSelective(remoteEntity); 
            return new Result(SUCCESS, "成功"); 
        }catch (DuplicateKeyException e){ 
            return new Result(EXIST_RESOURCE, "你已经支付了,请不要重复点击"); 
        }catch (Exception e){
            return new Result(SYSTEM_ERROR); 
        }
数据库乐观锁

数据库乐观锁实现也比较简单,不过针对项目设计初期需要考虑这个数据库version字段的是设计,每一次的update。delete操作的时候需要对version进行叠加。并且每一次需要对version进行条件校验。

分布式锁

这个实现方式依托与分布式锁,我在这里他提供一种借力Redis的实现方式供大家拍砖。我的实现是参照简书上的一个兄弟的设计,我参照canal的其中回调设计进行的一部分改良。使用自定义的注解用于拦截请求,在切面中处理。力求把redis的key打散,使用SPEL表达式来进行动态生成KEY。

自定义的注解

/**
 * @author: by Mood
 * @date: 2019-03-05 11:11:11
 * @Description: 幂等注解
 * @version: 1.0
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    /**
     * 幂等的key 使用SPEL表达式来创建#request.id+'-'+#request.code
     * @return
     */
    String express();
}

幂等注解切面

/**
 * @author: by Mood
 * @date: 2019-04-19 10:46:06
 * @Description: 幂等注解切面
 * @version: 1.0
 */
@Aspect
@Component
public class IdempotentAspect {
    private static final Logger LOGGER = LoggerFactory.getLogger(IdempotentAspect.class) ;
    @Autowired
    private Jedis jedis;
    @Around("@annotation(idempotent)" )
    public void invokeMethod(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
        Object resp = null;
        Class clazz=joinPoint.getTarget().getClass();
        String caller = getCaller(joinPoint);
        try{
            LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
            String [] paraNameArr=u.getParameterNames(clazz.getMethod(joinPoint.getSignature().getName(), AngelRequestParam.class));
            String requestKey =parseKey(idempotent.express(),paraNameArr,joinPoint.getArgs());
            LOGGER.info("Idempotent KEY:{} ",requestKey);
            resp = checkIdempotent(requestKey)? joinPoint.proceed():"已发生调用,禁止重复调用";
        }catch (Exception e){
            LOGGER.error("Idempotent create Error Info:{} ",e);
        }finally {
            directResponse(resp, caller);
        }
    }
    /**
     * 调用redis读取数据请求幂等key,如果存在,则禁止去调用业务方法
     * @param requestKey
     * @return
     */
    private Boolean checkIdempotent(String requestKey) {
        if (!RedisUtil.getInstance(jedis).getMonitorRequestKey(requestKey)){
            return RedisUtil.getInstance(jedis).getManagerRequestKey(requestKey);
        }
        return false;
    }

    /**
     * 幂等表达式 在当前版本boot无法自动解析。手动调用SpelExpressionParser去解析与一下。
     * @param express
     * @param paraNameArr
     * @param args
     * @return
     */
    private String parseKey(String express, String [] paraNameArr,Object[] args) {
        StandardEvaluationContext context = new StandardEvaluationContext();
        for(int i=0;i<paraNameArr.length;i++){
            context.setVariable(paraNameArr[i], args[i]);
        }
        return new SpelExpressionParser().parseExpression(express).getValue(context,String.class);
    }

    /**
     * 返回结果
     * @param resp
     * @param caller
     * @return
     */
    private static void directResponse(Object resp, String caller) {
        try {
            HttpServletResponse httpServletResponse = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getResponse();
            httpServletResponse.setCharacterEncoding("UTF-8");
            httpServletResponse.setContentType("application/json; charset=UTF-8");
            if (resp != null) {
                httpServletResponse.getWriter().write(resp.toString());
            } else {
                httpServletResponse.getWriter().write("resp is null" + caller);
            }
        } catch (Exception e) {
            LOGGER.error("IdempotentAspect.directResponse error", e);
        }
    }
    /**
     * 获取接口调用者
     * @param pjp
     * @return
     */
    public static String getCaller(ProceedingJoinPoint pjp) {
        // 获取简单类名
        String className = pjp.getSignature().getDeclaringTypeName();
        String simpleClassName = className.substring(className.lastIndexOf(".") + 1);
        // 获取方法名
        String methodName = pjp.getSignature().getName();
        return simpleClassName + "#"+ methodName;
    }

}

Redis工具类

/**
 * @author: by Mood
 * @date: 2019-04-19 10:46:06
 * @Description: 使用redis监控幂等KEY工具类
 * @version: 1.0
 */
@Setter
public class RedisUtil {
	//管理KEY
    private static volatile Map<String, String> managerRequestKey; 
    //监控KEY
    private static volatile Map<String, Boolean> monitorRequestKey; 
    public static int expire=5;
    private static RedisUtil INSTANCE;
    private RedisUtil(Jedis jedis){
        monitorRequestKey= MigrateMap.makeComputingMap(new Function<String, Boolean>() {
            public Boolean apply(String requestKey) {
                return false;
            }
        });
        managerRequestKey= MigrateMap.makeComputingMap(new Function<String, String>() {
            public String apply(String requestKey) {
                String result=jedis.set(requestKey, requestKey, "nx", "ex", expire);
                if(result == null || StringUtils.isEmpty(result.toString())){
                    monitorRequestKey.put("requestKey",false);
                }else if("OK".equals(result)){
                    monitorRequestKey.put("requestKey",true);
                }
                return result;
            }
        });
    }
    @SuppressWarnings("deprecation")
    public static RedisUtil getInstance(Jedis jedis){
        if (INSTANCE!=null){
            synchronized (RedisUtil.class){
                if(INSTANCE==null){
                    INSTANCE=new RedisUtil(jedis);
                }
            }
        }
        return INSTANCE;
    }

    /**
     * 获取KEY,如果不存在 则设置进行,原子性。
     * @param requestKey
     */
    public boolean getManagerRequestKey(String requestKey){
        managerRequestKey.get(requestKey);
        return monitorRequestKey.get(requestKey);
    }

    /**
     * 删除Key
     * @param jedis
     * @param requestKey
     */
    public void deleteRequestKey(Jedis jedis,String requestKey){
        jedis.decr(requestKey);
    }
    /**
     * 获取监控key
     * @param requestKey
     */
    public boolean getMonitorRequestKey(String requestKey){
       return monitorRequestKey.get(requestKey);
    }
}

Guava的预编译Map(当调用map.get(key),执行Function的apply方法。)

package com.google.common.collect;

import com.google.common.base.Function;

import java.util.concurrent.ConcurrentMap;

public class MigrateMap {

    @SuppressWarnings("deprecation")
    public static <K, V> ConcurrentMap<K, V> makeComputingMap(MapMaker maker,
                                                              Function<? super K, ? extends V> computingFunction) {
        return maker.makeComputingMap(computingFunction);
    }

    @SuppressWarnings("deprecation")
    public static <K, V> ConcurrentMap<K, V> makeComputingMap(Function<? super K, ? extends V> computingFunction) {
        return new MapMaker().makeComputingMap(computingFunction);
    }
}
令牌token
幂等使用不足
<think>好的,我现在需要回答用户关于等性问题的定义及其在IT领域中的影响的问题。首先,我需要确保自己正确理解等性的概念。根据用户提供的引用,等性在数学和计算机科学中指的是多次执行操作与一次执行的影响相同。引用[1]和[2]都提到了这一点,特别是引用[2]提到HTTP方法的等性,即多次相同请求的副作用应与单次请求相同。 接下来,用户还提到引用[4],其中讨论了在数据库更新中如何通过状态字段和条件更新来实现等性。例如,使用状态作为条件,如果更新操作的影响行数为0,则视为重复请求,直接返回成功。这说明在实际应用中,等性的实现方法多种多样,需要结合具体场景。 我需要组织回答的结构,首先给出等性的定义,然后说明其在IT领域的重要性,接着列举常见场景,最后给出解决方案。同时,用户可能希望了解等性带来的问题以及如何解决,所以需要涵盖这些方面。 需要注意引用用户提供的资料,比如引用[1]和[2]用于定义,引用[4]用于数据库的例子。此外,用户之前的问题涉及RabbitMQ的消息处理,可能等性在消息队列中也很重要,比如避免重复处理消息,但用户当前的提问并未直接关联,不过可以简要提及。 另外,要确保回答符合格式要求:使用中文,数学公式用$...$,独立公式用$$...$$,但当前问题可能不需要太多数学公式,但如果有相关例子可以用。同时,生成相关问题,放在最后。 需要检查是否有遗漏的关键点,比如HTTP方法的等性(如GET、PUT、DELETE是等的,而POST不是),分布式系统中的等性实现(如分布式锁,引用[4]提到),以及如何通过版本号、唯一ID等机制实现。 最后,确保回答清晰,结构合理,引用正确,并且语言自然,避免过于技术化,让用户容易理解。</think>### 等性问题的定义及影响分析 #### 一、等性的定义 **等性**(Idempotence)是计算机科学与数学中的核心概念,定义为: $$f(f(x)) = f(x)$$ 即对同一操作执行多次与执行一次的效果完全相同[^1]。在IT领域中,它表现为: - **接口层面**:多次调用同一接口对系统状态的影响等同于单次调用(例如HTTP的`PUT`、`DELETE`方法)[^2]。 - **数据操作**:重复执行更新或插入操作不会导致数据不一致(如引用[4]中通过状态字段校验实现)。 #### 二、等性问题的影响 若系统未实现等性,可能导致以下问题: 1. **重复数据**:例如用户多次点击提交按钮,导致订单重复创建。 2. **资金损失**:支付接口未等时,重复扣款可能引发财务纠纷。 3. **状态混乱**:消息队列中消费者重复处理消息,导致业务逻辑错误(如库存超卖)。 4. **资源浪费**:重复请求占用服务器资源,降低系统吞吐量。 #### 三、常见等性场景与解决方案 | **场景** | **问题** | **解决方案** | |---------------------------|-----------------------------|-----------------------------------------------------------------------------| | HTTP接口调用 | 网络超时重试导致重复提交 | 使用唯一请求ID + Redis原子性校验[^4] | | 数据库更新 | 并发更新覆盖数据 | 乐观锁(版本号)或条件更新(如`UPDATE table SET status=3 WHERE id=1 AND status=2`) | | 消息队列消费 | 消费者重复消费同一条消息 | 消息唯一ID + 本地事务表去重 | | 分布式事务 | 分支事务重复提交 | TCC模式(Try-Confirm-Cancel)或Saga补偿机制 | #### 四、等性实现的核心方法 1. **唯一标识法** 为每个操作生成全局唯一ID(如UUID、雪花算法ID),通过数据库唯一索引或Redis的`SETNX`命令实现去重: ```sql CREATE UNIQUE INDEX idx_request_id ON orders(request_id); ``` 2. **状态机控制** 限定操作必须按状态流转顺序执行(如订单状态:未支付 → 已支付 → 已完成),通过条件更新实现: ```java // 引用[4]示例:根据当前状态更新下一状态 update order set status = 3 where id = #{id} and status = 2; ``` 3. **令牌桶机制** 客户端先获取令牌,服务端校验令牌有效性后执行操作(适用于表单防重复提交): $$ \text{Token} = \text{Hash}(用户ID + 时间戳 + 随机数) $$ 4. **分布式锁** 对关键操作加锁(如Redis的RedLock算法),确保同一时刻仅一个请求能执行: ```java // 引用[4]中提到的分布式锁方案 String lockKey = "order_lock_" + orderId; boolean locked = redis.set(lockKey, "1", "NX", "EX", 30); if (locked) { try { // 执行业务逻辑 } finally { redis.del(lockKey); } } ``` #### 五、等性对系统设计的影响 1. **一致性保障**:在微服务架构中,等性是实现最终一致性的基础(如MQ消息重试机制)[^1]。 2. **容错能力增强**:系统可安全应对网络抖动、客户端重试等异常场景。 3. **复杂度权衡**:需根据业务需求选择实现方式,避免过度设计(如低频操作可简化校验逻辑)。 --- ### 相关问题 1. 如何在高并发场景下高效实现接口等性? 2. 等性与事务隔离级别(如RC、RR)有何关联? 3. 消息队列(如RabbitMQ、Kafka)如何结合等性避免消息重复消费? 4. 分布式锁在等性实现中的常见陷阱有哪些? [^1]: 微服务架构中,等是一致性方面的一个重要概念。 [^2]: HTTP方法的等性定义。 [^4]: 数据库状态字段实现等性的具体方法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值