Redis-分布式锁实现秒杀

上一篇文章,我详细解释了redis实现商品秒杀防止超卖https://blog.csdn.net/m0_68711597/article/details/146313029?spm=1001.2014.3001.5501

上一篇文章中 我使用原子命令结合lua脚本似乎就已经避免了并发 那问题来了 为啥还需要用锁?这俩有啥区别呢???

Lua 脚本在 Redis 中的原子性与锁的区别主要体现在以下方面:


1. Lua 脚本的原子性原理

  • 单线程模型:Redis 使用单线程处理命令,所有命令按顺序执行。Lua 脚本会被视为一个整体任务,执行期间不会被其他命令中断。
  • 原子性保证 :脚本内的所有操作(如 GETDECRRPUSH)会连续执行,中间不会有其他客户端操作插入。这保证了脚本的原子性。
  • 无需锁机制:由于 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>

结果:

可以看到防止超卖,并且实现秒杀

如果有帮助到你,是我的荣幸,感谢关注收藏

相关推荐
咖啡の猫8 分钟前
数据库的基本概念
数据库
小卓笔记43 分钟前
keepalived应用
linux·服务器·数据库
八股文领域大手子2 小时前
Leetcode32 最长有效括号深度解析
java·数据库·redis·sql·mysql
鹏神丶明月天3 小时前
mybatis_plus的乐观锁
java·开发语言·数据库
SelectDB技术团队3 小时前
天翼云:Apache Doris + Iceberg 超大规模湖仓一体实践
大数据·数据库·iceberg·doris·数据湖·湖仓一体·天翼云
zhuyasen3 小时前
在Go语言中的Redis缓存与本地内存缓存实战示例
redis·go·memcached
江湖有缘5 小时前
华为云之MySQL数据的导入导出实践【玩转华为云】
数据库·mysql·华为云
PersistJiao5 小时前
将数据添加到 Couchbase 的 Analytics(分析)服务
数据库·couchbase
java干货仓库5 小时前
Redisson 加锁和释放锁底层是怎么实现的?
java·redis
Honmaple5 小时前
Redis 三主三从集群部署的完整方案
数据库·redis·缓存