redis-操作带过期时间的key需保证操作的原子性

2023-09-19 10:48:34

问题
先执行get获取值,判断符合条件再执行incr、decr操作。在临界缓存失效的情况下,会默认赋值当前key为永不过期的0,再执行加减法,导致程序异常。

推荐解决方案
1、限制接口频率:先incr,执行后值为1,说明是第一次执行,需要额外设置过期时间,再判断是否超过当前接口频率限制(注意上述步骤不可调换顺序)

2、使用lua脚本完整提交一次操作,脚本中的key可以保证一致。以加减库存为例,先查询key存在的情况下,再进行库存变更,如果不存在无需处理,等待下次缓存加载即为最新的值


问题描述

场景1:我们缓存了一个商品的库存,过期时间为5分钟,根据用户的购买和取消执行 incr、decr 操作。代码通常会这样来编写:

		// 库存存在则加一
        if(redisService.get(prefix, key, Integer.class) != null){
            redisService.incr(prefix, key);
        }

场景2:为了接口限流,我们可以通过redis简单实现:

        // 首先获取当前访问频次
		Integer count = redisService.get(prefix, key, Integer.class);
        // 如果频次为空,则设置访问次数为1
		if (count == null) {
            redisService.set(prefix, key, 1);
        } else if (count < checkFrequencyCount) {
            // 如果频次小于限制,则设置访问次数加1
            redisService.incr(prefix, key);
        } else {
			// 如果频次超过限制,则限流
            throw new AppException("访问频次过高,请稍候再试");
        }

两种场景编码看似都没有问题,但实际运行中却发现redis中有一些key变成了永不过期的key,而且值不正确。

原因是: 因为redis的incr操作,当key不存在时, 会生成这个key并将值初始化为0, 并且默认设置key的有效时间为永久。

解决方案

方案1.优化Java代码,例如场景2,然后我们根据过期时间判断和根据value值判断可分为两种优化策略。

1.不论这个key是否存在都先加一,然后判断其过期时间是否为永不过期,如果是永不过期则说明是新生成的key,给它设置过期时间即可,如果非永不过期则无需操作。最后再判断一下是否值已经大于访问频次了,是则限流。

		long count = redisService.incr(prefix, key);
        long expire = redisService.ttl(prefix, key);
        if (expire == -1) {
            redisService.setExpire(prefix, key, accessExpireSecond);
        }
        // 判断必须放在后面,否则key没有过期时间永远无法清除
        if (count > checkFrequencyCount) {
            throw new AppException("访问频次过高,请稍候再试");
        }

2.不论这个key是否存在都先加一,然后判断其值是否为1,如果是1则说明是新生成的key,给它设置过期时间即可,如果非1则无需操作。最后再判断一下是否值已经大于访问频次了,是则限流。

		long count = redisService.incr(prefix, key);
        long expire = redisService.ttl(prefix, key);
        if (count == 1L) {
            redisService.setExpire(prefix, key, accessExpireSecond);
        }
        // 判断必须放在后面,否则key没有过期时间永远无法清除
        if (count > checkFrequencyCount) {
            throw new AppException("访问频次过高,请稍候再试");
        }

方案2.使用lua脚本执行,一次提交保证原子性。例如场景1,key存在则执行加减操作。

脚本updateStore.lua

--- 获取key
local key = KEYS[1]
--- 获取参数:incr、decr
local action = ARGV[1]
--- 如果key存在,再执行增加或减少的操作
if redis.call('exists', key) == 1 then
    redis.call(action, key)
    return 1
end

配置类LuaConfiguration.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;

@Configuration
public class LuaConfiguration {
    @Bean(name = "update")
    public DefaultRedisScript<Boolean> redisScript() {
        DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("luascript/updateStore.lua")));
        redisScript.setResultType(Boolean.class);
        return redisScript;
    }
}

使用方法:

    @Resource(name = "update")
    private DefaultRedisScript<Boolean> redisScript;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
	// 执行脚本并传参
	Boolean result = stringRedisTemplate.execute(redisScript, Arrays.asList(stockPrefix.getPrefix() + key), "incr");
更多推荐

提高接口自动化测试效率:使用 JMESPath 实现断言和数据提取!

