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

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

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


640?wx_fmt=png


我们在进入请求之后首选尝试获取锁对象,那么这个锁对象的键其实就是用户的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);	
}

有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号


640?wx_fmt=png


好文章,我在看❤️

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值