Permalink
Browse files

add any thing

  • Loading branch information...
1 parent 855498a commit cedf5f53d50d77f3fdf204dc31f7b3cdfb420f74 @hoterran hoterran committed Aug 18, 2012
Showing with 241 additions and 94 deletions.
  1. +14 −13 index.md
  2. +48 −14 redis-hash-table.md
  3. +37 −6 redis-memory.md
  4. +41 −0 redis-misc.md
  5. +24 −0 redis-network.md
  6. +20 −12 redis-replication.md
  7. +15 −7 redis-server-start.md
  8. +42 −42 redis-transaction.md
View
27 index.md
@@ -1,17 +1,18 @@
#目录
-1. redis介绍
+1. Redis介绍
* 特点,优点,缺点(slave没有tag, ha方案不好)
- * 同类项目的横向比较
+ * 同类项目的横向比较
* 趋势
-
+ * Redis 源码概览
+
2. 客户端服务端安装、配置
* 配置文件参数的说明
* 参数的优化
* 客户端的使用
- * pyredis
- * hiredis
- * jredis
+ * pyRedis
+ * hiRedis
+ * jRedis
* erldis
3. 基本命令使用
@@ -35,30 +36,30 @@
* 排行榜
* 标签(索引)
-5. redis源码分析,part1(数据结构)
+5. Redis 源码分析,part1(数据结构)
* string
* hash
* zipmap
* zset
* list
-6. redis源码分析,part2(工作原理)
- * hash
+6. Redis 源码分析,part2(工作原理)
+ * 哈希
* 事件分离器
* network
* rdb
* aof
* 复制
-7. redismemcahced的差异
+7. Redismemcahced 的差异
* memcached的源码分析
* 网络
* 内存分配
* lru
* jemalloc
* 两者内存使用的差异
-8. redis模块的复用
+8. Redis模块的复用
* ae的使用
* list的使用
* hash的使用
@@ -68,7 +69,7 @@
9.
* timing-wheel
-10. hiredis源码分析
+10. hiRedis源码分析
* 架构
11. script
@@ -78,6 +79,6 @@
* sentinal
* cluster
-13. 更好的使用
+13. 高级用法
* pipeline
*
View
62 redis-hash-table.md
@@ -1,47 +1,81 @@
-##btree and hash
-
+## B 树和哈希
key-value 数据库的 kv 查询的实现有很多种,
比如功能全面的 btree ,而 Redis 的作者选择了简单的 hash 来实现,使用 hash 就意味着无法使用范围查询等功能,但选择更好的 hash 函数可以达到更快的速度,而且代码的实现更简单。
-##Redis哪里用hash
+## Redis 在哪里使用哈希
-在Redis里 hash 无处不在,全局的 key-value 查询,内部的 hash 数据结构,命令与函数指针的关系都是使用 hash。hash的实现在src/dict.c、src/dict.h 里。
+在 Redis 里哈希无处不在,
-常见的命令(例如get,set等)会调用的函数指针,这个数据结构也是以hash table的形式存储的。
-每次客户端输入”set aa bb”等数据的时候,解析得到字符串”set”后,会根据“set”作为一个key,查找到value,一个函数指针(setCommand),然后再把“aa”、“bb“作为参数传给这个函数。这个hash table存储在redisServer->command里,每次redis-server启动的时候会对``readonlyCommandTable`` 这个数组进行加工(populateCommandTable)转化成 redisServer->command 这个 hash table 方便查询,而非遍历``readonlyCommandTable``查找要执行的函数。
+* server->db 全局的键值。
+* server->command 命令与函数指针的关系。
+* 哈希数据结构
-##hash源码分析
+哈希的实现在 src/dict.c,src/dict.h 里。
+##hash源码分析
+
![Redis hash table ](https://raw.github.com/redisbook/book/master/image/redis_dict.png)
-dict 为 hash table 的主结构体,dictht 是为 rehash 而存在的中间数据结构(在客户端的hash table实现中是没有 dictht,见附录3),bucket 就是 hash 算法里的桶,而 dictEntry 就为每个 key-value 结构体。
+dict 为哈希表的主结构体,dictht 是为 rehash 而存在的中间数据结构(在客户端的hash table实现中是没有 dictht,见附录3),bucket 就是哈希算法里的桶,而 dictEntry 就为每个 key-value 结构体。
-dictht ht 指向 2 个 dictht。 存在2个ht的目的是为了在rehash的时候可以平滑的迁移bucket里的数据,而不像client的dict要把老的hash table 里的一次性的全部数据迁移到新的 hash table,这在造成一个密集型的操作,在业务高峰期不可取。
+dictht->ht 指向 2 个 dictht。 存在 2 个 ht 的目的是为了在 rehash 的时候可以平滑的迁移桶里的数据,而不像client的dict要把老的哈希表里的一次性的全部数据迁移到新的哈希表,这种密集型的操作,在业务高峰期不可取。
-每次的key-value查询过程就是,把要查询的key,经过hash函数执行后的值与 dictht->sizemask 求位与,这样就获得一个大于等于 0 小于等于 sizemask 的值,这就定位到了 bucket 数组的位置。bucket 数组的元素是一个 dictEntry 的指针。而 dictEntry 包含一个 next 指针。
+每次的key-value查询过程就是,把要查询的键(Key),经过哈希函数运算后得到值(value)再次与 dictht->sizemask 求位与,这样就获得一个大于等于 0 小于等于 sizemask 的值,这个值决定了桶数组的索引。桶数组的元素是一个 dictEntry 的指针。而 dictEntry 包含一个 next 指针,这就形成了一个 dictEntry 的链表
-发生 hash conflict 的时候,解决 hash 冲突使用的是 seperate chaining(http://en.wikipedia.org/wiki/Hash_table#Separate_chaining) ,直接以链表的形式加到链表的头部,所以查询则是一个O(N)的操作,需要遍历这个dictEntry链表,插入在链表头部时,时间复杂度仅仅为O(1)
+发生哈希冲突之时,解决冲突使用的是 seperate chaining(http://en.wikipedia.org/wiki/Hash_table#Separate_chaining),把新的 dictEntry 加到链表的头部,所以插入是一个O(1)的操作,对于查询则是一个O(N)的操作,需要遍历这个 dictEntry 链表
-dictht->used表示这个hash table里已经插入的key的个数,也就是dictEntry的个数,每次dictAdd成功会+1,dictDel成功会-1。 随着key不断的添加,如果保持bucket数组大小不变,每个bucket元素的的单链表越来越长,查找、删除效率越来越低
+dictht->used 表示这个哈希表里已经插入的键值个数,也就是 dictEntry 的个数,每次 dictAdd 成功会对该值 +1,dictDel 成功会对该值 -1。 随着键值不断的添加,每个桶后面的单链表越来越长,查找、删除效率就变得越来越低
+### 触发 rehash 的条件
+
当dict->used/dict->size >= dict_force_resize_ratio(默认是5)的时候,就认为链表较长了。
于是就有了expand和rehash的,创建一个新的hash table(ht\[1\]),expand ht[1]的bucket数组的长度为ht[0]上的两倍,rehash会把ht[0]上所有的key移动到ht[1]上。
随着 bucket 数量的增多,每个 dictEntry链表的长度就缩短了。而 hash 查找是 O(1) 不会因为 bucket 数组大小的改变而变化,而遍历链表从 O(N) 变为 O(N/2) 的时间复杂度。
-##rehash
+## rehash
+当桶后面的链表越来越长,访问目标键值变慢,就需要 rehash 来加快访问速度。
rehash 并不是一次性的迁移所有的 key,而是随着 dictAdd,dictFind 函数的执行过程调度_dictRehashStep 函数一次一个 bucket 下的 key 从 ht[0] 迁移到 ht[1]。dict->rehashidx 决定哪个 bucket 需要被迁移。当前 bucket 下的 key 都被迁移后,dict->rehashidx++,然后迁移下一个 bucket,直到所有的 bucket下的key被迁走。
-除了 dict_add、dict_find 出发 rehash,另外 Redis 运行过程中会调用 dictRehashMilliseconds 函数,一次 rehash 100个 bucket,直到消耗了1秒才结束 rehash,这样使得即使没有发生查询行为也会进行 rehash 的迁移。
+除了 dict_add、dict_find 出发 rehash,另外在 serverCron 里也会调用 incrementallyRehash 函数,针对每个库的哈希表进行一次最大耗时 1s 的增量哈希,这样使得即使没有发生查询行为也会进行 rehash 的迁移。
+
+ void incrementallyRehash(void) {
+ int j;
+
+ for (j = 0; j < server.dbnum; j++) {
+ if (dictIsRehashing(server.db[j].dict)) {
+ dictRehashMilliseconds(server.db[j].dict,1);
+ break; /* already used our millisecond for this loop... */
+ }
+ }
+ }
+
+dictRehashMilliseconds 一次 rehash 100 个桶。
+
+ int dictRehashMilliseconds(dict *d, int ms) {
+ long long start = timeInMilliseconds();
+ int rehashes = 0;
+
+ while(dictRehash(d,100)) {
+ rehashes += 100;
+ if (timeInMilliseconds()-start > ms) break;
+ }
+ return rehashes;
+ }
+
rehash的具体过程如下,遍历 dict->rehashidx 对应的 bucket 下的 dictEntry 链表的每个key,对 key 进行 hash 函数运算后于 ht[1]->sizemask 求位与,确定 ht[1] 的新 bucket 位置,然后加入到 dictEntry 链表里,然后 ht[0].used--,ht[1].used++。当 ht[0].used=0,释放 ht[0] 的table,再赋值 ht[0] = ht[1]。
在rehash的过程中,如果有新的key加入,直接加到ht[1]。如果key的查找,会先查ht[0]再查询ht[1]。如果key的删除,在ht[0]找到则删除返回,否则继续到ht[1]里寻找。在rehash的过程中,不会再检测是否需要expand。由于ht[1]是ht[0]size的2倍,每次dictAdd的时候都会迁移一个bucket,所以不会出现后ht[1]满了,而ht[0]还有数据的状况。
+
+### resize
+
+
View
43 redis-memory.md
@@ -16,26 +16,41 @@ Redis对 malloc、free、calloc、realloc 等库函数进行了包装(zmalloc.
void zfree(void *ptr) {
void *realptr;
size_t oldsize;
- if (ptr == NULL) return; realptr = (char*)ptr-PREFIX_SIZE;
+ if (ptr == NULL) return;
+ realptr = (char*)ptr-PREFIX_SIZE;
oldsize = *((size_t*)realptr);
update_zmalloc_stat_free(oldsize+PREFIX_SIZE);
free(realptr);
}
update_zmalloc_stat_alloc 会记录全局的内存申请状况 (used_memory),与 redis.conf 里的 maxmmory 就能够控制全局的内存使用。另外还会并对内存划分的大小分组记录(zmalloc_allocations),这样你就对key-value的大小分布非常的清楚,便于接下来的迁移、合并工作。
-#Sharedobjects
+## Sharedobjects
-如果字符串是一个数字,则可以重用已经预分配的redisObject
+对于一些程序常见的字符串(例如协议内的\r\n,OK,error,pong),Redis 提前为我们产生了对象,这样再用的使用就不会额外的申请内存。
+
+ struct sharedObjectsStruct shared;
+
+ struct sharedObjectsStruct
+ {
+ robj *crlf, *ok, *err, *emptybulk, *czero, *cone, *cnegone, *pong, *space,
+ *colon, *nullbulk, *nullmultibulk, *queued,
+ *emptymultibulk, *wrongtypeerr, *nokeyerr, *syntaxerr, *sameobjecterr,
+ *outofrangeerr, *loadingerr, *plus,
+ *select[REDIS_SHARED_SELECT_CMDS],
+ *messagebulk, *pmessagebulk, *subscribebulk, *unsubscribebulk, *mbulk3,
+ *mbulk4, *psubscribebulk, *punsubscribebulk,
+ *integers[REDIS_SHARED_INTEGERS];
+ };
+
+还有从 1 到 REDIS_SHARED_INTEGERS (一般为1000)的数字都已经预分配好了。
for (j = 0; j < REDIS_SHARED_INTEGERS; j++) {
shared.integers[j] = createObject(REDIS_STRING,(void*)(long)j);
shared.integers[j]->encoding = REDIS_ENCODING_INT;
}
-如果发现这个数字的大小,正好在这个范围内(0 - 1000),那么就可以重用这个数字,而不需要动态的 malloc 一个对象了。这种使用引用技术的不变类的方法,在很多虚拟机语言里也常被使用,利用Python,java。
-
-
+当使用这个数字,不需要在栈,或者堆上申请而是引用计数的使用这些不变对象,在很多虚拟机语言里也常被使用,利用 Python,java。
##如何评估内存的使用大小?
@@ -211,3 +226,19 @@ used_memory:817228
简单把zmlen设置为2个字节(可以存储65534个subkey)可以解决这个问题,今天和antirez聊了一下,这会破坏rdb的兼容性,这个功能改进推迟到3.0版本,另外这个缺陷可能是weibo的redis机器cpu消耗过高的原因之一.
+
+## 当内存达到上限后的策略
+
+ lruclock 类似于时间戳,在 serverCron 里被执行
+
+
+当达到使用内存达到 maxmemory,那么有如下几种策略
+
+
+* REDIS_MAXMEMORY_ALLKEYS_LRU 当出现内存不够
+* REDIS_MAXMEMORY_ALLKEYS_RANDOM
+* REDIS_MAXMEMORY_VOLATILE_RANDOM
+* REDIS_MAXMEMORY_VOLATILE_LRU
+
+
+
View
41 redis-misc.md
@@ -199,7 +199,48 @@ encoding
##解读命令数组
+常见的命令(例如get,set等)会调用的函数指针,这个数据结构也是以hash table的形式存储的。
+每次客户端输入”set aa bb”等数据的时候,解析得到字符串”set”后,会根据“set”作为一个key,查找到value,一个函数指针(setCommand),然后再把“aa”、“bb“作为参数传给这个函数。这个hash table存储在redisServer->command里,每次redis-server启动的时候会对``readonlyCommandTable`` 这个数组进行加工(populateCommandTable)转化成 redisServer->command 这个 hash table 方便查询,而非遍历``readonlyCommandTable``查找要执行的函数。
+
我们知道``redis-server``启动的时候会调用``populateCommandTable``函数(src/redis.c 830)把``readonlyCommandTable``数组转化成``server.commands``这个哈希表,``lookupCommand``就是一个简单的哈希取值过程,通过 key(get)找到相应的命令函数指针``getCommand``(t_string.c 437)。
我们来解读一下``readonlyCommandTable``这个结构体。
+ struct redisCommand
+ {
+ char *name; //函数名,对应与哈希表的键
+ redisCommandProc *proc; //函数指针,对应哈希表的值
+ int arity; //函数的参数个数,可以提前对参数个数进行判断
+ int flags; //是否会引发内存的变化
+ /* Use a function to determine which keys need to be loaded
+ * in the background prior to executing this command. Takes precedence
+ * over vm_firstkey and others, ignored when NULL */
+ redisVmPreloadProc *vm_preload_proc;
+ /* What keys should be loaded in background when calling this command? */
+ int vm_firstkey; /* The first argument that's a key (0 = no keys) */
+ int vm_lastkey; /* THe last argument that's a key */
+ int vm_keystep; /* The step between first and last key */
+ };
+
+
+flags 如果为 REDIS_CMD_DENYOOM,则表示这个命令的执行会触发内存的变化,所以如果达到最大使用内存会报错;如果为 REDIS_CMD_FORCE_REPLICATION 则表示这个命令一定要传播到背库。
+
+
+###expire
+
+activeExpireCycle
+
+ REDIS_EXPIRELOOKUPS_PER_CRON
+
+ 255次,或者 消耗的时间 > REDIS_EXPIRELOOKUPS_TIME_LIMIT
+
+
+
+### shutdown
+
+在进程退出之前,会做两件事情。
+
+* fsync aof 文件,强制的aof 文件 fd 对应的文件系统级的缓存写入到磁盘里。
+* 做一次全库的快照,这样下次启动的时候,会自动载入这个快照文件,还能继续该次的数据。
+
+
View
24 redis-network.md
@@ -45,6 +45,30 @@
再根据``redisServer->command``这个哈希表找到命令相应的函数。然后把``argv``里的参数传入相应的函数。
+## call
+
+这是 Redis 最核心函数。执行完相应的命令之后,还有几步工作要做。s
+
+* 记录命令是否导致键值的变化,如果有变化则需要把变化传播到备库。
+
+ if ((dirty > 0 || c->cmd->flags & REDIS_CMD_FORCE_REPLICATION) &&
+ listLength(server.slaves))
+ replicationFeedSlaves(server.slaves,c->db->id,c->argv,c->argc);
+
+* 如果激活 AOF,则还会把变化写入到 AOF 文件。
+
+ if (server.appendonly && dirty > 0)
+ feedAppendOnlyFile(c->cmd,c->db->id,c->argv,c->argc);
+
+* 如果存在监控客户连接,则把命令发送给该客户连接
+
+ if (listLength(server.monitors))
+ replicationFeedMonitors(server.monitors,c->db->id,c->argv,c->argc);
+
+* 判断命令的执行时间是否超过慢日志的阀值,是否需要写入满日志
+
+ slowlogPushEntryIfNeeded(c->argv,c->argc,duration);
+
执行完函数之后,把执行的结果存储在``buf``里,然后再注册一个写事件函数``sendReplyToClient``。
View
32 redis-replication.md
@@ -59,24 +59,32 @@ slave端的server变量与复制相关的变量。
slave端的server->master
- redisClient {
+ redisClient {
+
+ int flags; //slave连接的是什么角色,这里是REDIS_MASTER
- int flags; //slave连接的是什么角色,这里是REDIS_MASTER
-
- }
+ }
服务端的redisClient的信息
- redisClient {
- int flags; //slave连接上来的client,这里是REDIS_SLAVE
- int repldbfd; //传送.rdb数据给slave的时候的fd
- int slaveseldb; //slave的当前db id
- int repldboff; //传送.rdb数据给slave的偏移量,直到等于repldbsize才传送完毕
- int repldbsize; //.rdb文件的大小
- int replstate; //
- }
+ redisClient {
+ int flags; //slave连接上来的client,这里是REDIS_SLAVE
+ int repldbfd; //传送.rdb数据给slave的时候的fd
+ int slaveseldb; //slave的当前db id
+ int repldboff; //传送.rdb数据给slave的偏移量,直到等于repldbsize才传送完毕
+ int repldbsize; //.rdb文件的大小
+ int replstate; //
+ }
+
+
REDIS_CMD_FORCE_REPLICATION 哪些命令必须传输给 slave 端。
+## slave-serve-stale-data
+
+该参数如果设置为 no,表示如果备库与主库尚未完全建立连接之前,仅能接收 info 和 slave 命令。
+
+设置为 no 是表合理的,因为此时备库尚不能称之为备库,因为主库的数据还未同步至此。
+
View
22 redis-server-start.md
@@ -65,19 +65,27 @@ RedisServer 这个结构非常重要,是 Redis 服务端程序唯一的一个
##beforeSleep
- #TODO
+
+ * 清理unblocked_clients??
+ * 处理尚未处理的数据,调用 processInputBuffer 为什么还会有数据呢?
+ * flushAppendOnlyFile
+
##serverCron
-时间事件``serverCron``做很多事情
+时间事件``serverCron``需要做很多事情
- * 出日志展现 Redis 目前的状况。
- * 查看是否需要 rehash 来迁移 keys 到新的 bucket,这个后面会详细讲。
+ * 更新lruclock
+ * 每50次,打印出库内键值状况
+ * 每10次,resize 哈希表??
+ * 调用 incrementallyRehash 增量哈希
* 关闭长时间不工作的 client。
* 处理 bgsave 或者 bgrewriteaof 的子进程退出后的收尾工作。
- * 判断有 keys 的变化而需要执行 bgsave。
- * 清理过期(expire)的 key。
- * 如果自己是主库,会检测备库节点的状况,如果自己是备库,会连接主库。
+ * rewriteAppendOnlyFileBackground ??
+ * 根据键值的变化判断是否需要启动子进程做快照,或者根据AOF文件的当前状况判断是否需要启动子进程执行 AOF 文件重整理工作。
+ * 如果激活 AOF 延迟刷到磁盘机制,则执行一次 AOF 文件的刷到磁盘
+ * 如果是主库,清理过期(expire)的 键值。
+ * 每10次,也就是1s,执行 replicationCron,如果自己是主库,会检测备库节点的状况,如果自己是备库,会连接主库。
##acceptTcpHandler
View
84 redis-transaction.md
@@ -11,31 +11,31 @@ Redis 使用 multi、exec、discard 等命令实现简单事务,可以同时
我们先来看multi,这个命令发送到服务端后,会修改 RedisClient 对象,把 client->flag 设置为 REDIS_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);
- }
-
-从此之后,本来要执行的命令不再执行 call 函数,反而执行了 queueMultiCommand 函数。
-
- int processCommand(RedisClient *c) {
- …........
- if (c->flags & REDIS_MULTI &&
- cmd->proc != execCommand && cmd->proc != discardCommand &&
- cmd->proc != multiCommand && cmd->proc != watchCommand)
- {
- queueMultiCommand(c,cmd);
- addReply(c,shared.queued);
- } else {
- if (server.vm_enabled && server.vm_max_threads > 0 &&
- blockClientOnSwappedKeys(c,cmd)) return REDIS_ERR;
- call(c,cmd);
+ 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);
}
+从此之后,原本要执行的命令不再调用 call 函数,反而执行了 queueMultiCommand 函数,把命令保存到一个管道里。
+
+ int processCommand(RedisClient *c) {
+ ...
+ if (c->flags & REDIS_MULTI &&
+ cmd->proc != execCommand && cmd->proc != discardCommand &&
+ cmd->proc != multiCommand && cmd->proc != watchCommand)
+ {
+ queueMultiCommand(c,cmd);
+ addReply(c,shared.queued);
+ } else {
+ if (server.vm_enabled && server.vm_max_threads > 0 &&
+ blockClientOnSwappedKeys(c,cmd)) return REDIS_ERR;
+ call(c,cmd);
+ }
+
queueMultiCommand 函数是将接下来的命令和参数塞入 RedisClient 对象的一个命令数组 mstate 里。命令都是数组的形式添加到command里,count表示命令的个数,扩展使用的是realloc整个数组。
![multi commands](https://raw.github.com/redisbook/book/master/image/redis_multi_command.png)
@@ -46,31 +46,31 @@ queueMultiCommand 函数是将接下来的命令和参数塞入 RedisClient 对
##watch
-来看watch命令,这个命令非常有用。我们假象一个这样的场景。如果没有INCR命令我们要自憎一个key,我们会如何做?
+来看 watch 命令,这个命令非常有用。我们假象一个这样的场景。如果没有 INCR 命令我们要自增加一个 key,我们会如何做?
- a = Redis.get(key)
- Redis.set(key, a+1)
+ a = Redis.get(key)
+ Redis.set(key, a+1)
-看起来是个不错的方法,假设初值是10,我们修改后应该为11的。如果在get、set之间另外一个client也执行了同样的操作也把key加1。这样key本应该等于12,结果等于了11。如何解决这种问题?
+看起来是个不错的方法,假设初值是 10,我们修改后应该为 11 的。如果在get、set之间另外一个client也执行了同样的操作也把 key 加 1。这样 key 本应该等于12,结果等于了 11。如何解决这种问题?
watch就是为此而生的。从watch开始到exec之间,一旦watch的key发生了变化,则提交失败,否则提交成功,从返回的结果里可以看出提交是否成功。代码如下
- >>> r = Redis.Redis("127.0.0.1", 6379, password="aliyundba")
- >>> r.watch("a")
- True
- >>> z = r.pipeline("a")
- >>> z.set("a", 4)
- <Redis.client.Pipeline object at 0xb7491d74>
- >>> z.execute()
- [True]
+ >>> r = Redis.Redis("127.0.0.1", 6379, password="aliyundba")
+ >>> r.watch("a")
+ True
+ >>> z = r.pipeline("a")
+ >>> z.set("a", 4)
+ <Redis.client.Pipeline object at 0xb7491d74>
+ >>> z.execute()
+ [True]
我们做依次中
- >>> r.watch("a")
- True
- >>> z = r.pipeline("a")
- >>> z.set("a", 5)
- <Redis.client.Pipeline object at 0xb74be8c4>
- >>> z.execute()
- Redis.exceptions.WatchError: Watched variable changed.
+ >>> r.watch("a")
+ True
+ >>> z = r.pipeline("a")
+ >>> z.set("a", 5)
+ <Redis.client.Pipeline object at 0xb74be8c4>
+ >>> z.execute()
+ Redis.exceptions.WatchError: Watched variable changed.

0 comments on commit cedf5f5

Please sign in to comment.