大家好,我是小悟
问题和概念
1、接口调用存在的问题
现如今我们的系统大多拆分为分布式SOA,或者微服务,一套系统中包含了多个子系统服务,而一个子系统服务往往会去调用另一个服务,而服务调用服务无非就是使用RPC通信或者restful,既然是通信,那么就有可能在服务器处理完毕后返回结果的时候挂掉,这个时候用户端发现很久没有反应,那么就会多次点击按钮,这样请求有多次,那么处理数据的结果是否要统一呢?那是肯定的!尤其在支付场景。
2、什么是接口幂等性
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条...,这就没有保证接口的幂等性。
实现思路
为需要保证幂等性的每一次请求创建一个唯一标识token, 先获取token, 并将此token存入redis, 请求接口时, 将此token放到header或者作为请求参数请求接口, 后端接口判断redis中是否存在此token: 如果存在, 正常处理业务逻辑, 并从redis中删除此token, 那么, 如果是重复请求, 由于token已被删除, 则不能通过校验, 返回请勿重复操作提示, 如果不存在, 说明参数不合法或者是重复请求, 返回提示即可。
代码实现
pom.xml
RedisUtil
package
com.
smartMap.
media.
common.
apiIdempotent.
utils;
import
org.
slf4j.
Logger;
import
org.
slf4j.
LoggerFactory;
import
org.
springframework.
beans.
factory.
annotation.
Autowired;
import
org.
springframework.
data.
redis.
core.
StringRedisTemplate;
import
org.
springframework.
stereotype.
Component;
import
java.
util.
concurrent.
TimeUnit;
/**
* @description
*/
@Component
public
class
RedisUtil {
private
static
final
Logger
logger
=
LoggerFactory.
getLogger(
RedisUtil.
class);
@Autowired
private
StringRedisTemplate
stringRedisTemplate;
/**
* 设值
* @param key
* @param value
* @return
*/
public
void
set(
String
key,
String
value) {
logger.
info(
"set key:{} value:{}",
key,
value);
stringRedisTemplate.
opsForValue().
set(
key,
value);
}
/**
* 设值
* @param key
* @param value
* @param expireTime 过期时间, 单位: s
* @return
*/
public
void
set(
String
key,
String
value,
int
expireTime) {
logger.
info(
"set key:{} value:{} expireTime:{}",
key,
value,
expireTime);
stringRedisTemplate.
opsForValue().
set(
key,
value,
expireTime,
TimeUnit.
SECONDS);
}
/**
* 取值
* @param key
* @return
*/
public
String
get(
String
key) {
logger.
info(
"get key:{}",
key);
return
stringRedisTemplate.
opsForValue().
get(
key);
}
/**
* 删除key
* @param key
* @return
*/
public
Boolean
del(
String
key) {
if (
exists(
key)) {
return
stringRedisTemplate.
delete(
key);
}
else {
logger.
error(
"del key:{}",
key
+
" 不存在");
return
false;
}
}
/**
* 判断key是否存在
* @param key
* @return
*/
public
Boolean
exists(
String
key) {
Boolean
exists
=
stringRedisTemplate.
hasKey(
key);
logger.
info(
"exists key:{} hasKey:{}",
key,
exists);
return
exists;
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
自定义注解 @ApiIdempotent
package
com.
smartMap.
media.
common.
apiIdempotent.
annotation;
import
java.
lang.
annotation.
ElementType;
import
java.
lang.
annotation.
Retention;
import
java.
lang.
annotation.
RetentionPolicy;
import
java.
lang.
annotation.
Target;
/**
* @description
*/
@Target(
ElementType.
METHOD)
@Retention(
RetentionPolicy.
RUNTIME)
public
@interface
ApiIdempotent {
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
响应状态码 ResponseCode
package
com.
smartMap.
media.
common.
apiIdempotent.
common;
/**
* @description
*/
public
enum
ResponseCode {
ILLEGAL_ARGUMENT(
10000,
"参数不合法"),
REPETITIVE_OPERATION(
10001,
"请勿重复操作"),
;
ResponseCode(
Integer
code,
String
msg) {
this.
code
=
code;
this.
msg
=
msg;
}
private
Integer
code;
private
String
msg;
public
Integer
getCode() {
return
code;
}
public
void
setCode(
Integer
code) {
this.
code
=
code;
}
public
String
getMsg() {
return
msg;
}
public
void
setMsg(
String
msg) {
this.
msg
=
msg;
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
常量 Constant
package
com.
smartMap.
media.
common.
apiIdempotent.
common;
/**
* @description
*/
public
class
Constant {
public
interface
Redis {
String
OK
=
"OK";
Integer
EXPIRE_TIME_MINUTE
=
60;
// 过期时间, 60s, 一分钟
Integer
EXPIRE_TIME_FIVE_MINUTE
=
5
*
60;
// 过期时间, 60s, 一分钟
Integer
EXPIRE_TIME_HOUR
=
60
*
60;
// 过期时间, 一小时
Integer
EXPIRE_TIME_DAY
=
60
*
60
*
24;
// 过期时间, 一天
String
TOKEN_PREFIX
=
"API_IDEMPOTENT_TOKEN:";
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
token接口 ApiIdempotentTokenService
package
com.
smartMap.
media.
common.
apiIdempotent.
service;
import
com.
smartMap.
media.
common.
utils.
R;
import
javax.
servlet.
http.
HttpServletRequest;
/**
* @description
*/
public
interface
ApiIdempotentTokenService {
R
createToken();
void
checkToken(
HttpServletRequest
request);
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
token接口实现类 ApiIdempotentTokenServiceImpl
package
com.
smartMap.
media.
common.
apiIdempotent.
service.
impl;
import
com.
smartMap.
media.
common.
apiIdempotent.
common.
Constant;
import
com.
smartMap.
media.
common.
apiIdempotent.
common.
ResponseCode;
import
com.
smartMap.
media.
common.
apiIdempotent.
service.
ApiIdempotentTokenService;
import
com.
smartMap.
media.
common.
apiIdempotent.
utils.
RedisUtil;
import
com.
smartMap.
media.
common.
exception.
RRException;
import
com.
smartMap.
media.
common.
utils.
R;
import
com.
smartMap.
media.
common.
utils.
UuidUtils;
import
org.
apache.
commons.
lang.
text.
StrBuilder;
import
org.
apache.
commons.
lang3.
StringUtils;
import
org.
springframework.
beans.
factory.
annotation.
Autowired;
import
org.
springframework.
stereotype.
Service;
import
javax.
servlet.
http.
HttpServletRequest;
/**
* @description
*/
@Service(
"apiIdempotentTokenService")
public
class
ApiIdempotentTokenServiceImpl
implements
ApiIdempotentTokenService {
private
static
final
String
API_IDEMPOTENT_TOKEN_NAME
=
"apiIdempotentToken";
@Autowired
private
RedisUtil
redisUtil;
@Override
public
R
createToken() {
String
str
=
UuidUtils.
randomUUID();
StrBuilder
token
=
new
StrBuilder();
token.
append(
Constant.
Redis.
TOKEN_PREFIX).
append(
str);
redisUtil.
set(
token.
toString(),
token.
toString(),
Constant.
Redis.
EXPIRE_TIME_FIVE_MINUTE);
return
R.
ok().
put(
"token",
token.
toString());
}
@Override
public
void
checkToken(
HttpServletRequest
request) {
String
token
=
request.
getHeader(
API_IDEMPOTENT_TOKEN_NAME);
// header中不存在token
if (
StringUtils.
isBlank(
token)) {
token
=
request.
getParameter(
API_IDEMPOTENT_TOKEN_NAME);
// parameter中也不存在token
if (
StringUtils.
isBlank(
token)) {
throw
new
RRException(
ResponseCode.
ILLEGAL_ARGUMENT.
getMsg(),
ResponseCode.
ILLEGAL_ARGUMENT.
getCode());
}
}
if (
!
redisUtil.
exists(
token)) {
throw
new
RRException(
ResponseCode.
REPETITIVE_OPERATION.
getMsg(),
ResponseCode.
REPETITIVE_OPERATION.
getCode());
}
Boolean
del
=
redisUtil.
del(
token);
if (
!
del) {
throw
new
RRException(
ResponseCode.
REPETITIVE_OPERATION.
getMsg(),
ResponseCode.
REPETITIVE_OPERATION.
getCode());
}
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
拦截器 ApiIdempotentInterceptor
package
com.
smartMap.
media.
common.
apiIdempotent.
interceptor;
import
com.
smartMap.
media.
common.
apiIdempotent.
annotation.
ApiIdempotent;
import
com.
smartMap.
media.
common.
apiIdempotent.
service.
ApiIdempotentTokenService;
import
org.
springframework.
beans.
factory.
annotation.
Autowired;
import
org.
springframework.
stereotype.
Component;
import
org.
springframework.
web.
method.
HandlerMethod;
import
org.
springframework.
web.
servlet.
handler.
HandlerInterceptorAdapter;
import
javax.
servlet.
http.
HttpServletRequest;
import
javax.
servlet.
http.
HttpServletResponse;
import
java.
lang.
reflect.
Method;
/**
* @description
*/
@Component
public
class
ApiIdempotentInterceptor
extends
HandlerInterceptorAdapter {
@Autowired
private
ApiIdempotentTokenService
apiIdempotentTokenService;
@Override
public
boolean
preHandle(
HttpServletRequest
request,
HttpServletResponse
response,
Object
handler) {
if (
!(
handler
instanceof
HandlerMethod)) {
return
true;
}
HandlerMethod
handlerMethod
= (
HandlerMethod)
handler;
Method
method
=
handlerMethod.
getMethod();
ApiIdempotent
methodAnnotation
=
method.
getAnnotation(
ApiIdempotent.
class);
if (
methodAnnotation
!=
null) {
check(
request);
}
return
true;
}
private
void
check(
HttpServletRequest
request) {
apiIdempotentTokenService.
checkToken(
request);
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
拦截器注册 WebConfig
package
com.
smartMap.
media.
common.
config;
import
com.
smartMap.
media.
common.
apiIdempotent.
interceptor.
ApiIdempotentInterceptor;
import
org.
springframework.
beans.
factory.
annotation.
Autowired;
import
org.
springframework.
context.
annotation.
Configuration;
import
org.
springframework.
web.
servlet.
config.
annotation.
InterceptorRegistry;
import
org.
springframework.
web.
servlet.
config.
annotation.
ResourceHandlerRegistry;
import
org.
springframework.
web.
servlet.
config.
annotation.
WebMvcConfigurer;
/**
* @description
*/
@Configuration
public
class
WebConfig
implements
WebMvcConfigurer {
@Autowired
private
ApiIdempotentInterceptor
apiIdempotentInterceptor;
@Override
public
void
addInterceptors(
InterceptorRegistry
registry) {
registry.
addInterceptor(
apiIdempotentInterceptor);
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
测试验证 controller
package
com.
smartMap.
media.
modules.
mobile.
controller;
import
com.
smartMap.
media.
common.
apiIdempotent.
annotation.
ApiIdempotent;
import
com.
smartMap.
media.
common.
apiIdempotent.
service.
ApiIdempotentTokenService;
import
com.
smartMap.
media.
common.
utils.
R;
import
org.
springframework.
beans.
factory.
annotation.
Autowired;
import
org.
springframework.
web.
bind.
annotation.
RequestMapping;
import
org.
springframework.
web.
bind.
annotation.
RestController;
/**
* @description
*/
@RestController
@RequestMapping(
"/mobile/test")
public
class
TestController {
@Autowired
private
ApiIdempotentTokenService
apiIdempotentTokenService;
/**
* 获取token
* @return
*/
@RequestMapping(
"getToken")
public
R
getToken() {
return
apiIdempotentTokenService.
createToken();
}
/**
* 测试接口幂等性, 在需要幂等性校验的方法上声明此注解即可
* @return
*/
@ApiIdempotent
@RequestMapping(
"testIdempotence")
public
R
testIdempotence() {
return
R.
ok(
"测试接口幂等性");
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
1、获取token

2、查看redis

3、利用jmeter测试工具模拟200个并发请求, 将上一步获取到的token作为参数



4、header或参数均不传token, 或者token值为空, 或者token值乱填, 均无法通过校验, 如token值为"123456"


注意点(非常重要)

上图中, 不能单纯的直接删除token而不校验是否删除成功, 会出现并发安全性问题, 因为, 有可能多个线程同时走到第51行, 此时token还未被删除, 所以继续往下执行, 如果不校验redisUtil.del(token)的删除结果而直接放行, 那么还是会出现重复提交问题, 即使实际上只有一次真正的删除操作。
总结
通过拦截器加注解的方式,就不用写很多重复的代码啦。
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海

1106

被折叠的 条评论
为什么被折叠?



