在 Redis 里,所谓 SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果,不过很多人没有意识到 SETNX 有陷阱!
比如说:某个查询数据库的接口,因为调用量比较大,所以加了缓存,并设定缓存过期后刷新,问题是当并发量比较大的时候,如果没有锁机制,那么缓存过期的瞬间,大量并发请求会穿透缓存直接查询数据库,造成雪崩效应,如果有锁机制,那么就可以控制只有一个请求去更新缓存,其它的请求视情况要么等待,要么使用过期的缓存。
下面以目前 PHP 社区里最流行的 PHPRedis 扩展为例,实现一段演示代码:
<?php $ok = $redis->setNX($key, $value); if ($ok) { $cache->update(); $redis->del($key); } ?>
缓存过期时,通过 SetNX 获取锁,如果成功了,那么更新缓存,然后删除锁。看上去逻辑非常简单,可惜有问题:如果请求执行因为某些原因意外退出了,导致创建了锁但是没有删除锁,那么这个锁将一直存在,以至于以后缓存再也得不到更新。于是乎我们需要给锁加一个过期时间以防不测:
<?php $redis->multi(); $redis->setNX($key, $value); $redis->expire($key, $ttl); $redis->exec(); ?>
因为 SetNX 不具备设置过期时间的功能,所以我们需要借助 Expire 来设置,同时我们需要把两者用 Multi/Exec 包裹起来以确保请求的原子性,以免 SetNX 成功了 Expire 却失败了。 可惜还有问题:当多个请求到达时,虽然只有一个请求的 SetNX 可以成功,但是任何一个请求的 Expire 却都可以成功,如此就意味着即便获取不到锁,也可以刷新过期时间,如果请求比较密集的话,那么过期时间会一直被刷新,导致锁一直有效。于是乎我们需要在保证原子性的同时,有条件的执行 Expire,接着便有了如下 Lua 代码:
local key = KEYS[1] local value = KEYS[2] local ttl = KEYS[3] local ok = redis.call('setnx', key, value) if ok == 1 then redis.call('expire', key, ttl) end return ok
没想到实现一个看起来很简单的功能还要用到 Lua 脚本,着实有些麻烦。其实 Redis 已经考虑到了大家的疾苦,从 2.6.12 起,SET 涵盖了 SETEX 的功能,并且 SET 本身已经包含了设置过期时间的功能,也就是说,我们前面需要的功能只用 SET 就可以实现。
<?php $ok = $redis->set($key, $value, array('nx', 'ex' => $ttl)); if ($ok) { $cache->update(); $redis->del($key); } ?>
如上代码是完美的吗?答案是还差一点!设想一下,如果一个请求更新缓存的时间比较长,甚至比锁的有效期还要长,导致在缓存更新过程中,锁就失效了,此时另一个请求会获取锁,但前一个请求在缓存更新完毕的时候,如果不加以判断直接删除锁,就会出现误删除其它请求创建的锁的情况,所以我们在创建锁的时候需要引入一个随机值:
<?php $ok = $redis->set($key, $random, array('nx', 'ex' => $ttl)); if ($ok) { $cache->update(); if ($redis->get($key) == $random) { $redis->del($key); } } ?>
补充:本文在删除锁的时候,实际上是有问题的,没有考虑到 GC pause 之类的问题造成的影响,比如 A 请求在 DEL 之前卡住了,然后锁过期了,这时候 B 请求又成功获取到了锁,此时 A 请求缓过来了,就会 DEL 掉 B 请求创建的锁,此问题远比想象的要复杂,具体解决方案参见本文最后关于锁的若干个参考链接。
如此基本实现了单机锁,假如要实现分布锁,请参考:Distributed locks with Redis,不过分布式锁需要注意的地方更多:How to do distributed locking,Is Redlock safe。此外,还有中文版:基于Redis的分布式锁到底安全吗(上/下)。
相关推荐
使用Redis的 SETNX 命令可以实现分布式锁,下文介绍其实现方法。 SETNX命令简介 命令格式 SETNX key value 将 key 的值设为 value,当且仅当 key 不存在。 若给定的 key 已经存在,则 SETNX 不做任何动作。 ...
Redis的SETNX的使用方法1
我就废话不多说了,大家还是直接看代码吧~ <?...//高并发分布式锁 ...charset=utf-8"); $redis = new Redis();...$redis->connect('127.0.0.1', 6379);...echo "Connection to server...$is_lock=$redis->setnx($key,time()+$ex
谈谈redis高性能缓存.docx
Python3的redis分布式锁,使用setnx和lua脚本,提供块和无块函数 用法 import redis from .redis_lock import RedisLock redis_client = redis.Redis(host="127.0.0.1", port=6379, db=0) lock = RedisLock(redis_...
但是在分布式架构中,我们的服务可能会有n个实例,但线程锁只对同一个实例有效,就需要用到分布式锁—-redis setnx 原理 修改某个资源时, 在redis中设置一个key,value根据实际情况自行决定如何表示 我们既然要通过...
一、redis实现分布式锁的主要原理: 1.加锁 最简单的方法是使用setnx命令。key是锁的唯一标识,按业务来决定命名。比如想要给一种商品的秒杀活动加锁,可以给key命名为 “lock_sale_商品ID” 。而value设置成什么呢...
前言 最近在参加学校安排的实训任务,我们小组需完成一套分布式&微服务跨境...主要原理是使用了 redis 的 setnx 去插入一组 key-value,其中 key 要上锁的标识(在项目中是锁死用户 userId),如果上锁失败则返回 fa
概述docs 测试 包裹 通过redis SETNX / BLPOP实现的锁上下文管理器。 免费软件:BSD 2条款许可目标接口完全类似于 。用法因为我们不想要求用户跨进程共享锁实例,所以您必须给他们起名字。 from redis import Redis...
redisredis redis redis redis
Windows 上安装 Redis安装Windows 上安装 Redis安装Windows 上安装 Redis安装Windows 上安装 Redis安装Windows 上安装 Redis安装Windows 上安装 Redis安装Windows 上安装 Redis安装Windows 上安装 Redis安装Windows ...
Redis是一种开源的内存数据结构存储系统,它支持多种数据结构,如字符串、哈希、列表、集合、有序集合等。Redis可以用作数据库、缓存和消息中间件。Redis在性能、可扩展性和灵活性方面表现出色,因此被广泛应用于Web...
Redis7.0.4.zip,解压缩到D盘根目录后,安装后启动为Windows服务 注意是windows 64位系统才可使用,不支持windows 32位系统使用 已经在Win10,Win11,Windows server 2012系统测试运行可用 使用步骤注意事项: ...
redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-5.0.5.redis-...
redis安装 1: 下载redis-5.0.4.tar.gz 2: 解压源码并进入目录 tar zxvf redis-5.0.4.tar.gz cd redis-5.0.4 3: 不用configure 4: 直接make (如果是32位机器 make 32bit) 查看linux机器是32位还是64位的方法:...
Redis实战 Redis实战 Redis实战 Redis实战 Redis实战 Redis实战 Redis实战
redis6.2.6 redis.conf配置文件
redis.conf Redis配置文件 下载 redis.conf 配置详解 Redis配置文件redis.conf 详解1.基本配置内存单位的表示# 1k => 1000 bytes# ...Redis 的详细介绍Redis 的下载地址