讲了这么多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);
}
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将会怎么处理呢?要回答这个问题,我们首先需要知道是什么原因导致命令执行出错:
1.语法错误:
语法错误表示命令不存在或者参数错误
这种情况需要区别Redis版本,Redis2.65之前的版本会忽略错误的命令,执行其他正确的命令,2.65之后的版本会忽略这个事务中的所有命令,都不执行,就比如上面的例子(使用的Redis版本是2.8的);
2.运行错误:
运行错误表示命令执行过程中出现错误,就比如用GET命令去获取一个散列表类型的键值。
这种错误在命令执行之前Redis是无法发现的,所以在事务里这样的命令都会被Redis接受并执行.如果事务里有一条命令执行错误,其他命令依旧会执行(包括出错后的命令)。