Redis如何实现原子性自增自减

一、背景

假设有一个需求,包含简单的两个步骤:

  1. 第一步是用户获取验证码,检验验证码成功后跳转到表单填写页面;
  2. 第二步是用户填写表单并提交申请

为了防止用户跳过第一步直接提交申请,我们采取了以下策略:

  1. 在验证码验证成功后,我们将用户Id作为Key,剩余可申请次数作为Value,将这个键值对存储在Redis中并设置过期时间;
  2. 在收到用户提交的申请请求后,首先检查Redis中该用户Id对应的Value是否存在且大于0。我们将申请次数减一,并允许用户提交申请;如果不满足条件,则直接抛出异常。

二、increment

针对以上策略的第一步,直接上伪代码

1.检验验证码方法如下:

java 复制代码
//以下为伪代码,重点关注redis的调用逻辑

//校验验证码方法
public void checkCode(String code){

    //校验验证码逻辑
    ...

    
    //校验通过
    redisBuryingPoint(userId);

}

以上代码,略过了校验验证码的逻辑,在验证码校验成功后,调用了'redisBuryingPoint(userId)'方法进行埋点,其中userId为用户Id。

2.redisBuryingPoint方法如下:

java 复制代码
/**
 * redis埋点
 * @param userId 用户id
*/
public void redisBuryingPoint(String userId) {

    int value = redisService.increment(userId, 1 * 60 * 60L);
    logger.info("用户[{}]验证成功共计[{}]次", userId, value);
}

3.redisService#increment(String key, Long expirationTimeInSeconds) 方法如下**:**

java 复制代码
/**
 * 自增  
 * @param key 要加一的键
 * @param expirationTimeInSeconds 过期时间
*/
public int increment(String key, Long expirationTimeInSeconds) {
        // +1 操作
        int result = redisTemplate.opsForValue().increment(key).intValue();
        // 设置过期时间
        redisTemplate.expire(KEY, expirationTimeInSeconds, TimeUnit.SECONDS);
        //返回
        return result;
}

4.小结

以上就完成了redis埋点的过程。即使同一个用户在同一时间进行多个验证码的验证操作,即在并发场景 下多个请求同时调用'increment() '方法时,RedisTemplate 会自动处理并发操作,确保操作的原子性和一致性。

三、decrement

针对以上策略的第二步,伪代码如下

1.申请提交预校验方法如下:

java 复制代码
/**
 * 预校验:校验短信验证成功的次数
 * @param userId 用户Id
*/
private void preCheck(String userId) {

    Long result = redisService.decrement(userId);

    if (result != null) {
        // 成功递减
        logger.info("发起申请成功,用户[{}]验证成功次数剩余[{}]次", userId, result);
    } else {
        // 值不存在,无需递减操作
        logger.error("发起申请失败,用户[{}]验证成功次数不足", userId);
        throw new RuntimeException("发起申请失败,请先进行短信验证!")
    }
}

在上述代码中,我们直接调用redisService的 decrement() 方法,我们检查返回的结果是否为 null,如果不为 null,表示递减操作成功,并打印递减后的剩余次数。如果结果为 null,表示值不存在,没有进行递减操作,打印错误日志并抛出异常。

2.redisService#decrement(String key) 方法如下**:**

java 复制代码
private RedisScript<Long> decrementScript;

public RedisService() {
        //定义 Lua 脚本
        this.decrementScript = new DefaultRedisScript<>(
                "if redis.call('exists', KEYS[1]) == 1 and tonumber(redis.call('get', KEYS[1])) > 0 then " +
                "   return redis.call('decr', KEYS[1]) " +
                "else " +
                "   return nil " +
                "end",
                Long.class);
    }


/**
 * 自减 
 * @param key 要减一的键
 * 
*/
public Long decrement(String key) {
        return redisTemplate.execute(decrementScript, Collections.singletonList(key));    
}

在上述代码中,我们使用 Lua 脚本 来执行递减操作。脚本首先检查键是否存在,如果存在则执行递减操作,如果不存在则返回空。在 Java 代码中,我们使用 DefaultRedisScript 来定义 Lua 脚本,并通过 **redisTemplate.execute()**方法来执行脚本。

3.小结

以上就完成了redis消费的过程。即使同一个用户在同一时间提交多次申请,即在并发场景 下多个请求同时调用'decrement()'方法时,Redis 执行 Lua 脚本,会将整个脚本作为一个原子操作进行执行。确保操作的原子性和一致性。

四、其他

当 Redis 的键不存在时,使用 opsForValue().increment() 方法会将键的值初始化为 1,并执行递增操作;使用 opsForValue().decrement() 方法会将键的值初始化为 -1,并执行递减操作。

所以,如果在调用 increment() 方法时,键对应的值不存在,它将被赋值为 1;如果在调用 decrement() 方法时,键对应的值不存在,它将被赋值为 -1。

相关推荐
启山智软3 分钟前
【启山智软智能商城系统技术架构剖析】
java·前端·架构
一线大码4 分钟前
Java 使用国密算法实现数据加密传输
java·spring boot·后端
我命由我123459 分钟前
Android Gradle - Gradle 自定义插件(Build Script 自定义插件、buildSrc 自定义插件、独立项目自定义插件)
android·java·java-ee·kotlin·android studio·android-studio·android runtime
Riu_Peter12 分钟前
【技术】Maven 配置 settings.xml 轮询下载
xml·java·maven
Rust语言中文社区18 分钟前
【Rust日报】用 Rust 重写的 Turso 是一个更好的 SQLite 吗?
开发语言·数据库·后端·rust·sqlite
Dontla19 分钟前
VScode插件SQLite Viewer介绍(允许开发者不离开编辑器,直接打开、浏览和查询SQLite数据库文件)(ChromaDB、向量库插件、数据库插件、.sqlite3)DBeaver
数据库·vscode
星辰徐哥21 分钟前
易语言数据库操作初步:内置Ado引擎与SQLite3快速上手
数据库·oracle·sqlite·易语言
守候秋林辉22 分钟前
JFinal+SQLite 解决Date类型与DATETIME类型转换异常
jvm·数据库·sqlite
qq_4160187226 分钟前
用Python批量处理Excel和CSV文件
jvm·数据库·python
十六年开源服务商44 分钟前
2026年WordPress网站地图完整指南
java·前端·javascript