文档章节

Redis如何保证接口的幂等性?

一个程序员的成长
 一个程序员的成长
发布于 07/15 23:00
字数 1358
阅读 85
收藏 4

在最近的一次业务升级中,遇到这样一个问题,我们设计了新的账户体系,需要在用户将应用升级之后将原来账户的数据手动的同步过来,就是需要用户自己去触发同步按钮进行同步,因为有些数据是用户存在自己本地的。那么在这个过程中就存在一个问题,要是因为网络的问题,用户重复点击了这个按钮怎么办?就算我们在客户端做了一些处理,在同步的过程中,不能再次点击,但是经过我最近的爬虫实践,要是别人抓到了我们的接口那么还是不安全的。

基于这样的业务场景,我就使用Redis加锁的方式,限制了用户在请求的时候,不能发起二次请求。

我们在进入请求之后首选尝试获取锁对象,那么这个锁对象的键其实就是用户的id,如果获取成功,我们判断用户时候已经同步数据,如果已同步,那么可以直接返回,提示用户已经同步,如果没有那么直接执行同步数据的业务逻辑,最后将锁释放,如果在进入方法之后获取锁失败,那么有可能就是在第一次请求还没有结束的时候,接着又发起了请求,那么这个时候是获取不到锁的,也就不会发生数据同步出现同步好几次的情况。

华丽的分割线

那么有了这个需求之后,我们就来用Redis实现以下这个代码。首先我们要知道我们要介绍一下Redis的一个方法。

那么我们想要用Redis做用户唯一的锁对象,那么它在Redis中应该是唯一的,而且还不应该被覆盖,这个方法就是存储成功之后会返回true,如果该元素已经存在于Redis实例中,那么直接返回false

setIfAbsent(key,value) 但是这中间又存在一个问题,如果在获取了锁对象之后,我们的服务挂了,那么这个时候其他请求肯定是拿不到锁的,基于这种情况的考虑我们还应该给这个元素添加一个过期时间,防止我们的服务挂掉之后,出现死锁的问题。

/**
 * 添加元素
 *
 * @param key
 * @param value
 */
public void set(Object key, Object value) {

    if (key == null || value == null) {
        return;
    }
    redisTemplate.opsForValue().set(key, value.toString());
}

/**
 * 如果已经存在返回false,否则返回true
 *
 * @param key
 * @param value
 * @return
 */
public Boolean setNx(Object key, Object value, Long expireTime, TimeUnit mimeUnit) {

    if (key == null || value == null) {
        return false;
    }
    return redisTemplate.opsForValue().setIfAbsent(key, value, expireTime, mimeUnit);
}

/**
 * 获取数据
 *
 * @param key
 * @return
 */
public Object get(Object key) {

    if (key == null) {
        return null;
    }
    return redisTemplate.opsForValue().get(key);
}

/**
 * 删除
 *
 * @param key
 * @return
 */
public Boolean remove(Object key) {

    if (key == null) {
        return false;
    }

    return redisTemplate.delete(key);
}

/**
 * 加锁
 *
 * @param key 
 * @param waitTime 等待时间
 * @param expireTime 过期时间
 */