前言做接口自动化,断言是比不可少的。如何快速巧妙的提取断言数据就成了关键,当然也可以提高用例的编写效率。笔者在工作中接触到了JMESPath,那到底该如何使用呢?带着疑惑一起往下看。JMESPath是啥?JMESPath是一种用于查询和转换JSON数据的简洁、强大的查询语言。它提供了一种灵活的方式来从复杂的JSON结构

思腾云计算

近年来,游戏行业发展迅猛,市场容量不断扩大。从游戏产业发展来看,玩家对于游戏内容和体验的需求不断攀升。那如何在同质化的游戏市场,通过AI来提高游戏探索和交互的趣味度?行业存在以下痛点:1、游戏迭代速度加快,如何加速研发创新,提供多元化的游戏体验;2、在线多人竞技类游戏因玩家能力不均,均衡对局匹配耗时长,对局质量差,再加

Spring注解家族介绍: @RequestMapping

前言:今天我们来介绍@RequestMapping这个注解,这个注解的内容相对来讲比较少,篇幅会比较短。目录前言:@RequestMapping应用场景:总结:@RequestMapping@RequestMapping是一个用于映射HTTP请求到处理方法的注解,它可以用在控制器类和处理方法上。当请求到达服务器时,根据

lv4 嵌入式开发-9 静态库与动态库的使用

目录1库的概念2库的知识3静态库特点4静态库4.1静态库创建4.2编译生成目标文件4.3创建静态库hello4.4查看库中符号信息4.5链接静态库5共享库特点6共享库6.1共享库创建6.2编译生成目标文件6.3创建共享库common6.4为共享库文件创建链接文件6.5编写应用程序6.6编译test.c并链接共享库lib

Java基于SpringBoot的漫画网站,附源码,教程

博主介绍:✌程序员徐师兄、7年大厂程序员经历。全网粉丝30W+、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌今天为大家带来的是基于SpringBoot+Vue的漫画之家系统,亲测可用,可以作为课程设计,毕业设计适用。文章目录1、前言介绍2.技术栈3系统分析3.

爬虫获取接口数据

上一讲讲的是获取静态网页数据的教程,适用于我们要爬取的数据在网页源代码中出现,但是还是有很多的数据是源代码中没有的,需要通过接口访问服务器来获得,下面我就来讲讲如何爬取这类数据。以巨潮资讯网爬取比亚迪企业年报为例。正常人的操作打开巨潮资讯网官网找到比亚迪的公告在分类里面选择筛选信息,找到自己想要的信息爬虫的思路获取请求

Vue 使用vue-pdf 显示pdf文件 切换页面 缩放 全屏 自动播放等

<template><divid="container"><!--上一页、下一页--><divclass="right-btn"><div@click="toFullOrExit"class="turn-btn"><span>{{isFull==1?"取消全屏":"全屏"}}</span></div><div@clic

ubuntu 22.04运行opencv4的c++程序遇到的问题

摘要:本文介绍一下在ubuntu系统中,运行一个最简单的opencv4程序都出问题的解决方法,并对其基本原理作简单阐述。解决问题的方法有很多,本文只提供其中一种。opencv版本是4.2.0,ubuntu版本是20.04查询opencv版本的指令是pkg-config--modversionopencv4,pkg-co

CRM客户管理系统主要用途

对于大多数企业而言业绩就是生命线,因此销售环节在企业管理过程中意义重大。面对愈发内卷的市场竞争企业就要借助CRM销售管理系统改善各个环节存在的漏洞,占据优势。那么,销售管理系统的用途有哪些,接下来我们从下面3个功能来介绍。1.客户管理通过销售管理系统中的商机管理等功能可以将系统中的客户信息关联整合,一方面保证客户数据安

性能测试知多少?怎样开展性能测试

看到好多新手,在性能需求模糊的情况下,随便找一个性能测试工具,然后就开始进行性能测试了,在这种情况下得到的性能测试结果很难体现系统真实的能力,或者可能与系统真实的性能相距甚远。与功能测试相比,性能测试在技术层面具有更大的复杂性。在以往的测试流程中,性能测试只是测试流程的一部分,是系统或验收测试的一个可选项。但随着测试技

计算机毕业设计 基于SSM+Vue的志愿者招募网站的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍:✌从事软件开发10年之余,专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌🍅文末获取源码联系🍅👇🏻精彩专栏推荐订阅👇🏻不然下次找不到哟————————————————计算机毕业设计题目《10

热文推荐