接口的幂等性与分布式锁redisson

您所在的位置:网站首页 java如何保证接口幂等性 接口的幂等性与分布式锁redisson

接口的幂等性与分布式锁redisson

2024-03-12 06:32| 来源: 网络整理| 查看: 265

接口的幂等性与分布式锁redisson

采用token方式实现接口幂等性的示意图。 在这里插入图片描述

一、幂等性

一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外),即第一次请求的时候对资源产生了副作用,但是以后的多次请求都不会再对资源产生副作用

二、使用地方

1、 前端重复提交表单:在填写一些表格时候,用户填写完成提交,很多时候会因网络波动没有及时对用户做出提交成功响应,致使用户认为没有成功提交,然后一直点提交按钮,这时就会发生重复提交表单请求。 2、 用户恶意进行刷单:例如在实现用户投票这种功能时,如果用户针对一个用户进行重复提交投票,这样会导致接口接收到用户重复提交的投票信息,这样会使投票结果与事实严重不符。 3、接口超时重复提交:很多时候 HTTP客户端工具都默认开启超时重试的机制,尤其是第三方调用接口时候,为了防止网络波动超时等造成的请求失败,都会添加重试机制,导致一个请求提交多次。 4、 消息进行重复消费:当使用 MQ 消息中间件时候,如果发生消息中间件出现错误未及时提交消费信息,导致发生重复消费。

三、幂等性解决办法

1、token 机制实现 通过token 机制实现接口的幂等性,这是一种比较通用性的实现方法 2、基于 mysql唯一索引 实现 这种实现方式是利用mysql 唯一索引的特性。 3、基于 redis SETNX 命令实现 这种实现方式是基于 SETNX 命令实现的 SETNX key value:将key 的值设为 value ,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。

四、采用token实现接口幂等性的具体做法(springboot)

例如: 1、点击提交订单按钮:

① 服务端提供获取 Token 的接口,该 Token 可以是一个序列号,也可以是一个分布式 ID 或者 UUID 串。 ② 客户端调用接口获取 Token,这时候服务端会生成一个 Token 串,返回给前端一份,客户端拿到后应存到表单隐藏域中,后端存redis一份,以该 Token 作为 Redis 的键(注意设置过期时间)。

2、点击结算按钮

③ 客户端在执行提交表单时,把Token 存入到 Headers 中,执行业务请求带上该 Headers。 ④ 服务端接收到请求后从 Headers 中拿到Token,然后根据 Token 到 Redis 中查找该 key 是否存 ⑤ 服务端根据 Redis 中是否存该 key 进行判断,如果存在就将该 key 删除,然后正常执行业务逻辑。如果不存在就抛异常,返回重复提交的错误信息。

**那么该笔交易token只有一个,所以无论你点多少次结算,那我都能以此token去判断是否重复提交** 方法一、采用lua脚本,实现在redis中查找数据和删除数据的原子操作

①导包

org.springframework.boot spring-boot-starter-data-redis org.redisson redisson 3.9.0

配置yml

spring: redis: host: 127.0.0.1 port: 6379

②返回一个后端生成的token(比如:提交订单按钮)

/** * 创建 Token 存入 Redis,并返回该 Token * @param value 用于辅助验证的 value 值 * @return 生成的 Token 串 */ public String generateToken(String value) { String token = UUID.randomUUID().toString(); String key = IDEMPOTENT_TOKEN_PREFIX + token; /** * 在真实业务中 采用唯一标志 例如 流水号啊 */ redisTemplate.opsForValue().set(key, value, 5, TimeUnit.MINUTES); return token; }

③执行业务逻辑(比如:点击结算按钮) 控制层:

