服务器之家:专注于服务器技术及软件下载分享
分类导航

Mysql|Sql Server|Oracle|Redis|MongoDB|PostgreSQL|Sqlite|DB2|mariadb|Access|数据库技术|

服务器之家 - 数据库 - Redis - 利用Redis实现防止接口重复提交功能

利用Redis实现防止接口重复提交功能

2022-01-24 17:59秃头哥编程 Redis

大家好,本篇文章主要讲的是利用Redis实现防止接口重复提交功能,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下,方便下次浏览

前言

在划水摸鱼之际,突然听到有的用户反映增加了多条一样的数据,这用户立马就不干了,让我们要马上修复,不然就要投诉我们。

利用Redis实现防止接口重复提交功能

这下鱼也摸不了了,只能去看看发生了什么事情。据用户反映,当时网络有点卡,所以多点了几次提交,最后发现出现了十几条一样的数据。

只能说现在的人都太心急了,连这几秒的时间都等不了,惯的。心里吐槽归吐槽,这问题还是要解决的,不然老板可不惯我。

利用Redis实现防止接口重复提交功能

其实想想就知道为啥会这样,在网络延迟的时候,用户多次点击,最后这几次请求都发送到了服务器访问相关的接口,最后执行插入。

既然知道了原因,该如何解决。当时我的第一想法就是用注解 + aop。通过在自定义注解里定义一些相关的字段,比如过期时间即该时间内同一用户不能重复提交请求。然后把注解按需加在接口上,最后在拦截器里判断接口上是否有该接口,如果存在则拦截。

解决了这个问题那还需要解决另一个问题,就是怎么判断当前用户限定时间内访问了当前接口。其实这个也简单,可以使用redis来做,用户名 + 接口 + 参数啥的作为唯一键,然后这个键的过期时间设置为注解里过期字段的值。设置一个过期时间可以让键过期自动释放,不然如果线程突然歇逼,该接口就一直不能访问。

利用Redis实现防止接口重复提交功能

这样还需要注意的一个问题是,如果你先去redis获取这个键,然后判断这个键不存在则设置键;存在则说明还没到访问时间,返回提示。这个思路是没错的,但这样如果获取和设置分成两个操作,就不满足原子性了,那么在多线程下是会出错的。所以这样需要把俩操作变成一个原子操作。

分析好了,就开干。

利用Redis实现防止接口重复提交功能

1、自定义注解

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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内同一用户不允许重复访问同一接口。使用的时候也可以传入自定义的值。

我们只需要在对应的接口上添加该注解即可

?
1
2
3
@norepeatcommit
或者
@norepeatcommit(expire = 10)

2、自定义拦截器

自定义好了注解,那就该写拦截器了。

?
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
@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命令。

?
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
/**
 * 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方法如下

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 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,不然耗尽连接数就完蛋了,我不会告诉你我把服务器搞挂了。

利用Redis实现防止接口重复提交功能

4、其他想说的

其实做完这三步差不多了,基本够用。再考虑一些其他情况的话,比如在expire设置的时间内,我这个接口还没执行完逻辑咋办呢?

其实我们不用自己在这整破轮子,直接用健壮的轮子不好吗?比如redisson,来实现分布式锁,那么上面的问题就不用考虑了。有看门狗来帮你做,在键过期的时候,如果检查到键还被线程持有,那么就会重新设置键的过期时间。

到此这篇关于利用redis实现防止接口重复提交功能的文章就介绍到这了,更多相关redis防止接口重复提交内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://blog.csdn.net/a_helloword/article/details/121939452

延伸 · 阅读

精彩推荐
  • Redis聊一聊Redis与MySQL双写一致性如何保证

    聊一聊Redis与MySQL双写一致性如何保证

    一致性就是数据保持一致,在分布式系统中,可以理解为多个节点中数据的值是一致的。本文给大家分享Redis与MySQL双写一致性该如何保证,感兴趣的朋友一...

    mind_programmonkey6432021-08-12
  • Redisredis启动,停止,及端口占用处理方法

    redis启动,停止,及端口占用处理方法

    今天小编就为大家分享一篇redis启动,停止,及端口占用处理方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧 ...

    澄海单挑狂5152019-11-14
  • RedisRedis分布式锁升级版RedLock及SpringBoot实现方法

    Redis分布式锁升级版RedLock及SpringBoot实现方法

    这篇文章主要介绍了Redis分布式锁升级版RedLock及SpringBoot实现,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以...

    等不到的口琴7802021-07-25
  • Redis在ssm项目中使用redis缓存查询数据的方法

    在ssm项目中使用redis缓存查询数据的方法

    本文主要简单的使用Java代码进行redis缓存,即在查询的时候先在service层从redis缓存中获取数据。如果大家对在ssm项目中使用redis缓存查询数据的相关知识感...

    caychen8962019-11-12
  • RedisLinux Redis 的安装步骤详解

    Linux Redis 的安装步骤详解

    这篇文章主要介绍了 Linux Redis 的安装步骤详解的相关资料,希望大家通过本文能掌握如何安装Redis,需要的朋友可以参考下 ...

    carl-zhao3822019-11-08
  • Redis就这?Redis持久化策略——AOF

    就这?Redis持久化策略——AOF

    今天为大家介绍Redis的另一种持久化策略——AOF。注意:AOF文件只会记录Redis的写操作命令,因为读命令对数据的恢复没有任何意义...

    头发茂密的刘叔4052021-12-14
  • RedisRedis存取序列化与反序列化性能问题详解

    Redis存取序列化与反序列化性能问题详解

    这篇文章主要给大家介绍了关于Redis存取序列化与反序列化性能问题的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参...

    这名字已经存在9742021-02-24
  • RedisRedis数据结构之链表与字典的使用

    Redis数据结构之链表与字典的使用

    这篇文章主要介绍了Redis数据结构之链表与字典的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友...

    白泽来了4052021-08-03