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

在最近的一次业务升级中,遇到这样一个问题,咱们设计了新的帐户体系,须要在用户将应用升级以后将原来帐户的数据手动的同步过来,就是须要用户本身去触发同步按钮进行同步,由于有些数据是用户存在本身本地的。那么在这个过程当中就存在一个问题,要是由于网络的问题,用户重复点击了这个按钮怎么办?就算咱们在客户端作了一些处理,在同步的过程当中,不能再次点击,可是通过我最近的爬虫实践,要是别人抓到了咱们的接口那么仍是不安全的。redis

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




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


华丽的分割线网络


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

那么咱们想要用Redis作用户惟一的锁对象,那么它在Redis中应该是惟一的,并且还不该该被覆盖,这个方法就是存储成功以后会返回true,若是该元素已经存在于Redis实例中,那么直接返回false测试

setIfAbsent(key,value)复制代码

可是这中间又存在一个问题,若是在获取了锁对象以后,咱们的服务挂了,那么这个时候其余请求确定是拿不到锁的,基于这种状况的考虑咱们还应该给这个元素添加一个过时时间,防止咱们的服务挂掉以后,出现死锁的问题。ui

/**
 * 添加元素
 *
 * @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就是用户若是获取不到锁,能够等待多久,过了这个时间就再也不等待,最后一个参数就是该锁的多久后过时,防止服务挂了以后,发生死锁。
spa

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


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

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

@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);
}复制代码
相关文章
相关标签/搜索