public Boolean lock(String key, Long waitTime, Long expireTime) {

    String value = UUID.randomUUID().toString().replaceAll("-", "").toLowerCase();

    Boolean flag = setNx(key, value, expireTime, TimeUnit.MILLISECONDS);

    // 尝试获取锁 成功返回
    if (flag) {
        return flag;
    } else {
        // 获取失败

        // 现在时间
        long newTime = System.currentTimeMillis();

        // 等待过期时间
        long loseTime = newTime + waitTime;

        // 不断尝试获取锁成功返回
        while (System.currentTimeMillis() < loseTime) {

            Boolean testFlag = setNx(key, value, expireTime, TimeUnit.MILLISECONDS);
            if (testFlag) {
                return testFlag;
            }

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    return false;
}

/**
 * 释放锁
 *
 * @param key
 * @return
 */
public Boolean unLock(Object key) {
    return remove(key);
}

我们整个加锁的代码逻辑已经写完了,我们来分析一下,用户在进来之后,首先调用lock尝试获取锁,并进行加锁,lock()方法有三个参数分别是:key,waitTime就是用户如果获取不到锁,可以等待多久,过了这个时间就不再等待,最后一个参数就是该锁的多久后过期,防止服务挂了之后,发生死锁。

当进入lock()之后,先进行加锁操作,如果加锁成功,那么返回true,再执行我们后面的业务逻辑,如果获取锁失败,会获取当前时间再加上设置的过期时间,跟当前时间比较,如果还在等待时间内,那么就再次尝试获取锁,直到过了等待时间。

注意:在设置值的时候,我们为了防止死锁设置了一个过期时间,大家一定要注意,不要等设置成功之后再去给元素设置过期时间,因为这个过程不是一个原子操作,等你刚设置成功之后,还没等设置过期时间成功,服务直接挂了,那么这个时候就会发生死锁问题,所以大家要保证存储元素和设置过期时间一定要是原子操作。

最后我们来写个测试类测试一下

@Test
public void test01() {

    String key = "uid:12011";

    Boolean flag = redisUtil.lock(key, 10L, 1000L * 60);

    if (!flag) {

        // 获取锁失败
        System.err.println("获取锁失败");
    } else {

        // 获取锁成功
        System.out.println("获取锁成功");
    }

    // 释放锁
    redisUtil.unLock(key);
}

 

© 著作权归作者所有

一个程序员的成长
粉丝 32
博文 14
码字总数 18367
作品 0
西安
后端工程师
私信 提问
分布式后端接口幂等性设计思路

在微服务架构下,我们在完成一个订单流程时经常遇到下面的场景: 以上问题,就是在单体架构转成微服务架构之后,带来的问题。当然不是说单体架构下没有这些问题,在单体架构下同样要避免重复...

rickiyeat
2017/10/30
0
0
如何使用 redis 实现分布式幂等服务中间件

背景 在编程领域,幂等性是指对同一个系统,使用同样的条件,一次请求和重复的多次请求对系统资源的影响是一致的。 在分布式系统里,服务通常通过 RPC 或 HTTP 或其他形式对外提供。不管怎样...

小刀爱编程
2018/11/06
608
3
高并发的核心技术-幂等的实现方案

一、背景 我们实际系统中有很多操作,是不管做多少次,都应该产生一样的效果或返回一样的结果。 例如: 前端重复提交选中的数据,应该后台只产生对应这个数据的一个反应结果。 我们发起一笔付...

vshcxl
2018/07/07
391
0
教你用 redis 实现分布式幂等服务中间件

背景 在编程领域,幂等性是指对同一个系统,使用同样的条件,一次请求和重复的多次请求对系统资源的影响是一致的。 在分布式系统里,client 调用 server 提供的服务,由于网络环境的复杂性,...

小刀爱编程
2018/11/24
40
0
消息队列之kafka的重复消费

Kafka 是对分区进行读写的,对于每一个分区的消费,都有一个 offset 代表消息的写入分区时的位置,consumer 消费了数据之后,每隔一段时间,会把自己消费过的消息的 offset 提交一下。表示已...

语落心生
07/11
0
0

没有更多内容

加载失败,请刷新页面

加载更多

python数据结构

1、字符串及其方法(案例来自Python-100-Days) def main(): str1 = 'hello, world!' # 通过len函数计算字符串的长度 print(len(str1)) # 13 # 获得字符串首字母大写的...

huijue
6分钟前
0
0
OSChina 周日乱弹 —— 我,小小编辑,食人族酋长

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @宇辰OSC :分享娃娃的单曲《飘洋过海来看你》: #今日歌曲推荐# 《飘洋过海来看你》- 娃娃 手机党少年们想听歌,请使劲儿戳(这里) @宇辰OSC...

小小编辑
今天
735
10
MongoDB系列-- SpringBoot 中对 MongoDB 的 基本操作

SpringBoot 中对 MongoDB 的 基本操作 Database 库的创建 首先 在MongoDB 操作客户端 Robo 3T 中 创建数据库: 增加用户User: 创建 Collections 集合(类似mysql 中的 表): 后面我们大部分都...

TcWong
今天
40
0
spring cloud

一、从面试题入手 1.1、什么事微服务 1.2、微服务之间如何独立通讯的 1.3、springCloud和Dubbo有哪些区别 1.通信机制:DUbbo基于RPC远程过程调用;微服务cloud基于http restFUL API 1.4、spr...

榴莲黑芝麻糊
今天
26
0
Executor线程池原理与源码解读

线程池为线程生命周期的开销和资源不足问题提供了解决方 案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。 线程实现方式 Thread、Runnable、Callable //实现Runnable接口的...

小强的进阶之路
昨天
79
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部