phpredis中的事务
背景
事务的定义
事务指逻辑上的一组操作,组成这组操作的各个单元,要不全部成功,要不全部不成功。
redis中的事务
redis自带了乐观锁watch 和 可以实现悲观锁的setnx,watch提供了并发中依赖公共资源被修改的回滚,setnx则原子性的保证锁不会被其他进程或请求占用。具体的api 可以参考 这儿。
实验
目的
常规的redis操作都是设置和返回的 单个请求,很少有需要多个api组合起来用的,这个算比较复杂的api,这里要验证 phpredis对事务的支持,和phpredis api的使用。分别对multi() Redis::MULTI or Redis::PIPELINE两种模式,和watch 和 没有watch时multi exec的影响。
工具
- wireshark
- php+phpredis 拓展
- redis-cli 和 一个redis服务端
步骤
一、multi + Redis::PIPELINE
$conn = new \Redis();
$conn->connect('192.168.1.9', 6379);
$conn->multi(Redis::PIPELINE);
$conn->set('a', 12);
$conn->set('b', 13);
$conn->get('a');
$ret = $conn->exec();
var_dump($ret);
wireshark抓包
返回
array(3) {
[0] =>
bool(true)
[1] =>
bool(true)
[2] =>
string(2) "12"
}
结论:var_dump可以看到总共只发起了一次请求(SYN和FIN之间C向S发了一个包得到一个ACK,S向C发了一个结果得到了一个ack,实际只算一次信息交换来回四个包),返回也返回了一个包含所有结果的数组。实际上这个api 和 redis本身的multi没有关系,只是phpredis为了减少网络开销借用而已,redis本身就支持一次发送多个命令。
二、multi + Redis::MULTI
$conn = new \Redis();
$conn->connect('192.168.1.9', 6379);
$conn->multi();
$conn->set('a', 12);
$conn->set('b', 13);
$conn->get('a');
$ret = $conn->exec();
var_dump($ret);
wireshark抓包
返回
array(3) {
[0] =>
bool(true)
[1] =>
bool(true)
[2] =>
string(2) "12"
}
结论:var_dump可以看出两次的返回是一摸一样的,但抓包的结果却大相径庭,总共5次请求(multi,3个get set,一个exec)来回,12个网络包,这个也和redis-cli里用multi和exec使用的效果是一致的。
三、multi + Redis::MULTI + 模拟其他进程修改变量
$conn = new \Redis();
$conn->connect('192.168.1.9', 6379);
$conn->multi();
$conn->set('a', 12);
sleep(10); // 给时间偷偷用redis-cli修改 a 的值,这里改成了15
$conn->set('b', 13);
$conn->get('a');
$ret = $conn->exec();
print_r($ret);
返回
array(3) {
[0] =>
bool(true)
[1] =>
bool(true)
[2] =>
string(2) "12"
}
结论:中间修改的15没成功,被exec时的覆盖了,说明单纯的multi和exec本身是没有任何事务特性的,只是exec提交的时候会一起执行(原子性的)。
四、watch + multi + Redis::MULTI + 模拟其他进程修改变量(multi之前)
$conn = new \Redis();
$conn->connect('192.168.1.9', 6379);
$conn->watch('a');
sleep(10); // 给时间偷偷用redis-cli修改 a 的值,这里改成了15
$conn->multi();
$conn->set('a', 12);
$conn->set('b', 13);
$conn->get('a');
$ret = $conn->exec();
print_r($ret);
返回:
array(3) {
[0] =>
bool(true)
[1] =>
bool(true)
[2] =>
string(2) "12"
}
结论:这里返回的是没有事务的结果,因为multi之后才发起了真正的原子性操作,但是因为multi和exec之间的get请求不会实时返回结果,通常multi之前读操作是必不可少的,但是在到multi之前变量被修改了,就算watch了也没有起到事务的作用,比如 抢使用watch秒杀 的例子中,就没有在获得mywatchkey时就watch,而是紧接着multi之前,但也因为这样就算获得了mywatchkey时还有库存,但如果watch之前修改了为没库存也会导致负数库存的出现。
五、watch + multi + Redis::MULTI + 模拟其他进程修改变量(multi之后)
$conn = new \Redis();
$conn->connect('192.168.1.9', 6379);
$conn->watch('a');
$conn->multi();
$conn->set('a', 12);
sleep(10); // 给时间偷偷用redis-cli修改 a 的值,这里改成了15
$conn->set('b', 13);
$conn->get('a');
$ret = $conn->exec();
print_r($ret);
返回
bool(false)
结论:因为watch 到 unwatch(由exec触发)的mutil和exec过程中watch 的 a被修改了返回了false,触发了事务,这里mutli没有用Redis::PIPELINE 因为PIPELINE模式并没有真正的发送multi 验证实际返回的也是设置成功的数组,没有发生事务。
总结
上面几种情况只有 第五种 才包含完成的事务(有二阶提交,可以回滚和重试),如果单纯用multi 用 PIPELINE 就够了(减少网络开销, 反正都是exec的时候才生效)。