使用redis的比较完美的加锁解锁
使用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进程获取key对应的val,发现没有锁,所以调用了set,可能在set前,另一个用户2的进程也发现没有这个锁,也进行set,就造成了两个进程都认为自己获取到了锁的情况,
- 然后继续,如果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的锁给删除了(极小概率事件)。
这里解决方案有两种
- 设置比较长的expire时间,弊端:设置的太长,占用内存时间长,设置的太短不能完全解决问题。(可能有人会想不设置过期时间就可以,那么回到最初的错误点,如果程序设置了锁后崩溃了就变成了永久的锁。)
- 把对比和删除弄成一个原子操作,这里呢找到了一个方法,就是用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的比较完美的加锁解锁的更多相关文章
- Redis分布式锁---完美实现
这几天在做项目缓存时候,因为是分布式的所以需要加锁,就用到了Redis锁,正好从网上发现两篇非常棒的文章,来和大家分享一下. 第一篇是简单完美的实现,第二篇是用到的Redisson. Redis分布式 ...
- Linux 进程与线程四(加锁--解锁)
线程共享进程的内存空间,打开的文件描述符,全局变量. 当有多个线程同事访问一块内存空间或者一个变量.一个文件描述符,如果不加控制,那么可能会出现意想不到的结果. 原子操作 对于我们的高级语言(C语言, ...
- 进程间通信(IPC)+进程加锁解锁
[0]README 0.1) source code and text description are from orange's implemention of a os: 0.2) for com ...
- 多线程与高并发(二)—— Synchronized 加锁解锁流程
前言 上篇主要对 Synchronized 的锁实现原理 Monitor 机制进行了介绍,由于 Monitor 基于操作系统调用,上下文切换导致开销大,在竞争不激烈时性能不算很好, 在 jdk6 之后 ...
- chattr -lsattr 文件加锁解锁简单用法
chattr: 加锁文件,无修改,无删除权限. 常用参数: +a: 可给文件追加内容,但无法删除. +i 加锁文件(文件不能被删除.改名.设定链接关系,同时不能写入或追加内容) -i ...
- 从ReentrantLock加锁解锁角度分析AQS
本文用于记录在学习AQS时,以ReentrantLock为切入点,深入源码分析ReentrantLock的加锁和解锁过程. 同步器AQS的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理 ...
- Redission加锁解锁流程
redission分布式锁的使用 RLock lock = redissonClient.getLock("myLock"); lock.lock(); try { System. ...
- 学习笔记:同程旅游缓存系统设计:如何打造Redis时代的完美体系(含PPT)
内容在:http://chuansong.me/n/478502951177 PPT在:http://pan.baidu.com/s/1nvnOEBf 工具 跟 服务 的差别 从工具到服务之间缺失了哪 ...
- 加锁解锁PHP实现 -转载
PHP并没有完善的线程支持,甚至部署到基于线程模型的httpd服务器都会产生一些问题,但即使是多进程模型下的PHP,也难免出现多进程共同访问同一资源的情况. 比如整个程序共享的数据缓存,或者因为资源受 ...
随机推荐
- MySQL出现“错误1067:进程意外终止”
1.错误描述 2.错误原因 今天,我在摸索如何利用命令查看MySQL日志,查了很多资料,大多数是通过修改my.ini文件配置.我修改了配置后,准备重启MySQL服务器,先执行了net stop mys ...
- Java中的List转换成JSON报错(四)
1.错误描述 Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/colle ...
- python学习之字典(Dictionary)练习
Python字典是另一种可变容器模型,且可存储任意类型对象,如字符串.数字.元组等其他容器模型 字典中分为键值对 , key 类型需要时被哈希. value 类型可以是 字符串.数字.元组等其他容器模 ...
- 命令行工具osql.exe使用
目标: 快速在21个库修改Test表的某条记录,这几个库都分别在不同的服务器上. 通常会想到,到每个库都执行一下语句不就好了吗?这个数据库切换来切换去,挺麻烦了,通过命令行工具osql.exe就可以快 ...
- Codeforces Round #432 Div. 1 C. Arpa and a game with Mojtaba
首先容易想到,每种素数是独立的,相互sg就行了 对于一种素数来说,按照的朴素的mex没法做... 所以题解的简化就是数位化 多个数同时含有的满参数因子由于在博弈中一同变化的,让他们等于相当于,那么这样 ...
- iOS - IM 即时通讯
1.即时通讯技术 即时通讯(IM:Instant Messaging):又称实时通讯,支持用户在线实时交谈,允许两人或多人使用网络实时的传递文字消息.文件.语音与视频交流. 即时通讯在开发中使用的场景 ...
- babel-runtime和babel-polyfill两者区别优缺点
先说两种方式的原理: babel-polyfill 使用场景 Babel 默认只转换新的 JavaScript 语法,而不转换新的 API.例如,Iterator.Generator.Set.Maps ...
- MSIL实用指南-加载null、string、long、float、double等值
本篇讲述怎么加载null.string值.long值.float值.double值. 加载null不需要参数值,只要 Emit ldnull 其它几个命令要 Emit <指令> <值 ...
- php表单提交时获取不到post数据的解决方法
找到了一位博主的方法完美解决,链接如下: http://blog.csdn.net/whd526/article/details/53263181
- 什么是tcp/ip
在了解Tcp /Ip之前.我们需要了解几个名词的含义: 什么是IP? IP层接收由更低层(网络接口层例如以太网设备驱动程序)发来的数据包,并把该数据包发送到更高层---TCP或UDP层:相反,IP层也 ...