上一篇文章中 我使用原子命令结合lua脚本似乎就已经避免了并发 那问题来了 为啥还需要用锁?这俩有啥区别呢???
Lua 脚本在 Redis 中的原子性与锁的区别主要体现在以下方面:
1. Lua 脚本的原子性原理
- 单线程模型:Redis 使用单线程处理命令,所有命令按顺序执行。Lua 脚本会被视为一个整体任务,执行期间不会被其他命令中断。
- 原子性保证 :脚本内的所有操作(如
GET
、DECR
、RPUSH
)会连续执行,中间不会有其他客户端操作插入。这保证了脚本的原子性。 - 无需锁机制:由于 Redis 的单线程特性,Lua 脚本天然避免了并发冲突,无需额外加锁。
2. 锁的机制
- 显式控制 :锁需要手动获取(如
SETNX
)和释放,确保临界区代码在同一时间只被一个客户端执行。 - 适用场景:适用于分布式系统或多实例环境,协调多个客户端/服务对共享资源的访问。
- 潜在问题:死锁、锁超时、竞争条件等需要额外处理。
3. 关键区别
特性 | Lua 脚本 | 锁 |
---|---|---|
原子性保证 | 由 Redis 单线程模型保证,无需额外操作。 | 需要显式加锁/解锁,且依赖锁的正确实现。 |
性能开销 | 无锁竞争,性能更高。 | 锁的获取/释放可能带来延迟和竞争开销。 |
适用场景 | 单 Redis 实例内的原子操作。 | 分布式系统或多实例间的资源协调。 |
复杂度 | 脚本内实现原子操作,逻辑简单。 | 需处理锁超时、死锁等问题,复杂度较高。 |
4. 示例对比
使用 Lua 脚本扣减库存
lua
-- 原子性由 Redis 单线程保证,无需锁
local stock = tonumber(redis.call('GET', 'stock')) or 0
if stock > 0 then
redis.call('DECR', 'stock')
return 1 -- 成功
end
return 0 -- 失败
使用锁扣减库存
python
# 需要显式加锁,处理锁超时和异常
lock = redis.lock('stock_lock', timeout=10)
try:
if lock.acquire():
stock = redis.get('stock')
if stock > 0:
redis.decr('stock')
return 1 # 成功
return 0 # 失败
finally:
lock.release()
5. 总结
- Lua 脚本:适合在单 Redis 实例中实现原子操作,简单高效,无锁开销。
- 锁:适合分布式环境或跨实例资源协调,但需处理锁的复杂性。
在 Redis 中优先使用 Lua 脚本实现原子操作,除非需要在分布式系统中跨节点协调资源。
尝试封装一个锁工具类 这里我用的php语法,不用细扣语法,单纯参考思路即可
6.代码实例-redis-lock-util
这里是php 不同语言对redis的封装是不一样的 但是逻辑和思路是一致的 重在思路:
php
<?php
/**
* @author 胖虎爱java
* @date 2025/3/18$ 09:26$
*
*/
class base_lib_RedisLock
{
private $redis;
public function __construct()
{
$SRedis = new SRedis();
$SRedis->init();
$this->redis = $SRedis->getRedisInstance();
//创建连接对象 在类初始化的时候
}
public function lock($lock_name, $acquire_timeout, $lock_timeout)
{
//这三个参数分别是锁名 持有锁的时间 以及锁的超时自动销毁时间
$uuid = md5(time() . bin2hex(openssl_random_pseudo_bytes(16)));
$lock_timeout = intval(ceil($lock_timeout));
$lock_end_time = time() + $acquire_timeout;
//这里采用乐观锁 取了一个uuid作为唯一标识区分不同的请求
while (time() < $lock_end_time) {
$lua = <<<LUA
local result = redis.call("setnx", KEYS[1], ARGV[1])
if result == 1 then
redis.call("expire", KEYS[1], ARGV[2])
return 1
elseif redis.call("ttl", KEYS[1]) == -1 then
redis.call("expire", KEYS[1], ARGV[2])
return 0
end
return 0
LUA;
//使用lua命令 不会自己去学
$result = $this->redis->eval($lua, [$lock_name, $uuid, $lock_timeout], 1);
if ($result == 1) {
return $uuid;
}
//每次重试间隔10毫秒
usleep(10000); // 等待10毫秒后重试
}
return false;
}
public function unlock($lock_name, $uuid)
{
//释放锁 通过唯一标识做区分 不然释放错了就惨了
$lua = <<<LUA
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
LUA;
$this->redis->eval($lua, [$lock_name, $uuid], 1);
}
}
封装好后
在控制器内进行处理 具体的处理思路如下
不同语言对redis的封装是不一样的 但是逻辑和思路是一致的 重在思路:
不懂可以看 上一篇文章,我详细解释了redis实现商品秒杀防止超卖
7.秒杀和防止超卖的实现
php
<?php
class controller_product extends components_sbasepage
{
public function __construct($need_login = false)
{
parent::__construct($need_login);
}
// public function pageIndex($inPath)
// {
// 会出现超卖的错误代码示例
// $path_data = base_lib_BaseUtils::sstripslashes($this->getUrlParams($inPath));
// $user_id = base_lib_BaseUtils::getStr($path_data['user_id']);
// $SRedis=new SRedis();
// $SRedis->init();
// $redis= $SRedis->getRedisInstance();
// if ($redis->get("stock:10")!=0)
// {
// $redis->set("stock:10",$redis->get("stock:10")-1);
// if ($redis->lLen("miaosha_user")<10){
// $redis->rPush("miaosha_user",$user_id);
// return base_lib_Return::REDataJson(0,"秒杀成功");
// }else{
// return base_lib_Return::REDataJson(0,"很抱歉秒杀失败");
// }
// }
//}
//redis原子命令实现
public function pageIndex($inPath) {
$path_data = base_lib_BaseUtils::sstripslashes($this->getUrlParams($inPath));
$user_id = base_lib_BaseUtils::getStr($path_data['user_id']);
$SRedis = new SRedis();
$SRedis->init();
$redis = $SRedis->getRedisInstance();
//lua脚本 原子命令
$lua = <<<LUA
local stock_key = KEYS[1]
local list_key = KEYS[2]
local user_id = ARGV[1]
-- 检查库存
local stock = tonumber(redis.call('GET', stock_key) or 0)
if stock <= 0 then return 0 end
-- 扣减库存
local remaining = redis.call('DECR', stock_key)
if remaining < 0 then
redis.call('INCR', stock_key)
return 0
end
-- 加入用户列表
redis.call('RPUSH', list_key, user_id)
return 1
LUA;
// 执行脚本(KEYS[1], KEYS[2], ARGV[1])
$result = $redis->eval($lua, ["stock:10", "miaosha_user", $user_id], 2);
if ($result === 1) {
return base_lib_Return::REDataJson(0, "秒杀成功");
} else {
return base_lib_Return::REDataJson(0, "很抱歉,秒杀失败");
}
}
//分布式锁实现
public function pageIndex2($inPath)
{
$path_data = base_lib_BaseUtils::sstripslashes($this->getUrlParams($inPath));
$user_id = base_lib_BaseUtils::getStr($path_data['user_id']);
$redis = new base_lib_RedisLock();
$result = false;
try {
// 尝试10秒内获取锁,锁有效期5秒
$result = $redis->lock("stock", 1, 5);
if (!$result) {
return base_lib_Return::REDataJson(0, "活动太火爆,请稍后重试");
}
// 复用Redis连接
$redisutil = new base_lib_Cache("redis");
// 原子操作:检查库存并记录用户
$lua = <<<LUA
local stock = redis.call ("get", KEYS[1])
if tonumber(stock) > 0 then
redis.call("decr", KEYS[1])
redis.call("rpush", KEYS[2], ARGV[1])
return 1
end
return 0
LUA;
$success = $redisutil->redis_eval($lua, ["stock:10", "miaosha_user", $user_id], 2);
if (!$success) {
return base_lib_Return::REDataJson(0, "库存不足");
}
return base_lib_Return::REDataJson(1, "秒杀成功");
} catch (Exception $e) {
error_log("秒杀异常[user_id=$user_id]: " . $e->getMessage());
return base_lib_Return::REDataJson(0, "系统繁忙,请重试");
} finally {
if ($result !== false) {
$redis->unlock("stock", $result);
}
}
}
}
8.前端调用示例 js并发多线程
html
<html>
<meta charset="utf-8">
<body>
<textarea style="width: 500px;height: 500px;font-size: 20px" id="box"></textarea>
<script>
// 生成20个随机用户ID(范围:10000-99999)
const generateRandomUserIds = () => {
const ids = new Set();
while (ids.size < 20) {
const timestamp = Date.now().toString().slice(-5); // 取时间戳后5位
const randomPart = Math.floor(Math.random() * 9000) + 1000; // 4位随机数
ids.add(`${timestamp}${randomPart}`);
}
return Array.from(ids);
};
// 修改后的请求函数
function readysell(userId) {
fetch(`/product/index2?user_id=${userId}`, {
method: 'POST',
headers: { 'Accept': 'application/json' }
})
.then(response => {
if (!response.ok) throw new Error('请求失败');
return response.json();
})
.then(data => {
const logMsg = data.data ? `用户 ${userId} 获得锁` : `用户 ${userId} 请求繁忙`;
document.getElementById('box').append(logMsg + '\n')
console.log(logMsg);
})
.catch(error => console.error(error));
}
// 并发模拟逻辑(携带20个随机ID)
function simulateConcurrency() {
const userIds = generateRandomUserIds();
userIds.forEach((userId, index) => {
setTimeout(() => {
console.log(`线程 ${index + 1} 使用ID: ${userId}`);
readysell(userId);
}, Math.random() * 500); // 随机延迟
});
}
// 启动并发测试
simulateConcurrency();
</script>
</body>
</html>
结果:


可以看到防止超卖,并且实现秒杀
如果有帮助到你,是我的荣幸,感谢关注收藏