精华内容
下载资源
问答
  • 什么叫幂等?如何实现幂等性?

    千次阅读 2020-12-18 12:55:16
    如何实现幂等性? 非商业转载,如有侵权,可联系本人删除 什么叫幂等?如何实现幂等性? 什么是幂等性? 幂等是一个数学与计算机学概念,在数学中某一元运算为幂等时,其作用在任一元素两次后会和其作用一次的...

    转载自:什么叫幂等?如何实现幂等性?

    非商业转载,如有侵权,可联系本人删除

    什么叫幂等?如何实现幂等性?

    什么是幂等性?

    幂等是一个数学与计算机学概念,在数学中某一元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同。

     

    在计算机中编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。

    幂等函数或幂等方法是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。

    什么是接口幂等性?

    HTTP/1.1中,对幂等性进行了定义。它描述了一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外),即第一次请求的时候对资源产生了副作用,但是以后的多次请求都不会再对资源产生副作用。

    这里的副作用是不会对结果产生破坏或者产生不可预料的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。

    为什么需要实现幂等性?

    在接口调用时一般情况下都能正常返回信息不会重复提交,不过在遇见以下情况时可以就会出现问题,如:

    1. 前端重复提交表单:在填写一些表格时候,用户填写完成提交,很多时候会因网络波动没有及时对用户做出提交成功响应,致使用户认为没有成功提交,然后一直点提交按钮,这时就会发生重复提交表单请求。

    2. 用户恶意进行刷单:例如在实现用户投票这种功能时,如果用户针对一个用户进行重复提交投票,这样会导致接口接收到用户重复提交的投票信息,这样会使投票结果与事实严重不符。

    3. 接口超时重复提交:很多时候 HTTP 客户端工具都默认开启超时重试的机制,尤其是第三方调用接口时候,为了防止网络波动超时等造成的请求失败,都会添加重试机制,导致一个请求提交多次。

    4. 消息进行重复消费:当使用 MQ 消息中间件时候,如果发生消息中间件出现错误未及时提交消费信息,导致发生重复消费。

     

    使用幂等性最大的优势在于使接口保证任何幂等性操作,免去因重试等造成系统产生的未知的问题。

    引入幂等性后对系统有什么影响?

    幂等性是为了简化客户端逻辑处理,能放置重复提交等操作,但却增加了服务端的逻辑复杂性和成本,其主要是:

    1. 把并行执行的功能改为串行执行,降低了执行效率。

    2. 增加了额外控制幂等的业务逻辑,复杂化了业务功能;

    所以在使用时候需要考虑是否引入幂等性的必要性,根据实际业务场景具体分析,除了业务上的特殊要求外,一般情况下不需要引入的接口幂等性。

    Restful API 接口幂等性如何?

    现在流行的 Restful 推荐的几种 HTTP 接口方法中,分别存在幂等行与不能保证幂等的方法,如下:

    1. 满足幂等

    2. x 不满足幂等

    3. - 可能满足也可能不满足幂等,根据实际业务逻辑有关

    图片

    方案一:数据库唯一主键实现幂等性

    数据库唯一主键的实现主要是利用数据库中主键唯一约束的特性,一般来说唯一主键比较适用于“插入”时的幂等性,其能保证一张表中只能存在一条带该唯一主键的记录。

    使用数据库唯一主键完成幂等性时需要注意的是,该主键一般来说并不是使用数据库中自增主键,而是使用分布式 ID 充当主键,这样才能能保证在分布式环境下 ID 的全局唯一性。

    适用操作

    • 插入操作

    • 删除操作

    使用限制

    • 需要生成全局唯一主键 ID;

    主要流程

    图片

    主要流程如下:

    1. 客户端执行创建请求,调用服务端接口。

    2. 服务端执行业务逻辑,生成一个分布式 ID,将该 ID 充当待插入数据的主键,然 后执数据插入操作,运行对应的 SQL 语句。

    3. 服务端将该条数据插入数据库中,如果插入成功则表示没有重复调用接口。如果抛出主键重复异常,则表示数据库中已经存在该条记录,返回错误信息到客户端。

    方案二:数据库乐观锁实现幂等性

    数据库乐观锁方案一般只能适用于执行更新操作的过程,我们可以提前在对应的数据表中多添加一个字段,充当当前数据的版本标识。

    这样每次对该数据库该表的这条数据执行更新时,都会将该版本标识作为一个条件,值为上次待更新数据中的版本标识的值。

    适用操作

    • 更新操作

    使用限制

    • 需要数据库对应业务表中添加额外字段

    描述示例

    图片

    例如,存在如下的数据表中:

    图片

    为了每次执行更新时防止重复更新,确定更新的一定是要更新的内容,我们通常都会添加一个 version 字段记录当前的记录版本,这样在更新时候将该值带上,那么只要执行更新操作就能确定一定更新的是某个对应版本下的信息。

    图片

    这样每次执行更新时候,都要指定要更新的版本号,如下操作就能准确更新 version=5的信息:

    UPDATE my_table SET price=price+50,version=version+1 WHERE id=1 AND version=5
    

    上面 WHERE 后面跟着条件 id=1 AND version=5 被执行后,id=1 的 version 被更新为 6,所以如果重复执行该条 SQL 语句将不生效,因为 id=1 AND version=5 的数据已经不存在,这样就能保住更新的幂等,多次更新对结果不会产生影响。

    方案三:防重 Token 令牌实现幂等性

    针对客户端连续点击或者调用方的超时重试等情况,例如提交订单,此种操作就可以用 Token 的机制实现防止重复提交。

    简单的说就是调用方在调用接口的时候先向后端请求一个全局 ID(Token),请求的时候携带这个全局 ID 一起请求(Token 最好将其放到 Headers 中),后端需要对这个 Token 作为 Key,用户信息作为 Value 到 Redis 中进行键值内容校验,如果 Key 存在且 Value 匹配就执行删除命令,然后正常执行后面的业务逻辑。如果不存在对应的Key 或 Value 不匹配就返回重复执行的错误信息,这样来保证幂等操作。

    适用操作

    • 插入操作

    • 更新操作

    • 删除操作

    使用限制

    • 需要生成全局唯一 Token 串

    • 需要使用第三方组件 Redis 进行数据效验

    主要流程:

    图片

    1. 服务端提供获取 Token 的接口,该 Token 可以是一个序列号,也可以是一个分布式 ID 或者 UUID 串。

    2. 客户端调用接口获取 Token,这时候服务端会生成一个 Token 串。

    3. 然后将该串存入 Redis 数据库中,以该 Token 作为 Redis 的键(注意设置过期时间)。

    4. 将 Token 返回到客户端,客户端拿到后应存到表单隐藏域中。

    5. 客户端在执行提交表单时,把 Token 存入到 Headers 中,执行业务请求带上该 Headers

    6. 服务端接收到请求后从 Headers 中拿到 Token,然后根据 Token 到 Redis 中查找该 key 是否存在。

    7. 服务端根据 Redis 中是否存该 key 进行判断,如果存在就将该 key 删除,然后正常执行业务逻辑。如果不存在就抛异常,返回重复提交的错误信息。

     

    注意,在并发情况下,执行 Redis 查找数据与删除需要保证原子性,否则很可能在并发下无法保证幂等性。其实现方法可以使用分布式锁或者使用 Lua 表达式来注销查询与删除操作。

    方案四: 下游传递唯一序列号实现幂等性

    所谓请求序列号,其实就是每次向服务端请求时候附带一个短时间内唯一不重复的序列号,该序列号可以是一个有序 ID,也可以是一个订单号,一般由下游生成,在调用上游服务端接口时附加该序列号和用于认证的 ID

    当上游服务器收到请求信息后拿取该 序列号 和下游 认证ID 进行组合,形成用于操作 Redis 的 Key,然后到 Redis 中查询是否存在对应的 Key 的键值对,根据其结果:

    1. 如果存在,就说明已经对该下游的该序列号的请求进行了业务处理,这时可以直接响应重复请求的错误信息。

    2. 如果不存在,就以该 Key 作为 Redis 的键,以下游关键信息作为存储的值(例如下游商传递的一些业务逻辑信息),将该键值对存储到 Redis 中 ,然后再正常执行对应的业务逻辑即可。

    适用操作

    • 插入操作

    • 更新操作

    • 删除操作

    使用限制

    • 要求第三方传递唯一序列号;

    • 需要使用第三方组件 Redis 进行数据效验;

    主要流程

    图片

    1. 下游服务生成分布式 ID 作为序列号,然后执行请求调用上游接口,并附带唯一序列号与请求的认证凭据ID

    2. 上游服务进行安全效验,检测下游传递的参数中是否存在序列号凭据ID

    3. 上游服务到 Redis 中检测是否存在对应的序列号认证ID组成的 Key,如果存在就抛出重复执行的异常信息,然后响应下游对应的错误信息。如果不存在就以该序列号认证ID组合作为 Key,以下游关键信息作为 Value,进而存储到 Redis 中,然后正常执行接来来的业务逻辑。

    上面步骤中插入数据到 Redis 一定要设置过期时间。这样能保证在这个时间范围内,如果重复调用接口,则能够进行判断识别。如果不设置过期时间,很可能导致数据无限量的存入 Redis,致使 Redis 不能正常工作。

    实现接口幂等示例

    这里使用防重 Token 令牌方案,该方案能保证在不同请求动作下的幂等性,实现逻辑可以看上面写的”防重 Token 令牌”方案,接下来写下实现这个逻辑的代码。

    1. Maven 引入相关依赖

    这里使用 Maven 工具管理依赖,这里在 pom.xml中引入SpringBootRedislombok 相关依赖。

    <dependencies>
            <!--springboot web-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <!--springboot data redis-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-pool2</artifactId>
            </dependency>
            <!--lombok-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
            </dependency>
        </dependencies>
    

    2. 配置连接 Redis 的参数

    在 application 配置文件中配置连接 Redis 的参数,如下:

    spring:
      redis:
        ssl: false
        host: 127.0.0.1
        port: 6379
        database: 0
        timeout: 1000
        password:
        lettuce:
          pool:
            max-active: 100
            max-wait: -1
            min-idle: 0
            max-idle: 20
    

    3. 创建与验证 Token 工具类

    创建用于操作 Token 相关的 Service 类,里面存在 Token 创建与验证方法,其中:

    1. Token 创建方法:使用 UUID 工具创建 Token 串,设置以 “idempotent_token:“+“Token串” 作为 Key,以用户信息当成 Value,将信息存入 Redis 中。

    2. Token 验证方法:接收 Token 串参数,加上 Key 前缀形成 Key,再传入 value值,执行 Lua 表达式(Lua 表达式能保证命令执行的原子性)进行查找对应 Key与删除操作。执行完成后验证命令的返回结果,如果结果不为空且非0,则验证成功,否则失败。

    @Slf4j
    @Service
    public class TokenUtilService {
    
        @Autowired
        private StringRedisTemplate redisTemplate;
    
        /**
         * 存入 Redis 的 Token 键的前缀
         */
        private static final String IDEMPOTENT_TOKEN_PREFIX = "idempotent_token:";
    
        /**
         * 创建 Token 存入 Redis,并返回该 Token
         *
         * @param value 用于辅助验证的 value 值
         * @return 生成的 Token 串
         */
        public String generateToken(String value) {
            // 实例化生成 ID 工具对象
            String token = UUID.randomUUID().toString();
            // 设置存入 Redis 的 Key
            String key = IDEMPOTENT_TOKEN_PREFIX + token;
            // 存储 Token 到 Redis,且设置过期时间为5分钟
            redisTemplate.opsForValue().set(key, value, 5, TimeUnit.MINUTES);
            // 返回 Token
            return token;
        }
    
        /**
         * 验证 Token 正确性
         *
         * @param token token 字符串
         * @param value value 存储在Redis中的辅助验证信息
         * @return 验证结果
         */
        public boolean validToken(String token, String value) {
            // 设置 Lua 脚本,其中 KEYS[1] 是 key,KEYS[2] 是 value
            String script = "if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end";
            RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
            // 根据 Key 前缀拼接 Key
            String key = IDEMPOTENT_TOKEN_PREFIX + token;
            // 执行 Lua 脚本
            Long result = redisTemplate.execute(redisScript, Arrays.asList(key, value));
            // 根据返回结果判断是否成功成功匹配并删除 Redis 键值对,若果结果不为空和0,则验证通过
            if (result != null && result != 0L) {
                log.info("验证 token={},key={},value={} 成功", token, key, value);
                return true;
            }
            log.info("验证 token={},key={},value={} 失败", token, key, value);
            return false;
        }
    
    }
    

    4、创建测试的 Controller 类

    创建用于测试的 Controller 类,里面有获取 Token 与测试接口幂等性的接口,内容如下:

    @Slf4j
    @RestController
    public class TokenController {
    
        @Autowired
        private TokenUtilService tokenService;
    
        /**
         * 获取 Token 接口
         *
         * @return Token 串
         */
        @GetMapping("/token")
        public String getToken() {
            // 获取用户信息(这里使用模拟数据)
            // 注:这里存储该内容只是举例,其作用为辅助验证,使其验证逻辑更安全,如这里存储用户信息,其目的为:
            // - 1)、使用"token"验证 Redis 中是否存在对应的 Key
            // - 2)、使用"用户信息"验证 Redis 的 Value 是否匹配。
            String userInfo = "mydlq";
            // 获取 Token 字符串,并返回
            return tokenService.generateToken(userInfo);
        }
    
        /**
         * 接口幂等性测试接口
         *
         * @param token 幂等 Token 串
         * @return 执行结果
         */
        @PostMapping("/test")
        public String test(@RequestHeader(value = "token") String token) {
            // 获取用户信息(这里使用模拟数据)
            String userInfo = "mydlq";
            // 根据 Token 和与用户相关的信息到 Redis 验证是否存在对应的信息
            boolean result = tokenService.validToken(token, userInfo);
            // 根据验证结果响应不同信息
            return result ? "正常调用" : "重复调用";
        }
    
    }
    

    最后总结

    幂等性是开发当中很常见也很重要的一个需求,尤其是支付订单等与金钱挂钩的服务,保证接口幂等性尤其重要。在实际开发中,我们需要针对不同的业务场景我们需要灵活的选择幂等性的实现方式:

    1. 对于下单等存在唯一主键的,可以使用“唯一主键方案”的方式实现。

    2. 对于更新订单状态等相关的更新场景操作,使用“乐观锁方案”实现更为简单。

    3. 对于上下游这种,下游请求上游,上游服务可以使用“下游传递唯一序列号方案”更为合理。

    4. 类似于前端重复提交重复下单没有唯一ID号的场景,可以通过 Token 与Redis 配合的“防重 Token 方案”实现更为快捷。

    上面只是给与一些建议,再次强调一下,实现幂等性需要先理解自身业务需求,根据业务逻辑来实现这样才合理,处理好其中的每一个结点细节,完善整体的业务流程设计,才能更好的保证系统的正常运行。最后做一个简单总结,然后本博文到此结束,如下:

    图片

    展开全文
  • 如何实现幂等

    千次阅读 2019-11-14 17:09:47
    什么是幂等 幂等本来是数学上的概念,它的定义是这样的: 如果一个函数 f(x) 满足:f(f(x)) = f(x),则函数 f(x) ...方法一:利用数据库的唯一约束实现幂等 我们在数据库中建一张转账流水表,这个表有三个字段:...

    什么是幂等

    幂等本来是数学上的概念,它的定义是这样的: 如果一个函数 f(x) 满足:f(f(x)) = f(x),则函数 f(x) 满足幂等性。比如,求绝对值的函数,abs(x) = abs(abs(x))。

    在计算机领域用来描述一个操作、方法或者服务。一个幂等操作的特点是,其任意多次执行所产生的影响均与一次执行的影响相同。

    非幂等接口带来的问题

    我们把系统解耦隔离后,服务间的调用可能会有三个状态,一个是成功(Success),一个是失败(Failed),一个是超时(Timeout)。前两者都是明确的状态,而超时则是完全不知道是什么状态。比如,超时原因是网络传输丢包的问题,可能是请求时就没有请求到,也有可能是请求到了,返回结果时没有正常返回等等情况。于是我们完全不知道下游系统是否收到了请求,而收到了请求是否处理了,成功 / 失败的状态在返回时是否遇到了网络问题。总之,请求方完全不知道是怎么回事。
    举几个例子:

    • 订单创建接口,第一次调用超时了,然后调用方重试了一次。是否会多创建一笔订单?
    • 订单创建时,我们需要去扣减库存,这时接口发生了超时,调用方重试了一次。是否会多扣一次库存?
    • 当这笔订单开始支付,在支付请求发出之后,在服务端发生了扣钱操作,接口响应超时了,调用方重试了一次。是否会多扣一次钱?

    因为系统超时,而调用方重试一下,会给我们的系统带来不一致的副作用。
    在这种情况下,一般有两种处理方式。

    • 一种是需要下游系统提供相应的查询接口。上游系统在 timeout 后去查询一下。如果查到了,就表明已经做了,成功了就不用做了,失败了就走失败流程。
    • 另一种是通过幂等性的方式。也就是说,把这个查询操作交给下游系统,我上游系统只管重试,下游系统保证一次和多次的请求结果是一样的。

    对于第一种方式,需要对方提供一个查询接口来做配合。而第二种方式则需要下游的系统提供支持幂等性的交易接口。


    场景

    将林志玲账户的余额加 100 元

    方法一(推荐使用): 令牌机制(全局ID) (记录并检查操作)

    在发送消息时,给每条消息指定一个全局唯一的ID,消费时,先根据这个ID检查这条消息是否有被消费过,如果没有消费过更新数据,然后将消费状态置为已消费。

    1. 服务器端派发token并将token记录在缓存中, 客户端携带此token请求服务, 如果此token有效证明是有效请求处理请求, 并删除token。token无效忽略此请求。
    2. 客户端请求携带一个唯一标识(流水号),服务器判断此唯一标识是否已经存在,如果已存在忽略此请求。

    方法二:利用数据库的唯一约束实现幂等(利用数据库实现幂等性)

    我们在数据库中建一张转账流水表,这个表有三个字段:转账单 ID、账户 ID 和变更金额,然后给转账单 ID 和账户 ID 这两个字段联合起来创建一个唯一约束,这样对于相同的转账单 ID 和账户 ID,表里至多只能存在一条记录。

    方法三:为更新的数据设置前置条件(将方法实现幂等)

    1. 方法入参传入林志玲的账户余额,拿参数中的余额与数据库中的余额做比较如果相同则执行变更操作。
    2. 最简单的做法给数据增加一个版本号属性,每次更改数据前,比较当前数据的版本号是否和消息中的版本一致,如果不一致就拒绝更新数据,更新数据的同时将版本号 1。(和乐观锁原理一样)

    实现幂等的核心是判断请求是否重复,具体实现方式比较多,想设计一个高可用的幂等方法还需要我们具体业务具体分析具体设计。

    其他方式

    1.将请求参数通过md5加密或者hashcode()计算后值存入缓存
    2.添加个版本号,当前提交版本号大于上次提交版本号


    HTTP的幂等性

    HTTP GET方法用于获取资源,不应有副作用,所以是幂等的。 比如: GET

    http: / /www . bank . com/ account/123456,不会改变资源的状态,不论调用一次还是 N次都没有副作用。请注意,这里强调的是- -次和N次具有相同的副作用,而不是每次GET的结果相同。GET http:/ /www. news . com/latest-news这个HTTP请求可能会每次得到不同的结果,但它本身并没有产生任何副作用,因而是满足幂等性的。

    HTTP HEAD和GET本质是一样的,区别在于HEAD不含有呈现数据,而仅仅是HTTP头信息,不应用有副作用,也是幂等的。 有的人可能觉得这个方法没什么用,其实不是这样的。想象一个业务情景:欲判断某个资源是否存在,我们通常使用GET,但这里用HEAD则意义更加明确。也就是说,HEAD 方法可以用来做探活使用。

    HTTP OPTIONS主要用于获取当前URL所支持的方法,所以也是幂等的。 若请求成功,则.它会在HTTP头中包含一个名为“Allow”的头,值是所支持的方法,如“GET, POST"。

    HTTP DELETE方法用于删除资源,有副作用,但它应该满足幂等性。 比如: DELETE
    http://www. forum. com/article/4231,调用一次和N次对系统产生的副作用是相同的,即删掉ID为4231的帖子。因此,调用者可以多次调用或刷新页面而不必担心引起错误。

    HTTP POST方法用于创建资源,所对应的URI并非创建的资源本身,而是去执行创建动作的操作者,有副作用,不满足幂等性。 比如: POST http:/ /www. forum. com/ articles的语义是在http: //www. forum. com/articles下创建一篇帖子, HTTP响应中应包含帖子的创建状态以及帖子的URI。两次相同的POST请求会在服务器端创建两份资源,它们具有不同的URI;所以,POST方法不具备幂等性。

    HTTP PUT方法用于创建或更新操作,所对应的URI是要创建或更新的资源本身,有副作用,它应该满足幂等性。 比如: PUT http: / /www. forum/articles/4231的语义是创建或更新ID为4231的帖子。对同一URI进行多次PUT的副作用和一次PUT是相同的;因此,PUT方法具有幂等性。

    所以,对于POST的方式,很可能会出现多次提交的问题,就好比,我们在论坛中发贴时,有时候因为网络有问题,可能会对同一篇贴子出现多次提交的情况。对此的一-般的幂等性的设计如下。

    • 首先,在表单中需要隐藏一个token,这个token可以是前端生成的一个唯一的ID。用 于防止用户多次点击了表单提交按钮,而导致后端收到了多次请求,却不能分辨是否是重复的提交。这个token是表单的唯一标识。 (这种情况其实是 通过前端生成ID把POST变成了PUT。)
    • 然后,当用户点击提交后,后端会把用户提示的数据和这个token保存在数据库中。如果有重复提交,那么数据库中的token会做排它限制,从而做到幂等性。
    • 当然,更为稳妥的做法是,后端成功后向前端返回302跳转,把用户的前端页跳转到GET请求,把刚刚POST的数据给展示出来。如果是Web.上的最好还把之前的表单设置成过期,这样用户不能通过浏览器后退按钮来重新提交。这个模式又叫做OPRG模式(Post/Redirect/Get)。
    展开全文
  • spring boot 实现幂等

    千次阅读 2019-08-09 12:17:47
    spring boot 实现幂等性 背景 在分布式服务中,业务在高并发的情况下会消费者出现多次请求的情况.这个时候如果执行插入的业务操作,则数据库中出现多条数据.造成脏数据的产生.而且也是对资源的浪费. 此时我们需要做的...

    spring boot 实现幂等性

    背景

    在分布式服务中,业务在高并发的情况下会消费者出现多次请求的情况.这个时候如果执行插入的业务操作,则数据库中出现多条数据.造成脏数据的产生.而且也是对资源的浪费.
    此时我们需要做的就是阻止多页业务的处理操作.

    执行方案

    实现接口的幂等性,让请求只成功一次.这里需要保存一个唯一标示,在下一个请求执行时获取标示如果重复提交则阻止执行.

    代码实现

    创建一个自定义异常,阻止接口的下一步执行

    /**
     * 自定义异常类
     */
    public class IdempotentException extends RuntimeException{
    
        private static final long serialVersionUID = 1L;
    
        public IdempotentException(String message){
            super(message); 
        }
    
        @Override
        public String getMessage() {
            return super.getMessage();
        }
    
    }
    

    生成key值工具类

    import java.lang.reflect.Method;
    import java.security.MessageDigest;
    import java.security.NoSuchAlgorithmException;
    
    import com.alibaba.fastjson.JSON;
    
    /**
     * IdempotentKeyUtil
     * 生成key值工具类
     */
    public class IdempotentKeyUtil {
    
        /**
         * 对接口的参数进行处理生成固定key
         * @param method
         * @param args
         * @return
         */
        public static String  generate(Method method,Object... args) {
            StringBuilder stringBuilder = new StringBuilder(method.toString());
            for (Object  arg : args) {
                stringBuilder.append(toStrinhg(arg));
            }
            //进行md5等长加密
            return md5(stringBuilder.toString());
        }   
        /**
         * 使用jsonObject对数据进行toString,(保持数据一致性)
         * @param object
         * @return
         */
        public static String toStrinhg(Object obj){
            if( obj == null ){
                return "-";
            }
            return JSON.toJSONString(obj);
        }
        
        /**
         * 对数据进行MD5等长加密
         * @param str
         * @return
         */
        public static String md5(String str){
            StringBuilder stringBuilder = new StringBuilder();
            try {
                //选择MD5作为加密方式
                MessageDigest mDigest = MessageDigest.getInstance("MD5");
                mDigest.update(str.getBytes());
                byte b[] =  mDigest.digest();
                int j = 0;
                for (int i = 0,max = b.length; i < max; i++) {
                    j = b[i];
                    if(j < 0 ){
                        i += 256;
                    }else if(j < 16){
                        stringBuilder.append(0);
                    }
                    stringBuilder.append(Integer.toHexString(j));
                }
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
    		}
            return stringBuilder.toString();
        }
    }
    

    自定义注解

    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * 自定义注解
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Idempotent{
        //注解自定义redis的key的一部分
        String key();
        //过期时间
        long expirMillis();
    }
    

    AOP对我们自定义注解进行拦截处理

    import java.lang.reflect.Method;
    import java.util.Objects;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
    import org.springframework.data.redis.core.RedisCallback;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Component;
    import redis.clients.jedis.JedisCommands;
    
    
    /**
     * 自定义切点
     */
    @Component
    @Aspect
    @ConditionalOnClass(RedisTemplate.class)
    public class IdempotentAspect{
        private static final String KEY_TEMPLATE = "idempotent_%S";
        
        @Autowired
        private RedisTemplate<String,String> redisTemplate;
    
        /**
         * 切点(自定义注解)
         */
        @Pointcut("@annotation(com.idempotent.core.Idempotent)")
        public void executeIdempotent(){
            
        }
    
        /**
         * 切点业务
         * 
         * @throws Throwable
         */
        @Around("executeIdempotent()")
        public Object arountd(ProceedingJoinPoint jPoint) throws Throwable {
            //获取当前方法信息
            Method method = ((MethodSignature)jPoint.getSignature()).getMethod();
            //获取注解
            Idempotent idempotent = method.getAnnotation(Idempotent.class);
            //生成Key
            String key = String.format(KEY_TEMPLATE,idempotent.key()+"_"+IdempotentKeyUtil.generate(method, jPoint.getArgs()));
            //https://segmentfault.com/a/1190000002870317 -- JedisCommands接口的分析
            //nxxx的值只能取NX或者XX,如果取NX,则只有当key不存在是才进行set,如果取XX,则只有当key已经存在时才进行set
            //expx expx的值只能取EX或者PX,代表数据过期时间的单位,EX代表秒,PX代表毫秒
            // key value nxxx(set规则) expx(取值规则) time(过期时间)
            String redisRes =  redisTemplate.execute((RedisCallback<String>)conn ->((JedisCommands)conn.getNativeConnection()).set(key, key,"NX","EX",idempotent.expirMillis()));
            // Jedis jedis = new Jedis("127.0.0.1",6379);
            // jedis.auth("xuzz");
            // jedis.select(0);
            // String redisRes = jedis.set(key, key,"NX","EX",idempotent.expirMillis());
            if(Objects.equals("OK", redisRes)){
                return jPoint.proceed();
            }else{
                //throw new IdempotentException("sorry!! Interface duplicates requests, violating idempotency.");
                System.err.println("数据错误");
                return null;
            }
           
        }
    
    }
    

    service 接口

    import com.alibaba.fastjson.JSONObject;
    
    /**
     * TestService
     */
    public interface TestService {
        /**
         * 数据测试
         */
        public void print(JSONObject params);
    }
    

    service 实现类

    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.JSONObject;
    import com.idempotent.core.Idempotent;
    
    import org.springframework.stereotype.Service;
    
    /**
     * TestServiceImpl
     */
    @Service
    public class TestServiceImpl implements TestService {
    
        /**
         * 
         */
        @Idempotent(key = "com.idempotent.controller.IdempotentController",expirMillis = 100)
        @Override
        public void print(JSONObject params) {
            System.err.println(JSON.toJSONString(params));
    	} 
    }
    

    controller

    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.JSONObject;
    import com.idempotent.core.TestService;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * IdempotentController
     */
    @RestController
    public class IdempotentController {
        @Autowired
        private TestService testService;
    
        /**
         * 测试push
         */
        //@Idempotent(key = "com.idempotent.controller.IdempotentController",expirMillis = 100)
        @PostMapping("/push/test")
        public String pushTest(@RequestBody JSONObject params){
            for (int i = 0; i < 5; i++) {
                new Thread(()->{
                    try {
                        testService.print(params);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }).start();
            }
            return JSON.toJSONString(params);
        }
        
    }
    

    pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    	<modelVersion>4.0.0</modelVersion>
    	<parent>
    		<groupId>org.springframework.boot</groupId>
    		<artifactId>spring-boot-starter-parent</artifactId>
    		<version>2.1.7.RELEASE</version>
    		<relativePath/> <!-- lookup parent from repository -->
    	</parent>
    	<groupId>com.idempotent</groupId>
    	<artifactId>idempotent-dome</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<name>demo</name>
    	<description>Demo project for Spring Boot</description>
    
    	<properties>
    		<java.version>1.8</java.version>
    	</properties>
    
    	<dependencies>
    		<!-- <dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter</artifactId>
    		</dependency> -->
    		
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-web</artifactId>
    		</dependency>
    
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-test</artifactId>
    			<scope>test</scope>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-data-redis</artifactId>
    			<exclusions>
    				<exclusion>
    					<groupId>io.lettuce</groupId>
    					<artifactId>lettuce-core</artifactId>
    				</exclusion>
    			</exclusions>
    			<version>2.1.7.RELEASE</version>
    		</dependency>
    		<dependency>
    			<groupId>redis.clients</groupId>
    			<artifactId>jedis</artifactId>
    			<version>2.10.2</version>
    		</dependency>
    		<dependency>
    			<groupId>com.alibaba</groupId>
    			<artifactId>fastjson</artifactId>
    			<version>1.2.59</version>
    		</dependency>
    		<dependency>
    			<groupId>org.aspectj</groupId>
    			<artifactId>aspectjweaver</artifactId>
    			<version>1.9.4</version>
    		</dependency>
    		<dependency>
    			<groupId>org.apache.commons</groupId>
    			<artifactId>commons-pool2</artifactId>
    			<version>2.6.0</version>
    		</dependency>
    	</dependencies>
    
    	<build>
    		<plugins>
    			<plugin>
    				<groupId>org.springframework.boot</groupId>
    				<artifactId>spring-boot-maven-plugin</artifactId>
    			</plugin>
    		</plugins>
    	</build>
    </project>
    
    

    application.yml

    server:
      port: 7001
    spring:
      redis:
        database: 0
        host: 127.0.0.1
        port: 6379
        password: xxxx
        timeout: 1000
        pool:
          max-wait: 10
          max-active: 1
          max-idle: 2
          min-idle: 50
    

    环境

    jdk1.8,docker-redis
    

    参考:

    Henry_Jing

    展开全文
  • 用索引来实现幂等

    千次阅读 2016-06-17 11:25:37
    随着大数据的到来,分布式处理变... 我们了解了索引之后就能实现幂等了,从而可以避免重复插入。这在电商或者其他的项目中,处理重复的订单就简单多了。当然,这只是其中的一种方法,其他的方法以后遇到再与大家分享。
  • Redis+token机制实现幂等性操作

    千次阅读 2020-01-08 16:46:18
    Redis+Token机制实现幂等性操作 前言  使用redis+token实现幂等性操作,防止表单的重复提交和一些其他重复请求操作。有些接口需要保证操作的唯一性,比如:订单接口(重复点击下单,只会生成一个订单号);支付接口...
  • 看法:从以下方式来看,要保证幂等性,必须要有一个标识,至始至终的保持不变的标识,只有这样,后来的操作才可以依据这个标识判断是否曾经更改过,从而实现幂等性 方式1:使用乐观锁: 在系统设计的过程中,合理...
  • 如何实现幂等性,通常实现手段有以下几点: 数据库建立唯一索引,可以保证插入数据库的只有一条数据; Token机制,每次接口请求前先获取一个token,然后再下次请求的时候在header体中加上这个token,然后进行后天...
  • 有些接口可以天然的实现幂等性,比如查询接口,对于查询来说,你查询一次和两次,对于系统来说,没有任何影响,查出的结果也是一样。 除了查询功能具有天然的幂等性之外,增加、更新、删除都要保证幂等性。那么如何...
  • 目录 什么是幂等? 读和写请求都需要做幂等吗? 系统的哪部分需要做幂等?...写请求需要做幂等(对数据发生改变了就根据需要做幂等)。 系统的哪部分需要做幂等? 因为数据访问层和数据库直接联系,涉及到数据的
  • 在高并发的核心技术中如何实现幂等性 https://blog.csdn.net/rdhj5566/article/details/50646599 如何保证微服务接口的幂等性 https://blog.csdn.net/wangyan9110/article/details/70953273    必测的支付...
  • 创建订单实现幂等的一点思考

    千次阅读 2018-03-06 13:55:49
    幂等的概念 大部分文章都会说,同一个操作,进行多次操作后,结果是一样的,就可以说这个操作是支持幂等的。感觉不太准确,比如一个http get操作,可能每次的结果都不一样,但是其实是幂等的。看了很多文章,感觉...
  • MQ如何实现幂等

    2020-01-03 16:58:39
    原文:消息总线真的能保证幂等? 一、缘起 如《消息总线消息必达》所述,MQ消息必达,架构上有两个核心设计点: (1)消息落地 (2)消息超时、重传、确认 再次回顾消息总线核心架构,它由发送端、服务端、...
  • 实现幂等性的方案

    千次阅读 2017-11-09 12:05:06
    实现幂等性的技术方案 1. 查询操作 查询一次和查询多次,在数据不变的情况下,查询结果是一样的,select是天然的幂等操作。 2. 删除操作 删除操作也是幂等的,删除一次和多次删除都是把数据删除。...
  • 1、首先说下什么是幂等性呢 对于同一笔业务操作,不管调用多少次,得到的结果都是一样的。 2、幂等性设计 我们以对接支付宝充值为例,来分析支付回调接口如何设计? 如果我们系统中对接过支付宝充值功能的,我们...
  • 探讨一下实现幂等性的几种方式

    千次阅读 2019-05-14 09:39:27
    什么是幂等性? 对于同一笔业务操作,不管调用多少次,得到的结果都是一样的。 幂等性设计 我们以对接支付宝充值为例,来分析支付回调接口如何设计? 如果我们系统中对接过支付宝充值功能的,我们需要给支付宝提供一...
  • 数据库唯一主键的实现主要是利用数据库中主键唯一约束的特性,一般来说唯一主键比较适用于“插入”时的幂等性,其能保证一张表中只能存在一条带该唯一主键的记录。 使用数据库唯一主键完成幂等性时需要注意的是,该...
  • 业务中如何实现幂等

    千次阅读 2019-04-23 21:49:03
    这个时候需要做幂等处理,也就是一个消息只能被消费一次,那如何判断一个消息已经被消费了呢? 1、利用类似mysql这种具有唯一性约束的数据库机制 评论:需要落库,比较重 2、消费消息的时候,先根据唯一key值做...
  • 通过自定义注解实现接口防止重复提交-Token方案开始1.添加自定义注解类2.添加实现层3.添加拦截器并注册3.1添加拦截器3.2注册4.控制层4.1生产Token4.2业务控制层 开始 Token机制,防止页面重复提交。 业务要求: ...
  • 在实际的开发项目中,一个对外暴露的接口往往会面临很多次请求,我们来解释一下幂等的概念:任意多次执行所产生的影响均与一次执行的影响相同。按照这个含义,最终的含义就是 对数据库的影响只能是一次性的,不能重复...
  • pass 转载于:https://www.cnblogs.com/taomin/p/10464169.html
  • 方法:通过redis分布式锁 场景举例:同一用户连续提交评论内容 function isIdempotent( $uid, $content) { $redis = RedisService::getInstance(); $key = 'lock:' . md5($uid ....empty($...
  • 实现幂等性的技术方案 1. 查询操作 查询一次和查询多次,在数据不变的情况下,查询结果是一样的,select是天然的幂等操作。 2. 删除操作 删除操作也是幂等的,删除一次和多次删除都是把数据删除。(注意可能...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 144,740
精华内容 57,896
关键字:

怎么实现幂等