简单模拟下redis并发的场景:
建立2个文件1.php,2.php,内容如下:
<?php
$redis=new redis();
$redis->connect('127.0.0.1');
$price=$redis->get('price');
sleep(10);
$price=$price+10;
$redis->set('price',$price);
?>
price的初始值为10,分别运行这2个文件,发现最后price的值是20,而不是我们预想中的值30.这就是redis并发问题的一个简单的场景,那么要如何应对呢 ?有以下几种方法可以解决,用什么方法取决于业务需求。
1.使用redis的事务处理
<?php
$redis=new redis();
$redis->connect('127.0.0.1');
function doll(){
global $redis;
$redis->set('price',10);
}
$redis->watch('price');//监听 当监听的值发生变化的话就执行失败
$redis->get('price');
//模拟并发 模拟当其他线程也同时在对price进行写操作 此时事务将执行失败 放在事务开始之前
doll();
$redis->multi();//事务开始标志
$redis->set('price',20);
// sleep(5);
$res=$redis->exec();//执行事务
var_dump($res);exit;
?>
假设price的初始值为0,那么执行完该脚本后,price的值为10,$res的值为false,也就避免了并发操作price的问题。也就是说,当你监听了price之后,如果price的值发生了变化(其他并发操作),那么事务将失败。但是这个场景有个缺点就是,假如某个时刻同时有100个用户进行写操作,由于他们watch的price是同一个值,那么只要有一个人写入成功之后,由于price的值已经发生了变化,所以其他99个用户的操作都将失败。
2.加个自定义锁(排它锁),当有用户在对price进行写操作的时候,其他用户不能读写。
<?php
/**
* 实现Redis分布锁
*/
$redis=new redis();
$redis->connect('127.0.0.1');
$key = 'price'; //要更新信息的缓存KEY
$lockName = 'lock'.$key; //设置锁KEY
$lockExpire = 10; //设置锁的有效期为5秒
//创建锁
$lock=$redis->setnx($lockName,time()+$lockExpire);
//如果锁不存在($lock为true) 说明锁创建成功 也就是说现在没人正在对price进行写操作 可以对price进行写操作
//如果锁存在 但是已经过期 也可以进行对price进行写操作 并重新设置过期时间
if ($lock || ($redis->get($lockName)<time() && $redis->getset($lockName,time()+$lockExpire)<time())) {
//给锁设置生存时间
$redis->expire($lockName, $lockExpire);//当对price写入操作的这部分代码的执行时间大于缓存时间($lockExpire)时,依然有可能会产生并发。所以对price的操作要尽量放在前面(比如当sleep(10)的代码放在set操作之前依然会产生并发)实际线上环境应该测试这部分代码的执行时间,然后$lockExpire的值要大于这个执行时间
//开始对price进行读写
$price=$redis->get('price');
$price=$price+10;
sleep(5);
$redis->set($key,$price);
//锁还没过期就删除 过期的没必要删除
if ($redis->ttl($lockName)) {
$redis->del($lockName);
}
}else{
//如果不是以上情况 说明有人正在对price进行写操作
echo 'please wait!';exit;
}
?>
参考文章:https://mp.weixin.qq.com/s/xVqlgOouFVjK_5vane6g8A
这种方法的缺点也很明显,当有人正在写操作时候,其他人都不能进行相关操作
3.可以使用队列
将用户的操作都存进一个队列中,然后用另一个线程去消费这个队列。这样的好处就是每个用户的操作都可以有效的操作。并且不会有并发操作的情况。
实际场景中应该视需求来决定要用什么办法。