浅谈Redis防重机制

961 阅读6分钟

“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第n篇文章,点击查看活动详情

使用防重复机制的背景

1.由于用户误操作,多次点击表单提交按钮。

2.由于网速等原因造成页面卡顿,用户重复刷新提交页面。

3.黑客或恶意用户使用postman等工具重复恶意提交表单(攻击网站)。

这些情况都会导致表单重复提交,造成数据重复,增加服务器负载,严重甚至会造成服务器宕机。因此有效防止表单重复提交有一定的必要性。

解决方案

针对目前比较流行的分布式系统,结合在项目实施过程中遇到的问题,以及在解决问题中总结的经验,我大致总结了几种方案,以及各自的优缺点,仅供大家参考。

1.通过前端控制:

具体做法:用户点击提交按钮之后,就把提交按钮置灰不可用,直到交易收到发馈之后,再把提交按钮设置成可用状态。

该方案优点:修改前端按钮,不修改具体业务逻辑,修改简便,易于理解。

缺点:该方案用户可以通过刷新页面方式、使用postman等工具可以绕过前端页面仍然可以重复提交。该种方案不推荐

2.数据库添加唯一索引:

具体做法:在数据库建表的时候在id字段添加唯一索引,例如:用户名称、联系方式、电子邮箱等字段添加唯一约束,通过该种方式保证数据库数据唯一和有效性。后端代码针对插入数据操作的方法,加入捕获异常的方式,避免数据库重复插入相同的数据。

该方案的优点:简单、易于理解。

缺点:如果在项目后期,修改数据库表的结果,可能影响比较大;该方案无法阻止恶意的网站攻击,服务器会出现大量的sql语句插入,增加服务器和数据库的负荷。

3.使用session防重复提交:

具体做法:在服务端生成一个唯一的随机编号,可以称之为Token,然后在当前用户的session域中保存这个Token。然后将这个Token发送到客户端的Form表单中,表单提交时候连同这个Token一起提交到服务端,服务端判断上送过来的Token是否和服务器生成的一致,如果不一致就是重复提交的数据,服务端就不再处理提交的数据。如果相同则处理,处理完成之后清理掉Session域中存储的随机编号。

我们项目实施初期一开始使用的是该种方案,在实施过程中发现存在缺点:

每次提交动作前都先发送一次请求,获取唯一编号,前端提交类交易比较多,方案改动量比较大,可用性差。而且后端改造会侵入具体的业务逻辑,也不推荐该种方案。

针对以上几种方案存在的问题,分布式系统推出了使用Redis,针对项目用遇到的问题,我和大家分享下自己对redis防重的一些理解和经验。

Redis技术解决防重复提交

关于重复请求,指的是我们服务端接收到很短的时间内的多个相同内容的重复请求。而这样的重复请求如果是幂等的(每次请求的结果都相同,如查询请求),那其实对于我们没有什么影响,但如果是非幂等的(每次请求都会对关键数据造成影响,如删除关系、建立关系等),那就会轻则产生脏数据,重则导致系统错误。

因此,在当前普遍分布式服务的情况下,如何避免和解决重复请求给我们带来的数据异常成为了亟待解决的问题。而避免重复请求,最好的做法是前后端共同去做。

  1. 前端或客户端在非幂等的按钮上直接做禁止提交重复请求的操作。

  2. 后端在接收到请求时加锁,完成后解锁,使用redis锁来实现

为何要使用分布式锁来解决呢?因为我们当前普遍的架构都是分布式的服务端,前端请求通过网关层转发至后端。

如下图所示,因此如果只在一个单独的服务器上做限制,就无法在分布式的服务中完成应对高频次的重复请求了。

基本思路:

  • 接收到请求后,根据方法名+参数取md5值,获取该方法及该参数的唯一标识;
  • 获取标识后设置分布式锁,并且设置过期时间;
  • 在请求结束后,释放分布式锁。

思考:后端防重按照这种思路,在提交方法中加入该类代码,这样的话就会出现重复的代码,不符合设计原则,后续维护也比较难。结合项目的实际情况,设计出通过的解决方案,使用注解+切面+md5key+反射+redis实现。自定义注解,把上述的思路封装成一个自定义注解的方式,封装成jar包,微服务的产品引用该jar包,在需要使用的方法上面加上注解,就可以实现防重复提交逻辑。

代码的具体实现如下:

  1. 自定义一个分布式锁注解,注解包含过期时间设置、忽略参数;
  2. 定义一个切面,切点为分布式锁注解,在切面中获取需要使用分布式锁的方法名、参数、过期时间,并且将方法名及未被忽略参数做md5取唯一标识;
  3. 再根据上述唯一标识设置redsis分布式锁;
  4. 方法结束后解锁。

Redis主从复制模式需要注意下面的问题:

该种模式情况下可能会遇到下面的问题:

  1. 在Redis的master节点上拿到了锁;
  2. 但是这个加锁的key还没有同步到slave节点;
  3. master故障,发生故障转移,slave节点升级为master节点;
  4. 导致锁丢失。

解决方案:

加锁之前不再先查询,直接执行加锁的方法,加锁失败后则证明该交易还在执行。