0

0

PHP实现Redis单据锁以及防止并发重复写入

不言

不言

发布时间:2018-04-10 15:53:34

|

1584人浏览过

|

来源于php中文网

原创

本篇文章给大家分享的内容是php实现redis单据锁以及防止并发重复写入,有着一定的参考价值,有需要的朋友可以参考一下

一、写在前面:
在整个供应链系统中,会有很多种单据(采购单、入库单、到货单、运单等等),在涉及写单据数据的接口时(增删改操作),即使前端做了相关限制,还是有可能因为网络或异常操作产生并发重复调用的情况,导致对相同单据做相同的处理;

为了防止这种情况对系统造成异常影响,我们通过Redis实现了一个简单的单据锁,每个请求需先获取锁才能执行业务逻辑,执行结束后才会释放锁;保证了同一单据的并发重复操作请求只有一个请求可以获取到锁(依赖Redis的单线程),是一种悲观锁的设计;

注:Redis锁在我们的系统中一般只用于解决并发重复请求的情况,对于非并发的的重复请求一般会去数据库或日志校验数据的状态,两种机制结合起来才能保证整个链路的可靠。

二、加锁机制:
主要依赖Redis setnx指令实现:
这里写图片描述
但使用setnx有一个问题,即setnx指令不支持设置过期时间,需要使用expire指令另行为key设置超时时间,这样整个加锁操作就不是一个原子性操作,有可能存在setnx加锁成功,但因程序异常退出导致未成功设置超时时间,在不及时解锁的情况下,有可能会导致死锁(即使业务场景中不会出现死锁,无用的key一直常驻内存也不是很好的设计);

立即学习PHP免费学习笔记(深入)”;

这种情况可以使用Redis事务解决,把setnx与expire两条指令作为一个原子性操作执行,但这样做相对而言会比较麻烦,好在Redis 2.6.12之后版本,Redis set指令支持了nx、ex模式,并支持原子化地设置过期时间:
这里写图片描述
三、加锁实现(完整测试代码会贴在最后):

  /**
     * 加单据锁
     * @param int $intOrderId 单据ID
     * @param int $intExpireTime 锁过期时间(秒)
     * @return bool|int 加锁成功返回唯一锁ID,加锁失败返回false
     */
    public static function addLock($intOrderId, $intExpireTime = self::REDIS_LOCK_DEFAULT_EXPIRE_TIME)
    {
        //参数校验
        if (empty($intOrderId) || $intExpireTime <= 0) {            return false;
        }        //获取Redis连接
        $objRedisConn = self::getRedisConn();        //生成唯一锁ID,解锁需持有此ID
        $intUniqueLockId =  self::generateUniqueLockId();        //根据模板,结合单据ID,生成唯一Redis key(一般来说,单据ID在业务中系统中唯一的)
        $strKey = sprintf(self::REDIS_LOCK_KEY_TEMPLATE, $intOrderId);        //加锁(通过Redis setnx指令实现,从Redis 2.6.12开始,通过set指令可选参数也可以实现setnx,同时可原子化地设置超时时间)
        $bolRes = $objRedisConn->set($strKey, $intUniqueLockId, ['nx', 'ex'=>$intExpireTime]);        //加锁成功返回锁ID,加锁失败返回false
        return $bolRes ? $intUniqueLockId : $bolRes;
    }

四、解锁机制:
解锁即比对加锁时的唯一lock id,如果比对成功,则删除key;需要注意的是,解锁整个过程中同样需要保证原子性,这里依赖redis的watch与事务实现;

WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。监控一直持续到EXEC命令(事务中的命令是在EXEC之后才执行的,所以在MULTI命令后可以修改WATCH监控的键值)  Redis watch与事务可参看简书文章:https://www.jianshu.com/p/361cb9cd13d5

五、解锁实现(完整测试代码会贴在最后):

代悟
代悟

开发者专属的AI搜索引擎

下载
/**
     * 解单据锁
     * @param int $intOrderId 单据ID
     * @param int $intLockId 锁唯一ID
     * @return bool
     */
    public static function releaseLock($intOrderId, $intLockId)
    {
        //参数校验
        if (empty($intOrderId) || empty($intLockId)) {            return false;
        }        //获取Redis连接
        $objRedisConn = self::getRedisConn();        //生成Redis key
        $strKey = sprintf(self::REDIS_LOCK_KEY_TEMPLATE, $intOrderId);        //监听Redis key防止在【比对lock id】与【解锁事务执行过程中】被修改或删除,提交事务后会自动取消监控,其他情况需手动解除监控
        $objRedisConn->watch($strKey);        if ($intLockId == $objRedisConn->get($strKey)) {            $objRedisConn->multi()->del($strKey)->exec();            return true;
        }        $objRedisConn->unwatch();        return false;
    }

六、附整体测试代码(此代码仅为简易版本)

