Redis的事务是非原子性的,且没有回滚;事务中如果有一条命令执行失败,后续的命令还会执行;
事务中所有命令都会序列化,按顺序地执行;事务在执行过程中,不会被其他客户端发送来的命令打断
事务中单个命令时具有原子性的
在没有exec之前,事务中的命令不会被执行;
一、用法
事务执行流程
1、开启事务:MULTI命令用于开启一个事务;
2、命令入队:客户端继续像服务器发送任意多条命令,这些命令不会立即执行,而是放在一个队列中;
3、
- 执行事务:EXEC命令执行所有在队列中的命令。
- 取消事务:DISCARD放弃执行事务;
//事务开始到执行
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) OK
4) "v1"
//事务开始到取消事务
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> get k1
(nil)
二、事务中遇到错误怎么办??
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 "ab"
QUEUED
127.0.0.1:6379> incr k1 //往字符串上自增
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR value is not an integer or out of range
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr b a c
(error) ERR wrong number of arguments for 'incr' command
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
总结:
如果入队队列是因为语法错误,则直接报错,事务执行也会失败;
如果组队中没有问题,执行中部分成功部分失败;
语法错误是在命令入队前就进行检测的,而类型错误是在执行时检测的
三、为什么Redis不支持回滚 ?
Redis在事务失败时不支持回滚,而是继续执行余下的命令。
优点:Redis命令只因为语法的错误而失败,或者命令用在了错误类型的键上面。也就是说,失败的命令是由编程错误造成的,而这些错误在开发过程中被发现,而不应该出现在生产环境中。
因为不需要对回滚进行支持,所以Redis的内部可以保持简单且快速。
回滚并不能解决编程错误带来的问题;
四、WATCH来实现乐观锁
Redis事务完全具有事务回滚的能力可以用WATCH这个命令;
WATCH:监视一个或多个key,如果事务在执行前,这个key被其他命令修改,则事务被中断,不会执行事务中的任何命令;
UNWATCH可以手动取消对所有键的监视;
WATCH命令可以被调用多次。对键的监视从WATCH执行之后开始生效,直到调用EXEC为止;当EXEC被调用时,不管事务是否成功执行,对所有键的监视都会被取消。
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set money 100
QUEUED
127.0.0.1:6379> incr money //此时所有的命令进入命令队列
QUEUED
//另一个客户端对这个被监控的值进行修改
127.0.0.1:6379> set money 1000
OK
//在第一个客户端执行事务操作发现失败了
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get money
"1000"
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> unwatch
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> get money
QUEUED
127.0.0.1:6379> exec
1) "100"
【此图讲解很详细】
五、php实现代码
<?php
try{
$redis = new Redis();
$redis->connect('127.0.0.1',6379);
} catch(Exception $e){
echo $e->getMessage();
die;
}
$key1 = 'x1';
$key2 = 'x2';
$redis->set($key1,'123');
$redis->set($key2,'234');
try{
$redis->watch($key1);//对key1实行监控
$redis->set($key1,'1234566');//重新设置key1的值
$redis->multi();//开启事务
$redis->set($key1,'1233');
$status = $redis->exec();//执行事务
if(!$status){
$redis->discard();//取消事务
}
}catch(Exception $e){
echo $e->getMessage();
die;
}
六、源码分析
开启事务:
void multiCommand(client *c) {
if (c->flags & CLIENT_MULTI) {
addReplyError(c,"MULTI calls can not be nested");
return;
}
c->flags |= CLIENT_MULTI;//设置客户端为事务模式
addReply(c,shared.ok);//返回结果
}
添加一些命令入队
void queueMultiCommand(client *c) {
multiCmd *mc;
int j;
c->mstate.commands = zrealloc(c->mstate.commands,
sizeof(multiCmd)*(c->mstate.count+1));
mc = c->mstate.commands+c->mstate.count;
mc->cmd = c->cmd;
mc->argc = c->argc;
mc->argv = zmalloc(sizeof(robj*)*c->argc);
memcpy(mc->argv,c->argv,sizeof(robj*)*c->argc);
for (j = 0; j < c->argc; j++)
incrRefCount(mc->argv[j]);
c->mstate.count++;
c->mstate.cmd_flags |= c->cmd->flags;
}
执行:
void execCommand(client *c) {
int j;
robj **orig_argv;
int orig_argc;
struct redisCommand *orig_cmd;
int must_propagate = 0; /* Need to propagate MULTI/EXEC to AOF / slaves? 标记是否需要把multi/exec传递到AOF或者slaves节点*/
int was_master = server.masterhost == NULL;//标记当前redis节点是否为主节点
if (!(c->flags & CLIENT_MULTI)) {
addReplyError(c,"EXEC without MULTI");
return;
}
/* Check if we need to abort the EXEC because: 检查是否需要终止exec
* 1) Some WATCHed key was touched. 一些被监视的key被改变了
* 2) There was a previous error while queueing commands.将命令排入队列时出现以前的错误。
* A failed EXEC in the first case returns a multi bulk nil object
* (technically it is not an error but a special behavior), while
* in the second an EXECABORT error is returned. 第一中错误情况结果为nil,第二种则为error*/
//CAS check-and-set watch命令
if (c->flags & (CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC)) {
addReply(c, c->flags & CLIENT_DIRTY_EXEC ? shared.execaborterr :
shared.nullmultibulk);
discardTransaction(c);//取消事务
goto handle_monitor;
}
/* If there are write commands inside the transaction, and this is a read
* only slave, we want to send an error. This happens when the transaction
* was initiated when the instance was a master or a writable replica and
* then the configuration changed (for example instance was turned into
* a replica). 如果事务中有写命令,而这是一个只读从机,我们希望发送一个错误。*/
if (!server.loading && server.masterhost && server.repl_slave_ro &&
!(c->flags & CLIENT_MASTER) && c->mstate.cmd_flags & CMD_WRITE)
{
addReplyError(c,
"Transaction contains write commands but instance "
"is now a read-only slave. EXEC aborted.");
discardTransaction(c);
goto handle_monitor;
}
/* Exec all the queued commands */
unwatchAllKeys(c); /* Unwatch ASAP otherwise we'll waste CPU cycles尽快取消连接,否则我们将浪费CPU周期 */
orig_argv = c->argv;
orig_argc = c->argc;
orig_cmd = c->cmd;
addReplyMultiBulkLen(c,c->mstate.count);
for (j = 0; j < c->mstate.count; j++) {
c->argc = c->mstate.commands[j].argc;
c->argv = c->mstate.commands[j].argv;
c->cmd = c->mstate.commands[j].cmd;
/* Propagate a MULTI request once we encounter the first command which
* is not readonly nor an administrative one.
* This way we'll deliver the MULTI/..../EXEC block as a whole and
* both the AOF and the replication link will have the same consistency
* and atomicity guarantees. */
if (!must_propagate && !(c->cmd->flags & (CMD_READONLY|CMD_ADMIN))) {
execCommandPropagateMulti(c);
must_propagate = 1;
}
call(c,server.loading ? CMD_CALL_NONE : CMD_CALL_FULL);
/* Commands may alter argc/argv, restore mstate. */
c->mstate.commands[j].argc = c->argc;
c->mstate.commands[j].argv = c->argv;
c->mstate.commands[j].cmd = c->cmd;
}
c->argv = orig_argv;
c->argc = orig_argc;
c->cmd = orig_cmd;
discardTransaction(c);
/* Make sure the EXEC command will be propagated as well if MULTI
* was already propagated. */
if (must_propagate) {
int is_master = server.masterhost == NULL;
server.dirty++;
/* If inside the MULTI/EXEC block this instance was suddenly
* switched from master to slave (using the SLAVEOF command), the
* initial MULTI was propagated into the replication backlog, but the
* rest was not. We need to make sure to at least terminate the
* backlog with the final EXEC. */
if (server.repl_backlog && was_master && !is_master) {
char *execcmd = "*1\r\n$4\r\nEXEC\r\n";
feedReplicationBacklog(execcmd,strlen(execcmd));
}
}
handle_monitor:
/* Send EXEC to clients waiting data from MONITOR. We do it here
* since the natural order of commands execution is actually:
* MUTLI, EXEC, ... commands inside transaction ...
* Instead EXEC is flagged as CMD_SKIP_MONITOR in the command
* table, and we do it here with correct ordering. */
if (listLength(server.monitors) && !server.loading)
replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
}
取消事务:
void discardCommand(client *c) {
//如果客户端没有处于事务状态,则返回错误信息
if (!(c->flags & CLIENT_MULTI)) {
addReplyError(c,"DISCARD without MULTI");
return;
}
//删除事务
discardTransaction(c);
addReply(c,shared.ok);
}
void discardTransaction(client *c) {
freeClientMultiState(c);//释放与MULTI/EXEC状态关联的所有资源
initClientMultiState(c);//初始化相关联的所有资源
c->flags &= ~(CLIENT_MULTI|CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC);
unwatchAllKeys(c);//取消被监控的key,
}
释放队列中的每个命令
void freeClientMultiState(client *c) {
int j;
for (j = 0; j < c->mstate.count; j++) {
int i;
multiCmd *mc = c->mstate.commands+j;
for (i = 0; i < mc->argc; i++)
decrRefCount(mc->argv[i]);
zfree(mc->argv);
}
zfree(c->mstate.commands);
}
WATCH 和UNWATCH
void watchCommand(client *c) {
int j;
if (c->flags & CLIENT_MULTI) {
addReplyError(c,"WATCH inside MULTI is not allowed");//由此可看watch必须要事物开始前使用
return;
}
//watch客户端的各个参数,说明watch命令可以一次watch一个或者多个key
for (j = 1; j < c->argc; j++)
watchForKey(c,c->argv[j]);
addReply(c,shared.ok);
}
void watchForKey(client *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);
}
void unwatchCommand(client *c) {
unwatchAllKeys(c);//取消watch所有的key
c->flags &= (~CLIENT_DIRTY_CAS);
addReply(c,shared.ok);
}
void unwatchAllKeys(client *c) {
listIter li;
listNode *ln;
if (listLength(c->watched_keys) == 0) return;//如果没有watch任何key则直接返回
listRewind(c->watched_keys,&li);
while((ln = listNext(&li))) {
list *clients;
watchedKey *wk;
/* Lookup the watched key -> clients list and remove the client
* from the list查找关注的键->客户端列表并删除客户端从名单上 */
wk = listNodeValue(ln);
clients = dictFetchValue(wk->db->watched_keys, wk->key);
serverAssertWithInfo(c,NULL,clients != NULL);
listDelNode(clients,listSearchKey(clients,c));
/* Kill the entry at all if this was the only client */
if (listLength(clients) == 0)
dictDelete(wk->db->watched_keys, wk->key);
/* Remove this watched key from the client->watched list 从客户端->监视列表中删除此监视的密钥
*/
listDelNode(c->watched_keys,ln);
decrRefCount(wk->key);
zfree(wk);
}
}