关于Redis的ACID
事务是一个数据库必备的元素,对于redis也不例外,对于一个传统的关系型数据库来说,数据库事务满足ACID四个特性:
- A代表原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
- C代表一致性:事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束
- I代表隔离性:多个事务并发执行时,一个事务的执行不应影响其他事务的执行
- D代表持久性:已被提交的事务对数据库的修改应该永久保存在数据库中
然而,对于redis来说,只满足其中的:
一致性和隔离性两个特性,其他特性是不支持的。
关于redis对ACID四个特性暂时先说这么多,在本文后面会详细说明。在详述之前,我们先来了解redis事务具体的使用和实现,这样我们接下来讨论ACID时才能更好的理解。
redis事务的使用
redis事务主要包括MULTI、EXEC、DISCARD和WATCH命令
MULTI命令
MULTI命令用来开启一个事务,当MULTI执行之后,客户端可以继续向服务器发送多条命令,这些命令会缓存在队列里面,只有当执行EXEC命令时,这些命令才会执行。
而DISCARD命令可以清空事务队列,放弃执行事务。
一个使用MULTI和EXEC执行事务的例子:
redis> MULTI
OK redis> SET book-name "Mastering C++ in 21 days"
QUEUED redis> GET book-name
QUEUED redis> SADD tag "C++" "Programming" "Mastering Series"
QUEUED redis> SMEMBERS tag
QUEUED redis> EXEC
1) OK
2) "Mastering C++ in 21 days"
3) (integer) 3
4) 1) "Mastering Series"
2) "C++"
3) "Programming"
一个事务从开始到执行会经历以下三个阶段:
- 开始事务。
- 命令入队。
- 执行事务。
redis事务产生错误
使用redis事务可能会产生错误,主要分为两大类:
- 事务在执行EXEC之前,入队的命令可能出错。
- 命令可能在 EXEC 调用之后失败
redis在2.6.5之后,如果发现事务在执行EXEC之前出现错误,那么会放弃这个事务。
在EXEC命令之后产生的错误,会被忽略,其他正确的命令会被继续执行。
redis事务不支持回滚
如果你有使用关系式数据库的经验, 那么 “Redis 在事务失败时不进行回滚,而是继续执行余下的命令”这种做法可能会让你觉得有点奇怪。
以下是这种做法的优点:
- Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
- 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。 有种观点认为 Redis 处理事务的做法会产生 bug , 然而需要注意的是, 在通常情况下, 回滚并不能解决编程错误带来的问题。 举个例子, 如果你本来想通过 INCR 命令将键的值加上 1 , 却不小心加上了 2 , 又或者对错误类型的键执行了 INCR , 回滚是没有办法处理这些情况的。
鉴于没有任何机制能避免程序员自己造成的错误, 并且这类错误通常不会在生产环境中出现, 所以 Redis 选择了更简单、更快速的无回滚方式来处理事务。
关于WATCH命令
WATCH命令可以添加监控的键,如果这些监控的键没有被其他客户端修改,那么事务可以顺利执行,如果被修改了,那么事务就不能执行。
redis> WATCH name
OK redis> MULTI
OK redis> SET name peter
QUEUED redis> EXEC
(nil)
以下执行序列展示了上面的例子是如何失败的:
时间 | 客户端 A | 客户端 B |
---|---|---|
T1 | WATCH name |
|
T2 | MULTI |
|
T3 | SET name peter |
|
T4 | SET name john |
|
T5 | EXEC |
在时间 T4 ,客户端 B 修改了 name
键的值, 当客户端 A 在 T5 执行 EXEC 时,Redis 会发现 name
这个被监视的键已经被修改, 因此客户端 A 的事务不会被执行,而是直接返回失败
当客户端发送 EXEC 命令、触发事务执行时, 服务器会对客户端的状态进行检查:
- 如果客户端的
REDIS_DIRTY_CAS
选项已经被打开,那么说明被客户端监视的键至少有一个已经被修改了,事务的安全性已经被破坏。服务器会放弃执行这个事务,直接向客户端返回空回复,表示事务执行失败。 - 如果
REDIS_DIRTY_CAS
选项没有被打开,那么说明所有监视键都安全,服务器正式执行事务。
这里是伪代码
def check_safety_before_execute_trasaction(): if client.state & REDIS_DIRTY_CAS:
# 安全性已破坏,清除事务状态
clear_transaction_state(client)
# 清空事务队列
clear_transaction_queue(client)
# 返回空回复给客户端
send_empty_reply(client)
else:
# 安全性完好,执行事务
execute_transaction()
redis事务的实现
在了解了redis事务的使用之后,我们再来看看redis事务的实现,主要是对上面说的MULTI、EXEC、DISCARD和WATCH命令的源码的分析。
MULTI命令实现
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);
}
从上面的源码可以看出,MULTI只能执行一次,而且就做一件事,把客户端的标志打上REDIS_MULTI。
EXEC命令实现
void execCommand(redisClient *c) {
int j;
robj **orig_argv;
int orig_argc;
struct redisCommand *orig_cmd; if (!(c->flags & REDIS_MULTI)) {
addReplyError(c,"EXEC without MULTI");
return;
} /* Check if we need to abort the EXEC if some WATCHed key was touched.
* A failed EXEC will return a multi bulk nil object. */
if (c->flags & REDIS_DIRTY_CAS) {
freeClientMultiState(c);
initClientMultiState(c);
c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS);
unwatchAllKeys(c);
addReply(c,shared.nullmultibulk);
goto handle_monitor;
} /* Replicate a MULTI request now that we are sure the block is executed.
* 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. */
execCommandReplicateMulti(c); /* Exec all the queued commands */
unwatchAllKeys(c); /* Unwatch ASAP otherwise we'll waste CPU cycles */
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;
call(c,REDIS_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;
freeClientMultiState(c);
initClientMultiState(c);
c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS);
/* Make sure the EXEC command is always replicated / AOF, since we
* always send the MULTI command (we can't know beforehand if the
* next operations will contain at least a modification to the DB). */
server.dirty++; 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 REDIS_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);
}
其主要步骤是:
- 检查是否已经是处在MULTI状态下,如果不是,直接返回
- 检查WATCH的key是否已经被其他客户端修改,如果是,放弃事务
- 执行事务命令队列里面的所有命令
执行命令队列里面的所有命令的代码如下:
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;
call(c,REDIS_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;
}
遍历queue队列的所有命令,一条条的执行
DISCARD命令实现
DICARD命令主要由下面两个函数完成:
void discardCommand(redisClient *c) {
if (!(c->flags & REDIS_MULTI)) {
addReplyError(c,"DISCARD without MULTI");
return;
}
discardTransaction(c);
addReply(c,shared.ok);
} void discardTransaction(redisClient *c) {
freeClientMultiState(c);
initClientMultiState(c);
c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS);;
unwatchAllKeys(c);
} void freeClientMultiState(redisClient *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);
}
函数调用的关系如下
discardCommand
|
|
|
discardTransaction
|
|
|
freeClientMultiState
freeClientMultiState就是完成DISCARD命令主要功能的函数,把其命令队列里面的所有命令所在的存储空间都释放。
WATCH命令
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 lits of keys watched by this client */
wk = zmalloc(sizeof(*wk));
wk->key = key;
wk->db = c->db;
incrRefCount(key);
listAddNodeTail(c->watched_keys,wk);
}
这个函数是在WATCH命令执行后,主要完成两件事情:
- 把key添加到当前客户端c->watched_keys的链表中,
- 并且把当前客户端添加到c->db->watched_keys的字典里面去。
当db中有能修改键状态的命令(比如INCR等)执行时,会自动把watch这个key的客户端的事务设置为放弃状态:
void touchWatchedKey(redisDb *db, robj *key) {
list *clients;
listIter li;
listNode *ln; if (dictSize(db->watched_keys) == 0) return;
clients = dictFetchValue(db->watched_keys, key);
if (!clients) return; /* Mark all the clients watching this key as REDIS_DIRTY_CAS */
/* Check if we are already watching for this key */
listRewind(clients,&li);
while((ln = listNext(&li))) {
redisClient *c = listNodeValue(ln); c->flags |= REDIS_DIRTY_CAS;
}
}
如上源码所示,当某个key被修改后,会遭到其字典中所在的hash链表,然后把链表中所有的客户端都设置成REDIS_DIRTY_CAS状态,即事务被DISCARD的状态。
redis事务的ACID性质讨论
A原子性
单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的
如果一个事务队列中的所有命令都被成功地执行,那么称这个事务执行成功
另一方面,如果 Redis 服务器进程在执行事务的过程中被停止 —— 比如接到 KILL 信号、宿主机器停机,等等,那么事务执行失败
事务失败时,Redis 也不会进行任何的重试或者回滚动作,不满足要么全部全部执行,要么都不执行的条件
C一致性
一致性分下面几种情况来讨论:
首先,如果一个事务的指令全部被执行,那么数据库的状态是满足数据库完整性约束的
其次,如果一个事务中有的指令有错误,那么数据库的状态是满足数据完整性约束的
最后,如果事务运行到某条指令时,进程被kill掉了,那么要分下面几种情况讨论:
- 如果当前redis采用的是内存模式,那么重启之后redis数据库是空的,那么满足一致性条件
如果当前采用RDB模式存储的,在执行事务时,Redis 不会中断事务去执行保存 RDB 的工作,只有在事务执行之后,保存 RDB 的工作才有可能开始。所以当 RDB 模式下的 Redis 服务器进程在事 务中途被杀死时,事务内执行的命令,不管成功了多少,都不会被保存到 RDB 文件里。 恢复数据库需要使用现有的 RDB 文件,而这个 RDB 文件的数据保存的是最近一次的数 据库快照(snapshot),所以它的数据可能不是最新的,但只要 RDB 文件本身没有因为 其他问题而出错,那么还原后的数据库就是一致的
如果当前采用的是AOF存储的,那么可能事务的内容还未写入到AOF文件,那么此时肯定是满足一致性的,如果事务的内容有部分写入到AOF文件中,那么需要用工具把AOF中事务执行部分成功的指令移除,这时,移除之后的AOF文件也是满足一致性的
所以,redis事务满足一致性约束
I隔离性
Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的。
D持久性
因为事务不过是用队列包裹起了一组 Redis 命令,并没有提供任何额外的持久性功能,所以事务的持久性由 Redis 所使用的持久化模式决定
- 在单纯的内存模式下,事务肯定是不持久的
- 在 RDB 模式下,服务器可能在事务执行之后、RDB 文件更新之前的这段时间失败,所以 RDB 模式下的 Redis 事务也是不持久的
- 在 AOF 的“总是 SYNC ”模式下,事务的每条命令在执行成功之后,都会立即调用
fsync
或fdatasync
将事务数据写入到 AOF 文件。但是,这种保存是由后台线程进行的,主线程不会阻塞直到保存成功,所以从命令执行成功到数据保存到硬盘之间,还是有一段非常小的间隔,所以这种模式下的事务也是不持久的。 - 其他 AOF 模式也和“总是 SYNC ”模式类似,所以它们都是不持久的。
来源:https://github.com/Charles0429/redis_doc/blob/master/function/redis_transaction.md
http://redisbook.readthedocs.org/en/latest/feature/transaction.html
关于Redis的ACID的更多相关文章
- 华为云企业级Redis揭秘第16期:超越开源Redis的ACID"真"事务
摘要: 开源Redis只支持伪事务,应用场景受限.高斯Redis发布企业级事务特性,支持完备ACID,为交易.库存等上层业务带来全新可能. 本文分享自华为云社区<华为云企业级Redis揭秘第16 ...
- Redis的ACID属性
事务是数据库的一个重要属性,有关事务的4个特性,原子性.一致性.隔离性.持久性,也就是ACID,这些属性既包含了对事务执行结果的要求,也有数据库在事务执行前后的数据状态变化的要求. Redis可以完全 ...
- Redis 设计与实现 (七)--事务
事务 *ACID,指数据库事务正确执行的四个基本要素的缩写.包含:原子性(Atomicity).一致性(Consistency).隔离性(Isolation).持久性(Durability) redi ...
- 架构师养成记--29.redis开篇
主要有从下几点讲解 NOSQL(Redis) 简介.redis安装与部署 Redis基础事件类型详解 Redis高级命令 Redis与java的使用 Redis集群搭建 Redis集群与spring的 ...
- Redis操作手册
一.Redis简介 1.1 NoSQL NoSQL,泛指非关系型数据库,NoSQL数据库分为四大类:键值存储数据库(Redis,Voldemort,Oracle BDB).列存储数据库(HBase,R ...
- Redis安装和基本操作
Redis 简介Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库. Redis 是属于非关系型数据库1.数据比模型较简单2.需要灵活性更强的IT系统3.对数据库性能 ...
- 刨树根,抓住redis 进行七连问
追着 redis 进行七连问 Hello Redis 有几个问题想请教你 Hello,Redis! 我们相处已经很多年了,从模糊的认识到现在我们已经深入结合,你的好我一直都知道也一直都记住,能否在让我 ...
- Redis 扫盲
Redis扫盲 非关系型数据库分类: 键值存储数据库:主要会使用到一个 Hash 表,这个表有一个特定的键和一个指针指向特定的数据,Redis 列存储数据库:应对分布式存储的海量数据,键仍然存在,但是 ...
- 不能回滚的Redis事务还能用吗
前言 事务是关系型数据库的特征之一,那么作为 Nosql 的代表 Redis 中有事务吗?如果有,那么 Redis 当中的事务又是否具备关系型数据库的 ACID 四大特性呢? Redis 有事务吗 这 ...
随机推荐
- Error connecting to database [Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock' (13)]
参照 http://stackoverflow.com/questions/4448467/cant-connect-to-local-mysql-server-through-socket-var- ...
- salesforce 零基础学习(五十六)实现getById
本来想通过template封装DAO中的getById,结果template中无法选择$(object_name),所以此种想法打消了,直接封装成一个Helper类,方便以后项目中如果有类似需要可以使 ...
- HTML5_06之拖放API、Worker线程、Storage存储
1.拖放API中源对象与目标对象事件间的数据传递: ①创建全局变量--污染全局对象: var 全局变量=null; src.ondragstart=function(){ 全局变量=数据值; ...
- 【Win10 应用开发】自定义应用标题栏
Win 10 app对窗口标题栏的自定义包括两个层面:一是只定义标题中各部分的颜色,如标题栏上文本的颜色.三个系统按钮(最大化,最小化,关闭)的背景颜色等:另一层是把窗口的可视区域直接扩展到标题栏上, ...
- seajs3.0.0源码分析记录
自己边读变加了一些注释,理解了一下seajs3.0.0工作的流程.正则没有一个个去理解,插件模块也没看, 以后有时间了可以补充完整~ 事件系统中事件队列的获取&定义方法 var list = ...
- scikit-learn随机森林调参小结
在Bagging与随机森林算法原理小结中,我们对随机森林(Random Forest, 以下简称RF)的原理做了总结.本文就从实践的角度对RF做一个总结.重点讲述scikit-learn中RF的调参注 ...
- 11.JAVA之GUI编程菜单
功能:添加菜单组件 知识总结: 代码如下: import java.awt.FlowLayout; import java.awt.Frame; import java.awt.Menu; impor ...
- Oracle 中的sql函数以及分页
SELECT LPAD(,'*.') "LPAD example" FROM DUAL; 1.分页查询 (1)方法一:使用 between and 来实现分页 select * ...
- 学C#之设计模式系列笔记(2)观察者模式
一.借鉴说明 1.<Head First Design Patterns>(中文名<深入浅出设计模式>) 2.维基百科,观察者模式,https://zh.wikipedia.o ...
- 解决NTKO Office中文文件名保存到服务器时出现乱码的问题
再使用NTKO office控件时,在ntko往服务器提交文件时,中文文件名会出现乱码的问题! 其实解决这个问题可以换一种思路,在ntko往服务器提交文件时英文肯定是不会出现乱码的问题的! 那么想办法 ...