/** * 接口幂等性测试接口 * * @param token 幂等 Token 串 * @return 执行结果 */ @PostMapping("/test") @ApiOperation(value = "测试幂等性") public String test(HttpServletRequest request) { String token = request.getHeader("token"); // 获取用户信息(这里使用模拟数据) String userInfo = "mydlq"; // 根据 Token 和与用户相关的信息到 Redis 验证是否存在对应的信息 boolean result = tokenUtilService.validToken(token, userInfo); // 根据验证结果响应不同信息 if (result) { /** * 执行正常的逻辑 */ log.info("执行正常的逻辑………………"); } return result ? "正常调用" : "重复调用"; }

业务层:

/** * 验证 Token 正确性 * * @param token token 字符串 * @param value value 存储在Redis中的辅助验证信息 * @return 验证结果 */ public Boolean validToken(String token, String value) { // 设置 Lua 脚本,其中 KEYS[1] 是 key,KEYS[2] 是 value,这段lua脚本的意思是获取redis的KEYS[1]的值,与KEYS[2]的值作比较,如果相等则返回KEYS[1]的值并删除redis中的KEYS[1],否则返回0 String script = "if redis.call('get',KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end"; RedisScript 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; } 方法二、分布式锁

分布式锁解决本地锁部属中的,分布式部署多服务会同时访问redis。例如:某服务分布式部署了3份,那么在redis为null,至少有3个请求会进入数据库(已使用本地锁的情况下)。理论应该一个区数据库查出来在放入redis。所以,为了解决这个问题,引入redisson ①导包

org.springframework.boot spring-boot-starter-data-redis org.redisson redisson 3.9.0

②写配置类(这里也可以把地址配置到yml,方便管理)

@Configuration public class MyRedissonConfig { @Bean(destroyMethod = "shutdown") public RedissonClient redisson() throws IOException { //创建配置 Config config = new Config(); /** *如果有密码: *config.useSingleServer().setAddress("redis://120.78.179.242:6379").setPassword("123456") **/ config.useSingleServer().setAddress("redis://120.78.179.242:6379"); //根据Config创建出RedissonClient示例 RedissonClient redissonClient = Redisson.create(config); return redissonClient; } }

③ 业务代码 controller层:

/** * 分布式锁实现幂等性 */ @PostMapping("/distributeLock") @ApiOperation(value = "分布式锁实现幂等性") public String distributeLock(HttpServletRequest request) { String token = request.getHeader("token"); // 获取用户信息(这里使用模拟数据) String userInfo = "mydlq"; RLock lock = redissonClient.getLock(token); lock.lock(10, TimeUnit.SECONDS); try { Boolean flag = tokenUtilService.validToken2(token, userInfo); // 根据验证结果响应不同信息 if (flag) { /** * 执行正常的逻辑 */ log.info("执行正常的逻辑………………"); } return flag ? "正常调用" : "重复调用"; } catch (Exception e) { e.printStackTrace(); return "重复调用"; } finally { lock.unlock(); } }

service层:

public Boolean validToken2(String token, String userInfo) { if (StringUtils.isBlank(token)) { return false; } Boolean aBoolean = redisTemplate.hasKey(IDEMPOTENT_TOKEN_PREFIX + token); if (aBoolean) { //删除token Boolean isDeleted = redisTemplate.delete(IDEMPOTENT_TOKEN_PREFIX + token); if (!isDeleted) { return false; } return true; } else { return false; } }

总结:lua脚本的母的是保证只有一个线程抢到分布式锁,并且获取后就删除,拿redis数据和删除redis数据是原子操作。分布式锁是分布式中只有一个拿到锁,并且删除redis数据,谁先占位谁执行业务,并删除redis,其他再进来redis已无数据,无需执行了,从而实现幂等性。

5. redisson看门狗自动续期源码 private void renewExpiration() { RedissonLock.ExpirationEntry ee = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName()); if (ee != null) { Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() { public void run(Timeout timeout) throws Exception { RedissonLock.ExpirationEntry ent = (RedissonLock.ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName()); if (ent != null) { Long threadId = ent.getFirstThreadId(); if (threadId != null) { RFuture future = RedissonLock.this.renewExpirationAsync(threadId); future.onComplete((res, e) -> { if (e != null) { RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e); } else { if (res) { RedissonLock.this.renewExpiration(); } } }); } } } }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS); ee.setTimeout(task); } } protected RFuture renewExpirationAsync(long threadId) { return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)}); }


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3