接口幂等性实现--Token令牌
一、什么是接口幂等性
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子,支付过程中,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条,这就没有保证接口的幂等性。
简单的说就是 一个用户对于同一个操作发起一次或多次的请求,请求的结果一致。不会因为多次点击而产生多条数据。
二、什么情况需要对接口进行幂等性设置
- Get 方法用于获取资源。其一般不会也不应当对系统资源进行改变,所以是幂等的。
- Post 方法一般用于创建新的资源。其每次执行都会新增数据,所以不是幂等的。
- Put 方法一般用于修改资源。该操作则分情况来判断是不是满足幂等,更新操作中直接根据某个值进行更新,也能保持幂等。不过执行累加操作的更新是非幂等。
- Delete 方法一般用于删除资源。该操作则分情况来判断是不是满足幂等,当根据唯一值进行删除时,删除同一个数据多次执行效果一样。不过需要注意,带查询条件的删除则就不一定满足幂等了。例如在根据条件删除一批数据后,这时候新增加了一条数据也满足条件,然后又执行了一次删除,那么将会导致新增加的这条满足条件数据也被删除。
三、接口幂等性的处理方式有很多,数据库唯一主键、数据库乐观锁、令牌表+唯一约束、下游传递唯一序列号、同步锁(单体项目)、分布式锁如redis 等等,这里只阐述使用token令牌的方式:
- 客户端请求服务器接口获取token。
- 服务器将token返给客户端的同时将信息(这里包括用户信息、和token)存储到redis中。
- 请求业务接口时,将token放入header中进行接口请求。
- 服务器通过用户信息和token检查token是否还存在,如果存在就删除,如果不存在直接返回结果。
- 响应服务器请求结果。
四、具体代码实现
1、创建一个springboot项目。
2、maven依赖:
<dependencies>
<!-- mysql:MyBatis相关依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<!-- mysql:mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- mysql:阿里巴巴数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--springboot data redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
3、redis配置
redis: host: 127.0.0.1 port: 6379 database: 0 timeout: 1000 password: 961230 lettuce: pool: max-active: 100 max-wait: -1 min-idle: 0 max-idle: 20 ssl: false
4、创建controller层
package com.dongliang.lcnorder.controller; import com.dongliang.lcnorder.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; /** * @author D-L * @version 1.0.0 * @ClassName UserController.java * @Description 接口幂等性实现--Token令牌 * @createTime 2021-05-26 16:40:00 */ @Slf4j @RestController public class UserController { @Autowired private UserService userService; /** * 获取 Token 接口 * @return */ @GetMapping("/getTokenInfo/{uid}") public String getTokenInfo(@PathVariable("uid") String uid) { return userService.generateToken(uid); } /** * 修改用户账户余额 (账户余额加100.0) Modify user account balance * @param token * @param uid 用户编码 * @return 返回调用结果 */ @PostMapping("/modifyUserAccountBalance/{uid}") public String modifyUserAccountBalance(@RequestHeader(value = "token") String token , @PathVariable("uid") String uid) { // 根据 Token 和与用户相关的信息到 Redis 验证是否存在对应的信息 boolean result = userService.modifyUserAccountBalance(token, uid); return result ? "正常调用" : "重复调用"; } }
5、创建service类
package com.dongliang.lcnorder.service; import com.dongliang.lcnorder.dao.UserDao; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.stereotype.Service; import java.util.Arrays; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * @author D-L * @version 1.0.0 * @ClassName UserService.java * @Description 接口幂等性实现--Token令牌 * @createTime 2021-05-26 16:27:00 */ @Slf4j @Service public class UserService { @Autowired private StringRedisTemplate redisTemplate; @Autowired private UserDao userDao; /** * 存入 Redis 的 Token 键的前缀 */ private static final String IDEMPOTENT_TOKEN_PREFIX = "user_token:"; /** * 创建 Token 存入 Redis,并返回该 Token * @param value * @return */ public String generateToken(String uid) { // 实例化生成 ID 工具对象 String token = UUID.randomUUID().toString(); // 设置存入 Redis 的 Key String key = IDEMPOTENT_TOKEN_PREFIX + token; // 存储 Token 到 Redis,且设置过期时间为5分钟 redisTemplate.opsForValue().set(key, uid, 5, TimeUnit.MINUTES); // 返回 Token return token; } /** * 修改账户余额 * @param token * @param uid * @return */ public boolean modifyUserAccountBalance(String token, String uid) { //验证 Token 正确性 boolean validToken = this.validToken(token, uid); if(validToken){ //执行具体业务逻辑 账户余额加100.00 int result = userDao.modifyUserAccountBalance(uid); return true; } return false; } /** * 验证 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; } }