PHP 如何在Redis中实现事物(事物提交和事物回滚)

讲了这么多Redis的使用,今天我们来讲下Redis的事物

1.首先,我们来看一下Redis中事物相关的指令,
命令原型       命令描述
MULTI   用于标记事务的开始,其后执行的命令都将被存入命令队列,直到执行EXEC时,这些命令才会被原子执行.

EXEC   执行在一个事务内命令执行了WATCH命令,那么只有当WATCH所监控的keys没有被修改的前提下,EXEC命令才能执行事务队列中的所有命令,那么只有
当WATCH所监控的keys没有被修改的前提下,EXEC命令才能执行事务队列中的所有命令,否则EXEC将放弃当前事务中的所有命令。

DISCARD   回滚事务队列中的所有命令,同时再将当前连接的状态恢复为正常状态,即非事务状态。如 果WATCH命令被使用,该命令将UNWATCH所有的keys.


WATCH  key[key...]   在MULTI命令执行之前,可以指定待监控的keys,然而在执行EXEC之前,如果被监控的keys发生修改,EXEC将放弃执行该事务队列中的所有指令。

UNWATCH    取消当前事务中指定监控的keys,如果执行了EXEC或DISCARD命令,则无需再手工执行 该命令了,因为在此之后,事务中所有的keys都将自动取消,
和关系型数据库中的事物相比,在redis事物中如果有某一条命令执行失败,其后的命令仍然会被继续执行。


我们可以通过MULTI命令开启一个事务,有关系型数据库开发经验的人可以理解为BEGIN TRANSACTION语句,在该语句之后执行的命令都将被视为事务之内的操作,最后我
们可以通过执行EXEC/DISCARD命令来提交/回滚该事务内的所有操作。这两个Redis命令可被视为等同于关系型数据库中的COMMIT/ROLLERBACK语句。

在事物开启之前,如果客户端与服务端之间出现通讯故障并导致网络断开,其后所有带执行的语句都将不会被服务器执行,然而如果网络中短事件是在客户端执行EXEC命令之后,那么该事务中所有命令都会被服务器执行。

当使用Append-Only模式时,Redis会通过调用系统函数write将该事物内的所有写操作在本次调用全部写入磁盘。然而如果在写入的过程中会出现系统崩溃,如电源故障导致的宕机,那么此时也许只有部分数据被写入到磁盘,而另外一部分数据却已经丢失,Redis服务会在重新启动时执行一系列必要的一致性检测,一旦发现类似问题,就会立即退出并给出相应的错误提示。此刻,我们就要充分利用Redis工具包中提供的Redis-check-aof工具,该工具可以帮助我们定位到数据不一致的错误,并将已经写入的部分数据进行回滚。修复之后我们就可以再次重新启动Redis服务器。



WATCH 命令
WATCH为MULTI执行之前的某个key提供监控(乐观锁)的功能,如果key的值发生了变化,就会放弃事务的执行。

当事务EXEC执行成功之后,就会自动UNWATCH。
下面我们根据一个命令行指令来看一下:
(1)第一步:
127.0.0.1:6379> redis-cli -h 127.0.0.1 -p 6379    //命令拼接redis服务器
ok
127.0.0.1:6379> get test                                      //获取test的键值
"hello world"
127.0.0.1:6379> multi           //生成事务
ok
127.0.0.1:6379> set test "hello mygod"               //修改指令
QUEUED
127.0.0.1:6379>exec                                           //提交事务
1) OK
127.0.0.1:6379>

从上面的命令执行,我们可以看出,当我们生成事务后,执行set指令时,反馈的信息是QUEUED,最后我们再执行
exec,这些命令才会真正的执行。在这里可能还会有人说事务中还有一个rollback操作,但是redis里面好像并没有发现,的确,redis里面是没有rollback操作的,下面我们再进行一个例子的演示:
127.0.0.1:6379>multi
ok
127.0.0.1:6379>set test "my name is god"
QUEUED
127.0.0.1:6379>lpush testName 99
QUEEUED
127.0.0.1:6379>exec
1)OK
2)(error) WRONGTYPE Opertion against a key holding the wrong kind of value
127.0.0.1:6379>

在上面的例子中故意用lpush命令执行string ,可想而知自然不会执行成功,但从结果来看,你看到了,一个OK一个Error,这就违反了事务的原子性,
redis仅仅是个数据结构服务器,多简单的一件事情,退一万步来说,很明显的错误指令它会直接返回的,比如我故意把lpush 写成了lpush1:
127.0.0.1:6379>multi
OK
127.0.0.1:6379> set test "woqunimeimeide"
QUEUED
127.0.0.1:6379> lpush1 testName 44
(error) ERR unknow command 'lpush1'
127.0.0.1:6379>

上面可以看到,命令终止了你的任何输入。


下面我们探索一下Redis事务的原理:

关于事务操作的源代码,大多数都在redis源码中的multi.c文件中,接下来我会一个一个简单剖析一下:
1.multi
在redis的源码中,它大概是这么写的:
void multiCommand(redisClient *c)
{
if(c->flags & REDIS_MULTI){
addReplyError(c,"MULTI calls can not be nested");
return; 
}
c->flags |= REDIS_MULTI;
addReply(c,shared.ok);
}

2.生成命令
在redisClient 中,里面有一个multiState命令:
typedef struct redisClient{
。。。
multiState mstate;           /* MULTI/EXEC state */
}redisClient;

从注释中我们看到了命令和multi/exec肯定有关系,接下来我很好奇的看看multiState的定义:
typedef struct multiState{
multiState *command;     /** Array of MULTI commands */
int count ;    /*Total number of MULTI comands*/
int minreplicas;                /*MINREPLICAS for synchronous as unixtime */
time_t minreplicas_timeout      /*MINREPLICAS timeout as unixtime*/
}multiState;

