使用redis的比较完美的加锁解锁

tags:redis read&write redis加锁和解锁 php


习惯性说一下写这篇文章要说明什么,我们经常用redis进行加锁操作,目的是为了解决并发可能带来的问题。但是使用redis加锁的方式有多种,本文对常见的几种方式进行解析,并提供一种相对完美的方案。

read & write 问题

这是一个经典问题,请看代码:

    //redis中的某个键自增
$val = $this->redis->get($key);
$val ++;
$this->redis->set($val);

这段代码逻辑没有问题,就是先读取数据,再修改数据,在写回修改,这里是希望每次访问都递增变量$val的值,但在并发情况下,存在情况是两个进程都读取到了一样的初始值,然后都加1,最后写回Redis,这种情况就会统计数据比实际的少。这个问题应该有许多人遇到过,思考过怎么解决这类问题。这里给出一个统一的解决方案,就是尽量保证操作的原子性,比如可以用redis的incr命令来实现自增(可以认为redis的命令是原子的)。

加锁

由上面的问题再进一步,来探讨一个大家常用的,为一个操作进行加锁。

问题场景如下:有一个商品,每个用户都可以去修改商品信息。假设用户id分别为6和8的用户对id为123的商品进行操作。

错误示例1

    $key = '123';
$val = $this->redis->get($key);
if(!$val){
$this->redis->set($key,'123');
$this->redis->expire($key,'4');
/**此处修改商品信息操作
******
**/
$this->redis->del($key);
}else{
echo '错误提示';
}

上面这个错误示例,

错误点1:set和expire是分开写的,如果说程序执行中再执行了set()后出现崩溃,则这个就变成了永久锁(虽然这是个小概率事件)。

错误点2:这个商品中设置的key是商品id,val也是商品id,很多人认为只有一个key就可以了,val是什么无所谓。这就缺少了锁的标识,无法判断这个锁的拥有者是谁,从而会带来一系列影响如下。

  1. 用户1进程获取key对应的val,发现没有锁,所以调用了set,可能在set前,另一个用户2的进程也发现没有这个锁,也进行set,就造成了两个进程都认为自己获取到了锁的情况,
  2. 然后继续,如果1用户的进程执行完了操作,删除了key,用户2进程未执行完毕,此时由于无法识别是否是自己加的锁,就删除了key,这时再有新的进程进入,检查不到锁,可以立即执行,则有可能和用户2的修改冲突。

针对错误1和错误2的第1点,我们只需要去除read & write模式就可以解决,解决方案为

    //同时设置val和过期时间,并使用setnx
$status = $this->redis->setnx($key,$val,$expireTime);
if($status){
/**此处修改商品信息操作
******
**/
$this->redis->del($key);
}else{
echo '错误提示';
}

setnx,可以在设置时检查是否存在锁不存在则设置并返回1,如果存在不覆盖并返回0。

针对错误2第2点,我们需要为每个进程设置一个独立的自己可以识别的val,如果一个用户只能开一个进程,这个val可以为用户id,如果一个用户可以设置多个进程,那么必须按照实际车情况采用其他方式来区分,这里我们以用户id为例,并且在删除的时候只能删除自己的锁。那么这里问题又出现了,如果我们写成这样:

    //同时设置val和过期时间,并使用setnx
$userId = 2;
$status = $this->redis->setnx($key,$userId,$expireTime);
if($status){
/**此处修改商品信息操作
******
**/
if($this->redis->get($key) == $userId){
$this->redis->del($key);
} }else{
echo '错误提示';
}

这种情况看似没有什么问题,其实不然,大家注意我再设置所得时候,设置了一个过期时间,假如这个时间设置的是4秒,那么如果进程A执行到删除前一刻一不小心超过了4秒,那么这个锁就自动消失了。而另一个进程B查到没有锁,就加了一把自己的锁,此时进程A执行删除,就把B的锁给删除了(极小概率事件)。

这里解决方案有两种

  1. 设置比较长的expire时间,弊端:设置的太长,占用内存时间长,设置的太短不能完全解决问题。(可能有人会想不设置过期时间就可以,那么回到最初的错误点,如果程序设置了锁后崩溃了就变成了永久的锁。)
  2. 把对比和删除弄成一个原子操作,这里呢找到了一个方法,就是用redis的eval,把语句变成原子操作。注意redis用的是lua语法,我也是新学的
     //同时设置val和过期时间,并使用setnx
$userId = 2;
$status = $this->redis->setnx($key,$userId,$expireTime);
if($status){
/**此处修改商品信息操作
******
**/
//因为写这个博客的机器没有装redis,所以没有验证这个语法对不对。请大家见谅
$script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
$result = $this->redis->eval(script,array($key,$val),1);
if ($result) {
return true;
} }else{
echo '错误提示';
}

这里就把两个操作变成了一个原子操作。解决的加锁和解锁可能出现的问题。