<?php/**
 * Class Lock_Service 单据锁服务
 */class Lock_Service
{    /**
     * 单据锁redis key模板
     */
    const REDIS_LOCK_KEY_TEMPLATE = 'order_lock_%s';    /**
     * 单据锁默认超时时间(秒)
     */
    const REDIS_LOCK_DEFAULT_EXPIRE_TIME = 86400;    /**
     * 加单据锁
     * @param int $intOrderId 单据ID
     * @param int $intExpireTime 锁过期时间(秒)
     * @return bool|int 加锁成功返回唯一锁ID,加锁失败返回false
     */
    public static function addLock($intOrderId, $intExpireTime = self::REDIS_LOCK_DEFAULT_EXPIRE_TIME)
    {        //参数校验
        if (empty($intOrderId) || $intExpireTime <= 0) {            return false;
        }        //获取Redis连接
        $objRedisConn = self::getRedisConn();        //生成唯一锁ID,解锁需持有此ID
        $intUniqueLockId =  self::generateUniqueLockId();        //根据模板,结合单据ID,生成唯一Redis key(一般来说,单据ID在业务中系统中唯一的)
        $strKey = sprintf(self::REDIS_LOCK_KEY_TEMPLATE, $intOrderId);        //加锁(通过Redis setnx指令实现,从Redis 2.6.12开始,通过set指令可选参数也可以实现setnx,同时可原子化地设置超时时间)
        $bolRes = $objRedisConn->set($strKey, $intUniqueLockId, ['nx', 'ex'=>$intExpireTime]);        //加锁成功返回锁ID,加锁失败返回false
        return $bolRes ? $intUniqueLockId : $bolRes;
    }    /**
     * 解单据锁
     * @param int $intOrderId 单据ID
     * @param int $intLockId 锁唯一ID
     * @return bool
     */
    public static function releaseLock($intOrderId, $intLockId)
    {        //参数校验
        if (empty($intOrderId) || empty($intLockId)) {            return false;
        }        //获取Redis连接
        $objRedisConn = self::getRedisConn();        //生成Redis key
        $strKey = sprintf(self::REDIS_LOCK_KEY_TEMPLATE, $intOrderId);        //监听Redis key防止在【比对lock id】与【解锁事务执行过程中】被修改或删除,提交事务后会自动取消监控,其他情况需手动解除监控
        $objRedisConn->watch($strKey);        if ($intLockId == $objRedisConn->get($strKey)) {
            $objRedisConn->multi()->del($strKey)->exec();            return true;
        }
        $objRedisConn->unwatch();        return false;
    }    /**
     * Redis配置:IP
     */
    const REDIS_CONFIG_HOST = '127.0.0.1';    /**
     * Redis配置:端口
     */
    const REDIS_CONFIG_PORT = 6379;    /**
     * 获取Redis连接(简易版本,可用单例实现)
     * @param string $strIp IP
     * @param int $intPort 端口
     * @return object Redis连接
     */
    public static function getRedisConn($strIp = self::REDIS_CONFIG_HOST, $intPort = self::REDIS_CONFIG_PORT)
    {
        $objRedis = new Redis();
        $objRedis->connect($strIp, $intPort);        return $objRedis;
    }    /**
     * 用于生成唯一的锁ID的redis key
     */
    const REDIS_LOCK_UNIQUE_ID_KEY = 'lock_unique_id';    /**
     * 生成锁唯一ID(通过Redis incr指令实现简易版本,可结合日期、时间戳、取余、字符串填充、随机数等函数,生成指定位数唯一ID)
     * @return mixed
     */
    public static function generateUniqueLockId()
    {        return self::getRedisConn()->incr(self::REDIS_LOCK_UNIQUE_ID_KEY);
    }
}//test$res1 = Lock_Service::addLock('666666');
var_dump($res1);//返回lock id,加锁成功$res2 = Lock_Service::addLock('666666');
var_dump($res2);//false,加锁失败$res3 = Lock_Service::releaseLock('666666', $res1);
var_dump($res3);//true,解锁成功$res4 = Lock_Service::releaseLock('666666', $res1);
var_dump($res4);//false,解锁失败

博客搬家:https://segmentfault.com/blog/leeonfancy

相关推荐:

php 在 windows 下扩展redis 的配置与说明

php操作redis的命令分享

redis内存锁与PHP防止并发操作详解

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

1825

2023.10.19

PHP接口编写教程
PHP接口编写教程

本专题整合了PHP接口编写教程,阅读专题下面的文章了解更多详细内容。

594

2025.10.17

php8.4实现接口限流的教程
php8.4实现接口限流的教程

PHP8.4本身不内置限流功能,需借助Redis(令牌桶)或Swoole(漏桶)实现;文件锁因I/O瓶颈、无跨机共享、秒级精度等缺陷不适用高并发场景。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2343

2025.12.29

java接口相关教程
java接口相关教程

本专题整合了java接口相关内容,阅读专题下面的文章了解更多详细内容。

45

2026.01.19

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

743

2023.08.10

windows查看端口占用情况
windows查看端口占用情况

Windows端口可以认为是计算机与外界通讯交流的出入口。逻辑意义上的端口一般是指TCP/IP协议中的端口,端口号的范围从0到65535,比如用于浏览网页服务的80端口,用于FTP服务的21端口等等。怎么查看windows端口占用情况呢?php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

1407

2023.07.26

查看端口占用情况windows
查看端口占用情况windows

端口占用是指与端口关联的软件占用端口而使得其他应用程序无法使用这些端口,端口占用问题是计算机系统编程领域的一个常见问题,端口占用的根本原因可能是操作系统的一些错误,服务器也可能会出现端口占用问题。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

1160

2023.07.27

windows照片无法显示
windows照片无法显示

当我们尝试打开一张图片时,可能会出现一个错误提示,提示说"Windows照片查看器无法显示此图片,因为计算机上的可用内存不足",本专题为大家提供windows照片无法显示相关的文章,帮助大家解决该问题。

832

2023.08.01

Rust内存安全机制与所有权模型深度实践
Rust内存安全机制与所有权模型深度实践

本专题围绕 Rust 语言核心特性展开,深入讲解所有权机制、借用规则、生命周期管理以及智能指针等关键概念。通过系统级开发案例,分析内存安全保障原理与零成本抽象优势,并结合并发场景讲解 Send 与 Sync 特性实现机制。帮助开发者真正理解 Rust 的设计哲学,掌握在高性能与安全性并重场景中的工程实践能力。

4

2026.03.05

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
进程与SOCKET
进程与SOCKET

共6课时 | 0.4万人学习

Redis+MySQL数据库面试教程
Redis+MySQL数据库面试教程

共72课时 | 7.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号