从multiState这个举例中,你可以看到下面有一个*commad命令,从注释中我们可以看到它其实指向一个数组,这个数组就是若干条在指令,下面还有一个count,可以看到是实际的command的总数。

3.watch 
为了方便说一下后面的exec,这里想说一下watch 大概是怎样实现的,在multi.c源码中是这样写的:
typedef struct watchedKey {
    robj *key;
    redisDb *db;
} watchedKey;


void watchCommand(redisClient *c) {
    int j;


    if (c->flags & REDIS_MULTI) {
        addReplyError(c,"WATCH inside MULTI is not allowed");
        return;
    }
    for (j = 1; j < c->argc; j++)
        watchForKey(c,c->argv[j]);
    addReply(c,shared.ok);
}


/* Watch for the specified key */
void watchForKey(redisClient *c, robj *key) {
    list *clients = NULL;
    listIter li;
    listNode *ln;
    watchedKey *wk;


    /* Check if we are already watching for this key */
    listRewind(c->watched_keys,&li);
    while((ln = listNext(&li))) {
        wk = listNodeValue(ln);
        if (wk->db == c->db && equalStringObjects(key,wk->key))
            return; /* Key already watched */
    }
    /* This key is not already watched in this DB. Let's add it */
    clients = dictFetchValue(c->db->watched_keys,key);
    if (!clients) {
        clients = listCreate();
        dictAdd(c->db->watched_keys,key,clients);
        incrRefCount(key);
    }
    listAddNodeTail(clients,c);
    /* Add the new key to the list of keys watched by this client */
    wk = zmalloc(sizeof(*wk));
    wk->key = key;
    wk->db = c->db;
    incrRefCount(key);
    listAddNodeTail(c->watched_keys,wk);
}
这段代码中最大的核心点就是:
 /* This key is not already watched in this DB. Let's add it */
    clients = dictFetchValue(c->db->watched_keys,key);

关于这个key的所有client ,最后还会塞入到redisclientde watched_keys字典中,如下代码:
 /* Add the new key to the list of keys watched by this client */
    wk = zmalloc(sizeof(*wk));
    wk->key = key;
    wk->db = c->db;
    incrRefCount(key);
    listAddNodeTail(c->watched_keys,wk);



如果画图大概就是下面这样:



其中watched_key是个字典结构,字典的键为上面的key1,key2....,,value为client的链表,这样的话,就可以非常的清楚知道某个key中是被哪些client监视着的。

4.exec
这个命令大概做了两件事:
<1>:判断c-flags = REDIS_DIRTY_EXEC 打开与否,如果是的话,取消事务discardTransation(c),也就是说这个key已经被别的client修改了。
<2>:如果没有修改,那么就for循环执行command[]中命令,如图所示的两处信息:





接下来是一段在php中使用Redis事务的案例:
//关于redis事务的案例
header("content-type:text/html;charset=utf-8");
$redis = new redis();
$redis->connect('localhost', 6379);
//$result = $redis->connect('localhost', 6379);

//$redis = Yii::app()->redis;
$redis->set("testName","33");
//$mywatchkey = $redis->get("mywatchkey");

$mywatchkey = $redis->get('testName');
//($test);exit;

$rob_total = 100;   //抢购数量
if($mywatchkey<$rob_total){
    $redis->watch("testName");
    $redis->multi();

    //设置延迟,方便测试效果。
    sleep(5);
    //插入抢购数据
    $redis->hSet("testName","user_id_".mt_rand(1, 9999),time());
    $redis->set("testName",$mywatchkey+1);
    $rob_result = $redis->exec();
    if($rob_result){
        $mywatchlist = $redis->hGetAll("testName");
        echo "抢购成功!<br/>";
        echo "剩余数量:".($rob_total-$mywatchkey-1)."<br/>";
        echo "用户列表:<pre>";
        var_dump($mywatchlist);
    }else{
        echo "手气不好,再抢购!";exit;
    }
}



在上例是一个秒杀的场景,该部分抢购的功能会被并行执行
通过已销售数量(mywhtchkey)的监控,达到了控制库存,避免超卖的作用。
WHTCH是一个乐观锁,有利于减少并发中的冲突,提高吞吐量。

乐观锁和共享锁
乐观锁(Optimistic Lock)又叫做共享锁,每次别人拿数据的时候都认为别人不会修改数据,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读得应用类型,这样会提高吞吐量。

悲观锁(Pessimistic Lock)又叫做排它锁(x锁),每次拿刀数据的时候都认为别人会修改数据,所以每次在拿到数据的时候都会上锁,这样别人想拿到这个数据就会block直到
它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁,都是在操作之前先上锁。


最后我们介绍一下Redis事务错误处理:
如果一个事务中的某个命令执行出错,Redis将会怎么处理呢?要回答这个问题,我们首先需要知道是什么原因导致命令执行出错:

1.语法错误:
语法错误表示命令不存在或者参数错误
这种情况需要区别Redis版本,Redis2.65之前的版本会忽略错误的命令,执行其他正确的命令,2.65之后的版本会忽略这个事务中的所有命令,都不执行,就比如上面的例子(使用的Redis版本是2.8的);
2.运行错误:
运行错误表示命令执行过程中出现错误,就比如用GET命令去获取一个散列表类型的键值。
这种错误在命令执行之前Redis是无法发现的,所以在事务里这样的命令都会被Redis接受并执行.如果事务里有一条命令执行错误,其他命令依旧会执行(包括出错后的命令)。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值