记一次并发下redis缓存失效问题

142 阅读4分钟

简单记录一下工作当中遇到的问题及解决方案。

较为弱智,仅做自行记录,菜鸡一个

事件一:Redis原子化操作

前两天运营联系我说有个库存没有正常扣除的问题。我先是按照惯例排查一圈日志,发现是发货按钮在同一时间被触发两次,时间间隔相差几十毫秒:

第一次请求时间:2025-02-04 18:42:31,300
第二次请求时间:2025-02-04 18:42:31,318

看了眼代码发现在入口处有用到 Redis 做重复访问的限制,部分代码如下:

public void orderSend(Long deliveryId){
    String key = "OrderSendLock_"+ deliveryId;
    String value = cacheService.get(key);
    if(ObjectUtils.isEmpty(value)){
        throw new Exeception("订单已发货,请勿重复点击!")
    }
    cacheService.set(key, value, 30);
    // 以下省略正常业务代码....
}

再次审视代码,发现自己是先去获取缓存是否存在,不存在再 set,那高并发下,会出现两次 get 都获取不到结果也正常。

简单查阅下,发现自己这种操作并不是原子化的,get 和 set 操作分离。解决方案简单,在 redis 当中存在一种 setnx 方法,是一个原子化操作,可以返回一个 boolean 值,存在 value 返回 true ,不存在返回 false 。具体代码如下:

public void orderSend(Long deliveryId){
    String key = "OrderSendLock_"+ deliveryId;
    boolean lockSuccess = cacheService.setnx(key, value, "NX", "EX",30);
    if (!lockSuccess) {
        throw new BizException("订单已发货,请勿重复点击!");
    }
    try{
        // 以下省略正常业务代码....
    }catch(){
    }
    finally{
        cacheService.del(key);
    }
}

其中 nx 表示key不存在的时候才能 set 成功,ex 表示设置 key 的过去时间,单位是秒。

到这里其实就可以解决我业务当中遇到的问题,但是这种方案也有隐患,在业务压力大或者处理时间长的情况下,可能会出现如下问题:

  • 1.业务处理时间超过30s,导致锁被提前释放,线程 b 新的请求又进来打乱原来的处理顺序。
  • 2.线程 a 业务处理后删除锁,但是删除的是线程 b 新添加的锁,因为线程 a 的锁已经过期,但它并不知道。

解决方案可添加 value 的唯一值,删除锁之前先判断下唯一值。对于业务时间过长的情况可以适当延长锁过期时间。或者使用 Redission 框架,增加一个守护线程,可以适当了解下。考虑到我的业务没有那么复杂,此处就不做过多延伸。


事件二:宿主机访问虚拟机

到此处本来问题已经解决,但是我打算先在本地项目部署 Redis 搞个并发试试情况。没想到部署过程中又出幺蛾子。

我是在 VMware Workstation 上装了个Ubuntu 64 来测试各种服务,毕竟 docker 安装各种应用比 windows 方便很多。拉取 Redis 镜像后,完成容器启动,并设置配置文件映射端口。结果发现在宿主机上怎么也访问不通端口。虚拟机能够 ping 通主机 ip,主机 telnet 虚拟机 ip 失败。把情况交给 chatGPT 后排查一圈,分别是:

  • 1.端口是否映射正确
  • 2.Redis 配置文件配置 bind:0.0.0.0
  • 3.确认网络连接模式,我的是 NAT,让我改桥接但是公司的网络只能登录一台机器,虚拟机登录主机就断网
  • 4.确认双方防火墙是否放行端口

以上内容确认一圈发现都没问题,最后发现是我使用 telnet 命令格式不对

错误:telnet 192.168.xxx.34:6379
正确:telnet 192.168.xxx.34 6379

这下 telnet 也通了,但是使用 Redis 桌面应用,或者在项目里启动还是不行!

跟 chatGPT 问了几个回合也问明白,想了想又问了下 deepseek,结果在ds给的回复当中看到一条:

“验证连接方式:宿主机连接时应使用 宿主机的IP(非虚拟机ip),端口为 VMware NAT 中配置的宿主机端口”

还说啥,修改 ip,一切正常。再想回去感谢下 deepseek,已经系统繁忙,稍后访问,深藏功与名。

后续的并发测试,我采用的是 Apache Bench ,依旧是部署在 Ubuntu,非常方便,使用简单。具体使用方式可以适当检索下,要注意如果提示无法识别参数,可以试试给具体的 URL 上加双引号,防止系统错误识别。