前言
在划水摸鱼之际,突然听到有的用户反映增加了多条一样的数据,这用户立马就不干了,让我们要马上修复,不然就要投诉我们。
这下鱼也摸不了了,只能去看看发生了什么事情。据用户反映,当时网络有点卡,所以多点了几次提交,最后发现出现了十几条一样的数据。
只能说现在的人都太心急了,连这几秒的时间都等不了,惯的。心里吐槽归吐槽,这问题还是要解决的,不然老板可不惯我。
其实想想就知道为啥会这样,在网络延迟的时候,用户多次点击,最后这几次请求都发送到了服务器访问相关的接口,最后执行插入。
既然知道了原因,该如何解决。当时我的第一想法就是用注解 + aop。通过在自定义注解里定义一些相关的字段,比如过期时间即该时间内同一用户不能重复提交请求。然后把注解按需加在接口上,最后在拦截器里判断接口上是否有该接口,如果存在则拦截。
解决了这个问题那还需要解决另一个问题,就是怎么判断当前用户限定时间内访问了当前接口。其实这个也简单,可以使用redis来做,用户名 + 接口 + 参数啥的作为唯一键,然后这个键的过期时间设置为注解里过期字段的值。设置一个过期时间可以让键过期自动释放,不然如果线程突然歇逼,该接口就一直不能访问。
这样还需要注意的一个问题是,如果你先去redis获取这个键,然后判断这个键不存在则设置键;存在则说明还没到访问时间,返回提示。这个思路是没错的,但这样如果获取和设置分成两个操作,就不满足原子性了,那么在多线程下是会出错的。所以这样需要把俩操作变成一个原子操作。
分析好了,就开干。
1、自定义注解
import java.lang.annotation.elementtype; import java.lang.annotation.retention; import java.lang.annotation.retentionpolicy; import java.lang.annotation.target; /** * 防止同时提交注解 */ @target({elementtype.parameter, elementtype.method}) @retention(retentionpolicy.runtime) public @interface norepeatcommit { // key的过期时间3s int expire() default 3; }
这里为了简单一点,只定义了一个字段expire
,默认值为3,即3s内同一用户不允许重复访问同一接口。使用的时候也可以传入自定义的值。
我们只需要在对应的接口上添加该注解即可
@norepeatcommit 或者 @norepeatcommit(expire = 10)
2、自定义拦截器
自定义好了注解,那就该写拦截器了。
@aspect public class norepeatsubmitaspect { private static logger _log = loggerfactory.getlogger(norepeatsubmitaspect.class); redislock redislock = new redislock(); @pointcut("@annotation(com.zheng.common.annotation.norepeatcommit)") public void point() {} @around("point()") public object doaround(proceedingjoinpoint pjp) throws throwable { // 获取request requestattributes requestattributes = requestcontextholder.getrequestattributes(); servletrequestattributes servletrequestattributes = (servletrequestattributes) requestattributes; httpservletrequest request = servletrequestattributes.getrequest(); httpservletresponse responese = servletrequestattributes.getresponse(); object result = null; string account = (string) request.getsession().getattribute(upmsconstant.account); user user = (user) request.getsession().getattribute(upmsconstant.user); if (stringutils.isempty(account)) { return pjp.proceed(); } methodsignature signature = (methodsignature) pjp.getsignature(); method method = signature.getmethod(); norepeatcommit form = method.getannotation(norepeatcommit.class); string sessionid = request.getsession().getid() + "|" + user.getusername(); string url = objectutils.tostring(request.getrequesturl()); string pg = request.getmethod(); string key = account + "_" + sessionid + "_" + url + "_" + pg; int expire = form.expire(); if (expire < 0) { expire = 3; } // 获取锁 boolean issuccess = redislock.trylock(key, key + sessionid, expire); // 获取成功 if (issuccess) { // 执行请求 result = pjp.proceed(); int status = responese.getstatus(); _log.debug("status = {}" + status); // 释放锁,3s后让锁自动释放,也可以手动释放 // redislock.releaselock(key, key + sessionid); return result; } else { // 失败,认为是重复提交的请求 return new upmsresult(upmsresultconstant.repeat_commit, validationerror.create(upmsresultconstant.repeat_commit.message)); } } }
拦截器定义的切点是norepeatcommit
注解,所以被norepeatcommit
注解标注的接口就会进入该拦截器。这里我使用了account + "_" + sessionid + "_" + url + "_" + pg
作为唯一键,表示某个用户访问某个接口。
这样比较关键的一行是boolean issuccess = redislock.trylock(key, key + sessionid, expire);
。可以看看redislock
这个类。
3、redis工具类
上面讨论过了,获取锁和设置锁需要做成原子操作,不然并发环境下会出问题。这里可以使用redis的setnx
命令。
/** * redis分布式锁实现 * lua表达式为了保持数据的原子性 */ public class redislock { /** * redis 锁成功标识常量 */ private static final long release_success = 1l; private static final string set_if_not_exist = "nx"; private static final string set_with_expire_time = "ex"; private static final string lock_success= "ok"; /** * 加锁 lua 表达式。 */ private static final string release_try_lock_lua = "if redis.call('setnx',keys[1],argv[1]) == 1 then return redis.call('expire',keys[1],argv[2]) else return 0 end"; /** * 解锁 lua 表达式. */ private static final string release_release_lock_lua = "if redis.call('get', keys[1]) == argv[1] then return redis.call('del', keys[1]) else return 0 end"; /** * 加锁 * 支持重复,线程安全 * 既然持有锁的线程崩溃,也不会发生死锁,因为锁到期会自动释放 * @param lockkey 加锁键 * @param userid 加锁客户端唯一标识(采用用户id, 需要把用户 id 转换为 string 类型) * @param expiretime 锁过期时间 * @return ok 如果key被设置了 */ public boolean trylock(string lockkey, string userid, long expiretime) { jedis jedis = jedisutils.getinstance().getjedis(); try { jedis.select(jedisutils.index); string result = jedis.set(lockkey, userid, set_if_not_exist, set_with_expire_time, expiretime); if (lock_success.equals(result)) { return true; } } catch (exception e) { e.printstacktrace(); } finally { if (jedis != null) jedis.close(); } return false; } /** * 解锁 * 与 trylock 相对应,用作释放锁 * 解锁必须与加锁是同一人,其他人拿到锁也不可以解锁 * * @param lockkey 加锁键 * @param userid 解锁客户端唯一标识(采用用户id, 需要把用户 id 转换为 string 类型) * @return */ public boolean releaselock(string lockkey, string userid) { jedis jedis = jedisutils.getinstance().getjedis(); try { jedis.select(jedisutils.index); object result = jedis.eval(release_release_lock_lua, collections.singletonlist(lockkey), collections.singletonlist(userid)); if (release_success.equals(result)) { return true; } } catch (exception e) { e.printstacktrace(); } finally { if (jedis != null) jedis.close(); } return false; } }
在加锁的时候,我使用了string result = jedis.set(lockkey, userid, set_if_not_exist, set_with_expire_time, expiretime);
。set方法如下
/* set the string value as value of the key. the string can't be longer than 1073741824 bytes (1 gb). params: key – value – nxxx – nx|xx, nx -- only set the key if it does not already exist. xx -- only set the key if it already exist. expx – ex|px, expire time units: ex = seconds; px = milliseconds time – expire time in the units of expx returns: status code reply */ public string set(final string key, final string value, final string nxxx, final string expx, final long time) { checkisinmultiorpipeline(); client.set(key, value, nxxx, expx, time); return client.getstatuscodereply(); }
在key不存在的情况下,才会设置key,设置成功则返回ok。这样就做到了查询和设置原子性。
需要注意这里在使用完jedis,需要进行close,不然耗尽连接数就完蛋了,我不会告诉你我把服务器搞挂了。
4、其他想说的
其实做完这三步差不多了,基本够用。再考虑一些其他情况的话,比如在expire设置的时间内,我这个接口还没执行完逻辑咋办呢?
其实我们不用自己在这整破轮子,直接用健壮的轮子不好吗?比如redisson
,来实现分布式锁,那么上面的问题就不用考虑了。有看门狗来帮你做,在键过期的时候,如果检查到键还被线程持有,那么就会重新设置键的过期时间。
到此这篇关于利用redis实现防止接口重复提交功能的文章就介绍到这了,更多相关redis防止接口重复提交内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!