使用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,也难免出现多进程共同访问同一资源的情况. 比如整个程序共享的数据缓存,或者因为资源受 ...
随机推荐
- Windows7 64位安装最新版本MySQL服务器
Windows7 64位安装最新版本MySQL服务器 近期,一直在研究MySQL数据库,经常修改配置文件,导致MySQL数据库无法使用,不得不反复重装MySQL数据库.以下是在Windows7 64位 ...
- WebService之CXF注解之二(Service接口)
ITeacherService.java: /** * @Title:ITeacherService.java * @Package:com.you.service * @Description:教师 ...
- 求助:关于sql如何统计时间的问题
三.现在我们假设应用计时分为app应用和web应用,需要考虑如下几个方面: (1)多时间段(2)表中有冗杂数据 (3)用户是在web端和app端都登陆,这种类型的重复时间段只能取其一 存在数据: 存在 ...
- 物联网框架ServerSuperIO在.NetCore实现跨平台的实践路线
正所谓天下大势,不跟风不行.你不跨平台,很low嘛.java说:你们能跨嘛,跨给我看看.C#说:不要强人所难嘛.java说:能部署在云上吗?docker?微服务?C#说:不要强人所难嘛.java说:你 ...
- STM32f4 ARM Bootloader
参考资料: 基于ARM 的嵌入式系统Bootloader 启动流程分析 Bootloader 详解 ( 代码环境 | ARM 启动流程 | uboot 工作流程 | 架构设计) Android系统启动 ...
- 「拆小鹤」使用 python 实现 QQ机器人服务。
使用的是python的qqbot机器人库,我其实只是实现了这个库的一个插件. 具体的说明,我觉得qqbot的官方文档,还有我的插件的注释都写得很详细了,可以直接看.所以有空再写吧. 没错我就是懒..
- Mybatis入门看这一篇就够了
什么是MyBatis MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了google code,并且改名为 ...
- 【BZOJ1415】【NOI2005】聪聪和可可(动态规划,数学期望)
[BZOJ1415][NOI2005]聪聪和可可(动态规划,数学期望) 题面 BZOJ 题解 先预处理出当可可在某个点,聪聪在某个点时 聪聪会往哪里走 然后记忆化搜索一下就好了 #include< ...
- 【Luogu3041】视频游戏的连击(AC自动机,动态规划)
题面链接 题解 首先构建出AC自动机 然后在AC自动机上面跑DP 转移很显然从Trie树的节点跳到他的儿子节点 但是要注意一个问题, 在计算的时候,每一个节点加入后能够 造成的贡献 要加上他的子串的贡 ...
- golang []byte转string
golang中,字符切片[]byte转换成string最简单的方式是 package main import ( "fmt" _ "unsafe" ) func ...