1.前言:在整个供应链系统中,会存在很多种单据(采购单、入库单、到货单、运单等),在写的时候对于文档数据(增删改操作)的接口,即使前端做了相关限制,仍然有可能因为网络或者异常操作导致并发重复调用,导致对同一份文件;为了防止这种情况对系统造成异常影响,我们通过Redis实现了一个简单的文档锁,每个请求都需要先获取锁来执行业务逻辑,执行完成后释放锁;对于同一个文档的并发重复操作请求,保证只有一个请求可以获得锁(依赖Redis的单线程),属于悲观锁设计;注意:Redis锁一般只在我们的系统中用于解决并发和重复请求。测试数据的状态,两种机制的结合可以保证整个链路的可靠性。2、加锁机制:主要依靠Redis的setnx命令来实现:但是使用setnx有个问题,就是setnx命令不支持设置过期时间,需要使用expire命令来设置单独给key设置超时时间,这样整个加锁操作就不是原子操作了,有可能是setnx加锁成功了,但是由于程序异常退出导致超时时间没有设置成功。如果不及时解锁,可能会造成死锁(即使业务场景没有死锁,无用的key一直留在内存中也不是什么好的设计);这种情况可以使用Redis事务来解决,将setnx和expire指令作为一个原子操作来执行,但是这样比较繁琐。幸运的是,Redis2.6。Redisset命令在12之后的版本支持nx和ex模式,支持原子设置过期时间:3.加锁的实现(完整的测试代码会在文末贴出):/***添加文档锁*@paramint$intOrderId文档ID*@paramint$intExpireTime锁过期时间(秒)*@returnbool|int如果加锁成功,返回唯一的锁ID,如果加锁失败,返回false*/publicstaticfunctionaddLock($intOrderId,$intExpireTime=self::REDIS_LOCK_DEFAULT_EXPIRE_TIME){//参数验证if(empty($intOrderId)||$intExpireTime<=0){returnfalse;}//获取Redis连接$objRedisConn=self::getRedisConn();//生成一个唯一的锁ID,开锁需要持有这个ID$intUniqueLockId=self::generateUniqueLockId();//根据模板,结合文档ID,生成唯一的Rediskey(一般来说,文档ID在业务系统中是唯一的)$strKey=sprintf(self::REDIS_LOCK_KEY_TEMPLATE,$intOrderId);//加锁(通过Redis的setnx命令实现,从Redis2.6.12开始,setnx也可以通过set命令的可选参数实现,超时时间可以原子设置)$bolRes=$objRedisConn->set($strKey,$intUniqueLockId,['nx','ex'=>$intExpireTime]);//如果加锁成功,返回锁ID,如果加锁失败,返回falsereturn$bolRes?$intUniqueLockId:$bolRes;}4.解锁机制:解锁是在加锁的时候比较唯一的锁id,如果比较成功,则删除key;需要注意的是,解锁过程中也要保证原子性,依赖于rediswatch和transaction实现;WATCH命令可以监视一个或多个键。一旦其中一个键被修改(或删除),后续交易将不会执行。一直监听到EXEC命令(事务中的命令都是在EXEC之后执行的,所以在MULTI命令之后可以修改WATCH监听的key值)5.解锁实现(完整的测试代码会在文末贴出):/***释放文档锁*@paramint$intOrderId文档ID*@paramint$intLockId锁唯一ID*@returnbool*/publicstaticfunctionreleaseLock($intOrderId,$intLockId){//参数校验if(empty($intOrderId)||empty($intLockId)){返回false;}//获取Redis连接$objRedisConn=self::getRedisConn();//生成Redis键$strKey=sprintf(self::REDIS_LOCK_KEY_TEMPLATE,$intOrderId);//监听Rediskey,防止在【比较锁id】和【解锁事务执行】中被修改或删除。提交交易后,会自动取消监听。其他情况需要手动取消监听。$objRedisConn->watch($strKey);如果($intLockId==$objRedisConn->get($strKey)){$objRedisConn->multi()->del($strKey)->exec();返回真;$objRedisConn->unwatch();返回假;}6.附上整体测试代码(此代码仅为简单版)%s';/***默认文档锁定超时(秒)*/constREDIS_LOCK_DEFAULT_EXPIRE_TIME=86400;/***添加文档锁*@paramint$intOrderId文档ID*@paramint$intExpireTime锁过期时间(秒)*@returnbool|int如果加锁成功,返回唯一的锁ID;锁失败返回false*/publicstaticfunctionaddLock($intOrderId,$intExpireTime=self::REDIS_LOCK_DEFAULT_EXPIRE_TIME){//参数验证if(empty($intOrderId)||$intExpireTime<=0){returnfalse;}//获取Redis连接$objRedisConn=self::getRedisConn();//生成一个唯一的锁ID,解锁需要持有这个ID$intUniqueLockId=self::generateUniqueLockId();//根据模板,结合文档ID,生成唯一的Rediskey(一般来说,文档ID在业务系统中是唯一的)$strKey=sprintf(self::REDIS_LOCK_KEY_TEMPLATE,$intOrderId);//加锁(通过Redis的setnx命令实现,从Redis2.6.12开始,setnx也可以通过set命令的可选参数实现,超时时间可以原子设置)$bolRes=$objRedisConn->set($strKey,$intUniqueLockId,['nx','ex'=>$intExpireTime]);//如果加锁成功,则返回锁ID;如果锁定失败,将返回falsereturn$bolRes?$intUniqueLockId:$bolRes;}/***释放文档锁*@paramint$intOrderId文档ID*@paramint$intLockId锁唯一ID*@returnbool*/publicstaticfunctionreleaseLock($intOrderId,$intLockId){//参数检查if(empty($intOrderId)||空($intLockId)){返回假;}//获取Redis连接$objRedisConn=self::getRedisConn();//生成Redis键$strKey=sprintf(self::REDIS_LOCK_KEY_TEMPLATE,$intOrderId);//监控Rediskey,防止在【比较锁id】和【解锁事务执行】中被修改或删除。提交交易后,会自动取消监听。其他情况需要手动取消监听$objRedisConn->watch($strKey);如果($intLockId==$objRedisConn->get($strKey)){$objRedisConn->multi()->del($strKey)->exec();返回真;$objRedisConn->unwatch();返回假;}/***Redis配置:IP*/constREDIS_CONFIG_HOST='127.0.0.1';/***Redis配置:端口*/constREDIS_CONFIG_PORT=6379;/***获取Redis连接(简单版本,单例可用实现)*@paramstring$strIpIP*@paramint$intPort端口*@returnobjectRedis连接*/publicstaticfunctiongetRedisConn($strIp=self::REDIS_CONFIG_HOST,$intPort=self::REDIS_CONFIG_PORT){$objRedis=newRedis();$objRedis->connect($strIp,$intPort);返回$objRedis;}/***用于生成唯一锁ID的Redis密钥*/constREDIS_LOCK_UNIQUE_ID_KEY='lock_unique_id';/***生成锁的唯一ID(简单版可以通过Redis的incr命令实现,可以结合日期、时间戳、余数、字符串填充、随机数等功能生成一个具有指定数字的唯一ID)*@returnmixed*/publicstaticfunctiongenerateUniqueLockId(){returnself::getRedisConn()->incr(self::REDIS_LOCK_UNIQUE_ID_KEY);}}//测试$res1=Lock_Service::addLock('666666');var_dump($res1);//返回锁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,解锁失败
