这几天都在学习Redis的相关知识,发现了一个问题,Redis虽然是单线程的,但是他有一个特点:IO多路复用,这样的特点使2个请求同时对同一key进行操作时,会出现2个请求同时拿到该key的值,进行了重复的操作,在秒杀中的体现为超卖;
具体代码为:
public function redis1(){
$redis = new \Redis();
$redis->connect('127.0.0.1',6379);
for ($i=1;$i<=500;$i++) {
$num = $redis->get('val');
$redis->set('val', $num+1);
usleep(10000);
}
}
使用usleep是因为redis速度太快,不睡眠的话,体现不出来2个请求同时进行。。。
我发现了一个很奇怪的现象,如果我分别请求两次相同的接口,程序是同步进行的,第一个请求进行中的时候,第二个请求会阻塞。当两个接口都执行完毕,查看val的值是1000。(大概因为是在同一个浏览器执行同一个接口)
但我又写了一个一模一样的接口,redis2
public function redis2(){
$redis = new \Redis();
$redis->connect('127.0.0.1',6379);
for ($i=1;$i<=500;$i++) {
$num = $redis->get('val');
$redis->set('val', $num+1);
usleep(10000);
}
}
我分别请求接口redis1和redis2,结果这两个请求是异步的(不同浏览器请求同一个接口也可以),所以也导致了最后val的值是800多,结果是小于1000的,这说明了两个请求在抢占资源,而且可能会同时抢占到同一个资源,然后进行操作。这就是为什么redis还是需要锁机制的原因了。。。
要使用redis的锁,我们需要用到的Redis方法是set,其中第三个参数是option,EX代表持续时间(防止死锁),NX表示如果key已经存在,则返回0,不存在则设值,并返回1。这个方法就很符合锁的特点,代码如下:
public function redisTran1(){
$redis = new \Redis();
$redis->connect('127.0.0.1',6379);
for ($i=1;$i<=500;$i++) {
while (true){
//拿锁的权限,没拿到则continue
if(!$redis->set('lock', 1, ['EX'=>5, 'NX']))
continue;
$redis->incr('val', 1);
$redis->del('lock');//解锁
break;
}
usleep(10000);
}
}
我们也是和上面一样,写一个一模一样的接口,redisTran2,然后分别请求接口redisTran1和redisTran2,最后的val的结果是1000,说明锁机制成功生效了。
2020-2-22 注:
对于redis的原子性,我有了更加深入的理解,redis的原子性,说的是redis的api具有原子性,像第一次测试,val的值小于1000,这样的结果是不具备原子性的;事实上,redis的工作流程如下:
上面图片中的箭头都代表了redis的api,且它们是上一个执行完,下一个才继续执行,api是具有原子性的。
如果想要业务也具有原子性,直接用一步到位的方法,如incr。
此外,根据我的 调查,redis的事务是不具备原子性的,即前一条语句执行失败,后面的还是会继续执行。