我们来说一些题外话拓展:在进程有可能出现冲突的地方,一般我们叫做临界区(操作系统中也有这个概念,是通过另一种叫做PV信号量的方式来解决的,其实可以理解为组织等待进程队列,P操作不能获取到资源使用权的则进入等待队列,等待V操作释放资源后,检查是否有等待队列,进行进程释放。当然PV操作也是原子性的。所以说解决相似问题的办法也有一定的相似性)。

                                                                 欢迎大家评论补充   ---  vinter_he

使用redis的比较完美的加锁解锁的更多相关文章

  1. Redis分布式锁---完美实现

    这几天在做项目缓存时候,因为是分布式的所以需要加锁,就用到了Redis锁,正好从网上发现两篇非常棒的文章,来和大家分享一下. 第一篇是简单完美的实现,第二篇是用到的Redisson. Redis分布式 ...

  2. Linux 进程与线程四(加锁--解锁)

    线程共享进程的内存空间,打开的文件描述符,全局变量. 当有多个线程同事访问一块内存空间或者一个变量.一个文件描述符,如果不加控制,那么可能会出现意想不到的结果. 原子操作 对于我们的高级语言(C语言, ...

  3. 进程间通信(IPC)+进程加锁解锁

    [0]README 0.1) source code and text description are from orange's implemention of a os: 0.2) for com ...

  4. 多线程与高并发(二)—— Synchronized 加锁解锁流程

    前言 上篇主要对 Synchronized 的锁实现原理 Monitor 机制进行了介绍,由于 Monitor 基于操作系统调用,上下文切换导致开销大,在竞争不激烈时性能不算很好, 在 jdk6 之后 ...

  5. chattr -lsattr 文件加锁解锁简单用法

    chattr: 加锁文件,无修改,无删除权限. 常用参数:        +a:  可给文件追加内容,但无法删除. +i  加锁文件(文件不能被删除.改名.设定链接关系,同时不能写入或追加内容) -i ...

  6. 从ReentrantLock加锁解锁角度分析AQS

    本文用于记录在学习AQS时,以ReentrantLock为切入点,深入源码分析ReentrantLock的加锁和解锁过程. 同步器AQS的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理 ...

  7. Redission加锁解锁流程

    redission分布式锁的使用 RLock lock = redissonClient.getLock("myLock"); lock.lock(); try { System. ...

  8. 学习笔记:同程旅游缓存系统设计:如何打造Redis时代的完美体系(含PPT)

    内容在:http://chuansong.me/n/478502951177 PPT在:http://pan.baidu.com/s/1nvnOEBf 工具 跟 服务 的差别 从工具到服务之间缺失了哪 ...

  9. 加锁解锁PHP实现 -转载

    PHP并没有完善的线程支持,甚至部署到基于线程模型的httpd服务器都会产生一些问题,但即使是多进程模型下的PHP,也难免出现多进程共同访问同一资源的情况. 比如整个程序共享的数据缓存,或者因为资源受 ...

随机推荐

  1. zTree实现地市县三级级联数据库映射

    zTree实现地市县三级级联数据库映射 Province.hbm.xml: <?xml version="1.0" encoding="UTF-8"?&g ...

  2. com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Unknown database 'user'

    1.错误描述 2014-7-12 21:06:05 com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource getPoolManager 信息: I ...

  3. Java中的java.math.BigInteger

    Java中的java.math.BigInteger /** * */ package com.you.model; /** * @author YouHaidong * */ public clas ...

  4. Linux显示各栏位的标题信息列

    Linux显示各栏位的标题信息列 youhaidong@youhaidong-ThinkPad-Edge-E545:~$ who -H 名称 线路 时间 备注 youhaidong :0 2015-0 ...

  5. Java中的List转换成JSON报错(五)

    1.错误描述 Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/beanu ...

  6. StackExchange.Redis 二次封装

    在NuGet直接搜索StackExchange.Redis,下载引用包: 帮助类: public class RedisUtils { /// <summary> /// redis配置文 ...

  7. EF6CodeFirst+MVC5+Autofac泛型注册 入门实例

    贴一个EF6 CodeFirst模式结合MVC5和Autofac(泛型注册)的一个入门实例 网上类似的例子实在太少,最近自己也有用到这一块的知识,总结了一下,不要让后人踩了自己踩过的坑. 1:新建三个 ...

  8. js去重

    <!DOCTYPE html><html><head lang="en"> <meta charset="UTF-8" ...

  9. iOS学习——自动定位

    最近在项目中需要做自动定位功能,就是你在参加会议通过扫描二维码签到的时候自动定位并将你的定位信息在签到中上传,这样可以避免我们进行假签到.在这个功能中,主要用到的是系统自带的定位模块,首先我们是需要配 ...

  10. 【洛谷1131】【ZJOI2007】时态同步

    题面 题目描述 小Q在电子工艺实习课上学习焊接电路板.一块电路板由若干个元件组成,我们不妨称之为节点,并将其用数字1,2,3-.进行标号.电路板的各个节点由若干不相交的导线相连接,且对于电路板的任何两 ...