暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Springboot接口幂等性基于token实现方案

IT大咖说 2020-11-24
2022

什么是接口幂等

幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中,即f(f(x)) = f(x).简单的来说就是一个操作多次执行产生的结果与一次执行产生的结果一致。有些系统操作天生就具有幂等性例如数据库的select语句,但更多时候是需要程序员来做保证的,尤其是在分布式系统环境中,接口能不能做到保证幂等性对系统的影响可能是非常大的,例如很常见的支付下单等场景,由于分布式环境中网络的复杂性,用户误操作,网络抖动,消息重复,服务超时导致业务自动重试等等各种情况都可能会使线上数据产生了不一致,造成生产事故。

实现方案

1、查询操作:查询一次和查询多次,在数据不变的情况下,查询结果是一样的。select是天然的幂等操作;
2、删除操作:删除操作也是幂等的,删除一次和多次删除都是把数据删除。(注意可能返回结果不一样,删除的数据不存在,返回0,删除的数据多条,返回结果多个) ;
3、唯一索引:利用数据库新增脏数据。比如:支付宝的资金账户,支付宝也有用户账户,每个用户只能有一个资金账户,怎么防止给用户创建资金账户多个,那么给资金账户表中的用户ID加唯一索引,所以一个用户新增成功一个资金账户记录。要点:唯一索引或唯一组合索引来防止新增数据存在脏数据(当表存在唯一索引,并发时新增报错时,再查询一次就可以了,数据应该已经存在了,返回结果即可);
4、token机制:防止页面重复提交。采用token加redis或token加jvm内存。处理流程:1. 数据提交前要向服务的申请token,token放到redis或jvm内存,token有效时间;2. 提交后后台校验token,同时删除token,生成新的token返回。token特点:要申请,一次有效性,可以限流。注意:redis要用删除操作来判断token,删除成功代表token校验通过,如果用select+delete来校验token,存在并发问题,不建议使用;
5、分布式锁:如果是分布式系统的话,构建全局唯一索引会比较困难,比如唯一性的字段就没有办法确定。这时候可以引入分布式锁,通过第三方的系统(Redis或Zookeeper),在业务系统插入数据或者更新数据前,需要先获取分布式锁,然后才能做操作,操作完成之后就释放锁。这样其实是把单机系统里面多线程并发锁的思路引入了多个系统的场景,也就是分布式系统中的解决思路。要点:某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志(用户ID+后缀等)获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁(分布式锁要第三方系统提供)。
6、select + insert:在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机(状态变更图)。简单理解,就是业务单据上面有个状态的字段,状态在不同的情况下会发生变更,一般情况下存在有限状态机。这时候,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。注意:订单等单据类业务,存在很长的状态流转,一定要深刻理解状态机,对业务系统设计能力提高有很大帮助。

基于token+Redis的实现方案

环境:springboot2.2.11.RELEASE + Redis

  • pom.xml 依赖

<dependency>			<groupId>org.springframework.boot</groupId>			<artifactId>spring-boot-starter-data-redis</artifactId>		</dependency>		<dependency>			<groupId>org.springframework.boot</groupId>			<artifactId>spring-boot-starter-web</artifactId>		</dependency>		<dependency>			<groupId>org.apache.commons</groupId>			<artifactId>commons-pool2</artifactId>		</dependency>
  • 自定义注解类,有该注解的需要验证token是否有效

@Documented@Inherited@Retention(RUNTIME)@Target({ METHOD, TYPE})public @interface ApiIdempotent {}
  • 拦截器定义,拦截请求方法进行token有效性验证

public class MethodIdempotentCheck implements HandlerInterceptor {	private Logger logger = LoggerFactory.getLogger(MethodIdempotentCheck.class) ;		@Resource	private StringRedisTemplate stringRedisTemplate ;		@Override	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)			throws Exception {		if (handler instanceof HandlerMethod) {			HandlerMethod handlerMethod = (HandlerMethod) handler ;			Method method = handlerMethod.getMethod() ;			Class<?> clazz = method.getClass() ;			if (clazz.isAnnotationPresent(ApiIdempotent.class)) {				if (!checkToken(request)) {					failure(response) ;					return false ;				}			} else {				if (method.isAnnotationPresent(ApiIdempotent.class)) {					if (!checkToken(request)) {						failure(response) ;						return false ;					}				}			}		}		return true ;	}		private void failure(HttpServletResponse response) throws Exception {		response.setContentType("application/json;charset=utf-8") ;		response.getWriter().write("{\"code\": -1, \"message\": \"重复提交\"}") ;	}		private boolean checkToken(HttpServletRequest request) {		logger.info("验证token") ;		String token = request.getParameter("access-token") ;		if (token == null || token.length() == 0) {			token = request.getHeader("access-token") ;		}		logger.info("获取token:{}", token) ;		if (token == null || token.length() == 0) {			return false ;		}		boolean exists = stringRedisTemplate.hasKey(token) ;		if (!exists) {			return false ;		}		return stringRedisTemplate.delete(token) ;	}	}token会从header中获取请求参数中获取。
  • WebConfig 配置拦截器

@Configurationpublic class WebConfig implements WebMvcConfigurer {		@Override	public void addInterceptors(InterceptorRegistry registry) {		registry.addInterceptor(tokenInterceptor()) ;	}		@Bean	public HandlerInterceptor tokenInterceptor() {		return new MethodIdempotentCheck() ;	}	}
  • Controller 测试

@RestController@RequestMapping("/demo")public class DemoController {		@Resource	private StringRedisTemplate stringRedisTemplate ;		/**	 * 	生成Token;这里token的有效期设置了10分钟。	 * 	@return	 */	@GetMapping("/create")	public Object create() {		String access_token = UUID.randomUUID().toString() ;		stringRedisTemplate.opsForValue().set(access_token, access_token, 10, TimeUnit.MINUTES) ;		return access_token ;	}		@PostMapping("/save")	@ApiIdempotent	public Object business(@RequestBody Users user) {		Map<String, Object> result = new HashMap<>() ;		// todo save Users		result.put("code", 0) ;		result.put("message", "创建成功") ;		return result ;	}	}

business 方法加入了@ApiIdempotent注解,表示该方法需要进行token验证是否有效的请求

整个请求流程要先获取token,然后将得到的token放入header中或者请求参数中。

测试:

获取token:

请求业务方法将获取的token 添加到header中

再次请求:

每次请求token的验证是通过删除token进行的,所以当第二次再请求时,redis中已经没有了token所以这里就提示:重复提交了。

完毕!!!

来源:

https://www.toutiao.com/i6897027912868889091/

“IT大咖说”欢迎广大技术人员投稿,投稿邮箱:aliang@itdks.com



来都来了,走啥走,留个言呗~




 IT大咖说  |  关于版权 

由“IT大咖说(ID:itdakashuo)”原创的文章,转载时请注明作者、出处及微信公众号。投稿、约稿、转载请加微信:ITDKS10(备注:投稿),茉莉小姐姐会及时与您联系!

感谢您对IT大咖说的热心支持!



相关推荐


推荐文章

文章转载自IT大咖说,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论