├── 1 ├── .gitignore ├── README.md ├── code └── usage │ ├── cache │ ├── hash_cache.rb │ └── string_cache.rb │ ├── counter │ ├── auto_id.rb │ ├── hash_simple_counter.rb │ ├── string_simple_counter.rb │ ├── unique_counter.rb │ └── vote_question.rb │ ├── lock │ ├── lock_scripting_implement.rb │ └── lock_transaction_implement.rb │ ├── log │ ├── fixed_size_log.rb │ ├── list_log.rb │ ├── time_log.rb │ └── timeline.rb │ ├── relation │ └── relation.rb │ └── semaphore │ └── semaphore.rb ├── convention.md ├── image ├── RedisBook.png ├── memcached_slab.png ├── memcached_slabs.png ├── redis-aof-backgroud-thread.png ├── redis.gif ├── redis_adlist.png ├── redis_ae.png ├── redis_aof.png ├── redis_arch.png ├── redis_client.png ├── redis_commands.png ├── redis_db_data_structure.png ├── redis_dict.png ├── redis_dict_add_element.png ├── redis_dict_create.png ├── redis_dict_datastruct_overview.png ├── redis_dict_incremental_rehashing.png ├── redis_multi_command.png ├── redis_network_arch.png ├── redis_network_event_arch.png ├── redis_protocol_command.png ├── redis_replication.png ├── redis_replication_interactive.png ├── redis_replication_state.png ├── redis_sdshdr.png ├── redis_server.png ├── redis_server_data_structure.png ├── redis_slave.png ├── string.jpg ├── usage │ ├── twitter_recommend.png │ ├── twitter_relation.png │ └── vote.png └── zipmap.jpg ├── index.md ├── redis-adlist-implement.md ├── redis-ae.md ├── redis-aof.md ├── redis-backgroud-thread.md ├── redis-dict-implement.md ├── redis-directory-intro.md ├── redis-hash-table.md ├── redis-how-to-use-dict.md ├── redis-intro.md ├── redis-memory.md ├── redis-misc.md ├── redis-network.md ├── redis-protocol.md ├── redis-pubsub.md ├── redis-replication.md ├── redis-sds-implement.md ├── redis-server-data-structure.md ├── redis-server-start.md ├── redis-snapshot.md ├── redis-transaction.md ├── resource.md ├── style.md └── usage ├── automatic_cache_hot_data.md ├── beyond_redis.md ├── cache.md ├── counter.md ├── indexing_and_searching.md ├── limiter.md ├── lock.md ├── log.md ├── message_passing.md ├── relation.md ├── rq_project ├── rq_project_analysis.md ├── rq_worker.dot └── rq_worker.png ├── semaphore.md ├── sorting.md └── tag.md /1: -------------------------------------------------------------------------------- 1 | #简介 2 | 3 | 4 | Redis(REmote DIctionary Server)是一个开源的键-值内存数据库,与它类似的有 memcached,Tokyo Cabinet 等。Redis 以支持丰富的数据结构著称,同时兼具主从复制、持久化等高可用特性,与程序无缝的结合, 5 | 6 | 7 | ##无第三方库依赖 8 | 9 | memcached 使用 libevent 这个已经不那么轻量级的网络事件库,而 Redis 本身不依赖任何第三方的函数库,无论是网络事件、哈希表,数据结构都是自己实现的,全部代码只有 2w 行,算是一个小型的项目,代码清晰,阅读起来非常的流畅,甚至都无须 debug 调试来辅助理解。 10 | 11 | 12 | ##从哪里开始 13 | 14 | 本书将对其的源代码进行分析,版本为2.4.16。下载源代码解压 redis.tar.gz 包后,进入``redis``目录,我们从``src/Redis.c``的主函数开始我们代码旅行。 15 | 16 | 推荐读者使用 cscope 这样可以很方便的从函数之间跳转,如何使用 cscope 可见附录1,如何设置vim的快捷键可见附录2。 17 | 18 | 19 | ##为何redis是单线程? 20 | 21 | 由于支持复杂的数据结构,所以如果采用多线程,将非常的麻烦,试想一个双链表,要支持多线程将多么的复杂。 22 | 23 | Redis 并不是一个``fit all``的键-值数据库,单线程意味着任何一个客户端连接的处理速度影响的全局的性能,所以一些较消耗性能的操作(set的操作或者zset的排序操作)都尽量移动到备库来处理。 24 | 25 | 受内存限制的特点使得目前Redis不能成为处理海量数据的total solutions,而仅仅是一个复杂大型系统里的一部分。 26 | 27 | 28 | ##缺陷 29 | 30 | Redis 有很多缺陷,比如无法做到双主库 ,无法进行同步复制(当然这些都是可以改进的)。复制做增量复制,复制容易受网络的影响, 无完美的集群方案,但这不影响 Redis 成为一个优秀的可信赖的组件。 31 | 32 | 33 | #同类项目 34 | 35 | ##memcached 36 | 37 | ##tc 38 | 39 | #趋势 40 | 41 | 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redis源代码分析系列文章 2 | 3 | ## 4 | style.md 风格 5 | convention.md 约定 6 | index.md 目录 7 | 8 | ##DOING 9 | redis-server-start.md 10 | 11 | ##TODO 12 | 1. 原来图片都是用dia,xmind,gliffy画的,准备全部改成graphviz,工程浩大... 13 | 14 | ##DONE 15 | redis-intro.md 16 | redis-directory-intro.md 17 | 18 | 19 | ##附录 20 | 1. 利用cscope和vim看代码 21 | 2. graphviz的使用 22 | -------------------------------------------------------------------------------- /code/usage/cache/hash_cache.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | 3 | $redis = Redis.new 4 | 5 | def set(hash_name, key, value) 6 | return $redis.hset(hash_name, key, value) 7 | end 8 | 9 | def get(hash_name, key) 10 | return $redis.hget(hash_name, key) 11 | end 12 | 13 | def expire(hash_name, timeout) 14 | return $redis.expire(hash_name, timeout) 15 | end 16 | -------------------------------------------------------------------------------- /code/usage/cache/string_cache.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | 3 | $redis = Redis.new 4 | 5 | def set(key, value, timeout=nil) 6 | if timeout == nil 7 | return $redis.set(key, value) 8 | else 9 | return $redis.setex(key, timeout, value) 10 | end 11 | end 12 | 13 | def get(key) 14 | return $redis.get(key) 15 | end 16 | -------------------------------------------------------------------------------- /code/usage/counter/auto_id.rb: -------------------------------------------------------------------------------- 1 | load 'string_simple_counter.rb' 2 | 3 | def generate_id(tag) 4 | return incr(tag) 5 | end 6 | -------------------------------------------------------------------------------- /code/usage/counter/hash_simple_counter.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | 3 | $redis = Redis.new 4 | 5 | def incr(hash, counter, increment=1) 6 | return $redis.hincrby(hash, counter, increment) 7 | end 8 | 9 | def decr(hash, counter, decrement=1) 10 | return $redis.hincrby(hash, counter, -decrement) 11 | end 12 | 13 | def get(hash, counter) 14 | value = $redis.hget(hash, counter) 15 | return value.to_i if value != nil 16 | end 17 | 18 | def reset(hash, counter) 19 | value = $redis.hset(hash, counter, 0) 20 | return 0 21 | end 22 | -------------------------------------------------------------------------------- /code/usage/counter/string_simple_counter.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | 3 | $redis = Redis.new 4 | 5 | def incr(counter, increment=1) 6 | return $redis.incrby(counter, increment) 7 | end 8 | 9 | def decr(counter, decrement=1) 10 | return $redis.decrby(counter, decrement) 11 | end 12 | 13 | def get(counter) 14 | value = $redis.get(counter) 15 | return value.to_i if value != nil 16 | end 17 | 18 | def reset(counter) 19 | value = $redis.set(counter, 0) 20 | return 0 if value == "OK" 21 | end 22 | -------------------------------------------------------------------------------- /code/usage/counter/unique_counter.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | 3 | $redis = Redis.new 4 | 5 | def add(counter, member) 6 | return $redis.sadd(counter, member) 7 | end 8 | 9 | def remove(counter, member) 10 | return $redis.srem(counter, member) 11 | end 12 | 13 | def is_member?(counter ,member) 14 | return $redis.sismember(counter, member) 15 | end 16 | 17 | def members(counter) 18 | return $redis.members(counter) 19 | end 20 | 21 | def count(counter) 22 | return $redis.scard(counter) 23 | end 24 | -------------------------------------------------------------------------------- /code/usage/counter/vote_question.rb: -------------------------------------------------------------------------------- 1 | load 'unique_counter.rb' 2 | 3 | def vote_up(question_id, user_id) 4 | if voted?(question_id, user_id) 5 | raise "alread voted" 6 | end 7 | return add("question-vote-up #{question_id}", user_id) 8 | end 9 | 10 | def vote_down(question_id, user_id) 11 | if voted?(question_id, user_id) 12 | raise "alread voted" 13 | end 14 | return add("question-vote-down #{question_id}", user_id) 15 | end 16 | 17 | def voted?(question_id, user_id) 18 | return (is_member?("question-vote-up #{question_id}", user_id) or \ 19 | is_member?("question-vote-down #{question_id}", user_id)) 20 | end 21 | 22 | def count_vote_up(question_id) 23 | return count("question-vote-up #{question_id}") 24 | end 25 | 26 | def count_vote_down(question_id) 27 | return count("question-vote-down #{question_id}") 28 | end 29 | -------------------------------------------------------------------------------- /code/usage/lock/lock_scripting_implement.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | require "redis" 4 | 5 | $redis = Redis.new 6 | 7 | def acquire(key, timeout, uid) 8 | 9 | script = " 10 | if redis.call('exists', KEYS[1]) == 0 then 11 | return redis.call('setex', KEYS[1], ARGV[1], ARGV[2]) 12 | end 13 | " 14 | 15 | return "OK" == $redis.eval(script, :keys => [key], :argv => [timeout, uid]) 16 | 17 | end 18 | 19 | def release(key, uid) 20 | 21 | script = " 22 | if redis.call('get', KEYS[1]) == ARGV[1] then 23 | return redis.call('del', KEYS[1]) 24 | end 25 | " 26 | 27 | return 1 == $redis.eval(script, :keys => [key], :argv => [uid]) 28 | 29 | end 30 | -------------------------------------------------------------------------------- /code/usage/lock/lock_transaction_implement.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | require "redis" 4 | 5 | $redis = Redis.new 6 | 7 | def acquire(key, timeout, uid) 8 | 9 | $redis.watch(key) 10 | 11 | # 已经被其他客户端加锁? 12 | if $redis.exists(key) 13 | $redis.unwatch 14 | return false 15 | end 16 | 17 | # 尝试加锁 18 | result = $redis.multi do |t| 19 | t.setex(key, timeout, uid) 20 | end 21 | 22 | # 加锁成功? 23 | return result != nil 24 | 25 | end 26 | 27 | def release(key, uid) 28 | 29 | $redis.watch(key) 30 | 31 | # 锁不存在或已经释放? 32 | if $redis.exists(key) == false 33 | $redis.unwatch 34 | return true 35 | end 36 | 37 | # 比对 uid ,如果匹配就删除 key 38 | if uid == $redis.get(key) 39 | result = $redis.multi do |t| 40 | t.del(key) 41 | end 42 | # 删除成功? 43 | return result != nil 44 | else 45 | return false 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /code/usage/log/fixed_size_log.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | 3 | LENGTH = 4 4 | 5 | $redis = Redis.new 6 | 7 | def write(category, content) 8 | raise "Content's length must equal to #{LENGTH}" unless content.length == LENGTH 9 | return $redis.append(category, content) 10 | end 11 | 12 | def read(category, n) 13 | return $redis.getrange(category, n*LENGTH, (n+1)*LENGTH-1) 14 | end 15 | 16 | def read_all(category) 17 | all_log = $redis.get(category) 18 | total_log_length = count(category) 19 | 20 | arr = Array.new 21 | 0.upto(total_log_length-1) do |i| 22 | arr << all_log[i*LENGTH ... (i+1)*LENGTH] 23 | end 24 | 25 | return arr 26 | end 27 | 28 | def count(category) 29 | total_log_length = $redis.strlen(category) 30 | if total_log_length == 0 31 | return 0 32 | else 33 | return total_log_length / LENGTH 34 | end 35 | end 36 | 37 | def flush(category) 38 | return $redis.del(category) 39 | end 40 | -------------------------------------------------------------------------------- /code/usage/log/list_log.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | 3 | $redis = Redis.new 4 | 5 | def write(category, content) 6 | return $redis.rpush(category, content) 7 | end 8 | 9 | def read(category, n) 10 | return $redis.lindex(category, n) 11 | end 12 | 13 | def read_all(category) 14 | return $redis.lrange(category, 0, -1) 15 | end 16 | 17 | def count(category) 18 | return $redis.llen(category) 19 | end 20 | 21 | def flush(category) 22 | return $redis.del(category) 23 | end 24 | -------------------------------------------------------------------------------- /code/usage/log/time_log.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | 3 | $redis = Redis.new 4 | 5 | def write(category, content) 6 | return $redis.zadd(category, Time.now.to_f, content) 7 | end 8 | 9 | def read(category, n) 10 | return $redis.zrange(category, n, n, :with_scores => true).first 11 | end 12 | 13 | def read_all(category) 14 | return $redis.zrange(category, 0, -1, :with_scores => true) 15 | end 16 | 17 | def count(category) 18 | return $redis.zcard(category) 19 | end 20 | 21 | def flush(category) 22 | return $redis.del(category) 23 | end 24 | -------------------------------------------------------------------------------- /code/usage/log/timeline.rb: -------------------------------------------------------------------------------- 1 | load 'time_log.rb' 2 | 3 | def recent(category, n) 4 | return $redis.zrevrange(category, 0, n-1, :with_scores => true) 5 | end 6 | -------------------------------------------------------------------------------- /code/usage/relation/relation.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | 3 | $redis = Redis.new 4 | 5 | def follow(my_id, target_id) 6 | # 将目标添加到我的 following 集合里 7 | following_status = $redis.sadd(following_key(my_id), target_id) 8 | 9 | # 将我加到目标的 follower 集合里 10 | follower_status = $redis.sadd(follower_key(target_id), my_id) 11 | 12 | # 返回状态 13 | return following_status && follower_status 14 | end 15 | 16 | def unfollow(my_id, target_id) 17 | # 将目标从我的 following 集合中移除 18 | following_status = $redis.srem(following_key(my_id), target_id) 19 | 20 | # 将我从目标的 follower 集合中移除 21 | follower_status = $redis.srem(follower_key(target_id), my_id) 22 | 23 | # 返回状态 24 | return following_status && follower_status 25 | end 26 | 27 | 28 | # 关注 29 | 30 | def following(my_id) 31 | return $redis.smembers(following_key(my_id)) 32 | end 33 | 34 | def count_following(my_id) 35 | return $redis.scard(following_key(my_id)) 36 | end 37 | 38 | 39 | # 被关注 40 | 41 | def follower(my_id) 42 | return $redis.smembers(follower_key(my_id)) 43 | end 44 | 45 | def count_follower(my_id) 46 | return $redis.scard(follower_key(my_id)) 47 | end 48 | 49 | 50 | # 谓词 51 | 52 | def is_following?(my_id, target_id) 53 | return $redis.sismember(following_key(my_id), target_id) 54 | end 55 | 56 | def have_follower?(my_id, target_id) 57 | return is_following?(target_id, my_id) 58 | end 59 | 60 | 61 | # 好友推荐 62 | 63 | def recommend(my_id, target_id) 64 | return $redis.sdiff(following_key(target_id), following_key(my_id)) 65 | end 66 | 67 | 68 | # 辅助函数 69 | 70 | def following_key(id) 71 | return "#{id}::following" 72 | end 73 | 74 | def follower_key(id) 75 | return "#{id}::follower" 76 | end 77 | -------------------------------------------------------------------------------- /code/usage/semaphore/semaphore.rb: -------------------------------------------------------------------------------- 1 | require "redis" 2 | 3 | $redis = Redis.new 4 | 5 | # 设置信号量的名字和数量 6 | def init(name, size) 7 | item = [] << name 8 | all_item = item * size 9 | $redis.lpush(name, all_item) 10 | end 11 | 12 | # 获取一个信号量,成功返回一个非空元素,失败返回一个 nil 。 13 | # 如果暂时没有信号量可用,则阻塞直到有其他客户端释放信号量,或者超过 timeout 为止 14 | def acquire(name, timeout=0) 15 | $redis.blpop(name, timeout) 16 | end 17 | 18 | # 释放一个信号量 19 | # 客户端程序应该保证,只有获取了信号量的客户端可以调用这个函数 20 | def release(name) 21 | $redis.lpush(name, name) 22 | end 23 | -------------------------------------------------------------------------------- /convention.md: -------------------------------------------------------------------------------- 1 | # 翻译约定 2 | 3 | 4 | ## Redis 的各部分命令 5 | 6 | * string 字符串 7 | * hash 哈希 8 | * list 列表 9 | * set 集合 10 | * sorted set 有序集合 11 | * pub/sub 发布/订阅 12 | * transaction 事务 13 | * scripting 脚本 14 | * connection 连接 15 | * server 服务器 16 | * save 快照 17 | * aof aof 18 | 19 | 20 | ## 相关术语 21 | 22 | * replication 复制 23 | * master 主库 24 | * slave 备库 25 | * event driven 事件驱动 26 | * cluster 集群 27 | * aof aof 28 | * key value 键-值 29 | * expire 过期 30 | * expire time 过期时间 31 | * fork 派生 32 | * instance 实例 33 | 34 | 35 | ## Redis 内部数据结构 36 | 37 | * sds(simple dynamic string) 动态字符串 38 | * dict 字典 39 | * hash table 哈希表 40 | * double-linked list 双链表 41 | * skip list 跳跃表 42 | * sentinel sentinel 43 | * intset intset 44 | * ziplist ziplist 45 | * zipmap zipmap 46 | 47 | -------------------------------------------------------------------------------- /image/RedisBook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/RedisBook.png -------------------------------------------------------------------------------- /image/memcached_slab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/memcached_slab.png -------------------------------------------------------------------------------- /image/memcached_slabs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/memcached_slabs.png -------------------------------------------------------------------------------- /image/redis-aof-backgroud-thread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis-aof-backgroud-thread.png -------------------------------------------------------------------------------- /image/redis.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis.gif -------------------------------------------------------------------------------- /image/redis_adlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_adlist.png -------------------------------------------------------------------------------- /image/redis_ae.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_ae.png -------------------------------------------------------------------------------- /image/redis_aof.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_aof.png -------------------------------------------------------------------------------- /image/redis_arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_arch.png -------------------------------------------------------------------------------- /image/redis_client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_client.png -------------------------------------------------------------------------------- /image/redis_commands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_commands.png -------------------------------------------------------------------------------- /image/redis_db_data_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_db_data_structure.png -------------------------------------------------------------------------------- /image/redis_dict.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_dict.png -------------------------------------------------------------------------------- /image/redis_dict_add_element.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_dict_add_element.png -------------------------------------------------------------------------------- /image/redis_dict_create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_dict_create.png -------------------------------------------------------------------------------- /image/redis_dict_datastruct_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_dict_datastruct_overview.png -------------------------------------------------------------------------------- /image/redis_dict_incremental_rehashing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_dict_incremental_rehashing.png -------------------------------------------------------------------------------- /image/redis_multi_command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_multi_command.png -------------------------------------------------------------------------------- /image/redis_network_arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_network_arch.png -------------------------------------------------------------------------------- /image/redis_network_event_arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_network_event_arch.png -------------------------------------------------------------------------------- /image/redis_protocol_command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_protocol_command.png -------------------------------------------------------------------------------- /image/redis_replication.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_replication.png -------------------------------------------------------------------------------- /image/redis_replication_interactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_replication_interactive.png -------------------------------------------------------------------------------- /image/redis_replication_state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_replication_state.png -------------------------------------------------------------------------------- /image/redis_sdshdr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_sdshdr.png -------------------------------------------------------------------------------- /image/redis_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_server.png -------------------------------------------------------------------------------- /image/redis_server_data_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_server_data_structure.png -------------------------------------------------------------------------------- /image/redis_slave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/redis_slave.png -------------------------------------------------------------------------------- /image/string.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/string.jpg -------------------------------------------------------------------------------- /image/usage/twitter_recommend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/usage/twitter_recommend.png -------------------------------------------------------------------------------- /image/usage/twitter_relation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/usage/twitter_relation.png -------------------------------------------------------------------------------- /image/usage/vote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/usage/vote.png -------------------------------------------------------------------------------- /image/zipmap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/image/zipmap.jpg -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- 1 | #目录 2 | 3 | 1. Redis介绍 4 | * 特点,优点,缺点(slave没有tag, ha方案不好) 5 | * 同类项目的横向比较 6 | * 趋势 7 | * Redis 源码概览 8 | 9 | 2. 客户端服务端安装、配置 10 | * 配置文件参数的说明 11 | * 参数的优化 12 | * 客户端的使用 13 | * pyRedis 14 | * hiRedis 15 | * jRedis 16 | * erldis 17 | 18 | 3. 基本命令使用 19 | * string 20 | * hash 21 | * list 22 | * set 23 | * zset 24 | * 事务 25 | * 管理 26 | 27 | 4. 案例、应用实践及相关开源项目 28 | * 缓存 29 | * 计数器 30 | * 好友关系 31 | * 消息队列 32 | * 自动补全 33 | * 物品推荐 34 | * 日志 35 | * mapreduce 36 | * 排行榜 37 | * 标签(索引) 38 | 39 | 5. Redis 源码分析,part1(数据结构) 40 | * string 41 | * hash 42 | * zipmap 43 | * zset 44 | * list 45 | 46 | 6. Redis 源码分析,part2(工作原理) 47 | * 哈希 48 | * 事件分离器 49 | * network 50 | * rdb 51 | * aof 52 | * 复制 53 | 54 | 7. Redis 和 memcahced 的差异 55 | * memcached的源码分析 56 | * 网络 57 | * 内存分配 58 | * lru 59 | * jemalloc 60 | * 两者内存使用的差异 61 | 62 | 8. Redis模块的复用 63 | * ae的使用 64 | * list的使用 65 | * hash的使用 66 | * linenose的使用 67 | * sds的使用 68 | 69 | 9. 70 | * timing-wheel 71 | 72 | 10. hiRedis源码分析 73 | * 架构 74 | 75 | 11. script 76 | 77 | 12. 高可用 78 | * rename 79 | * sentinal 80 | * cluster 81 | 82 | 13. 高级用法 83 | * pipeline 84 | * 85 | -------------------------------------------------------------------------------- /redis-adlist-implement.md: -------------------------------------------------------------------------------- 1 | 本文内容 2 | ------------- 3 | 4 | 链表是 Redis 的核心数据结构之一,它不仅大量应用在 Redis 自身内部的实现中,而且它也是 Redis 的 List 结构的底层实现之一。 5 | 6 | 本文通过分析 Redis 源码里的 ``adlist.h`` 和 ``adlist.c`` ,了解链表结构的详细实现,籍此加深对 Redis 的理解。 7 | 8 | 9 | 数据结构 10 | --------------- 11 | 12 | Redis 的链表结构是一个典型的双端链表([doubly linked list](http://en.wikipedia.org/wiki/Doubly_linked_list))实现。 13 | 14 | 除了一个指向值的 ``void`` 指针外,链表中的每个节点都有两个方向指针,一个指向前驱节点,另一个指向后继节点: 15 | 16 | typedef struct listNode { 17 | struct listNode *prev; 18 | struct listNode *next; 19 | void *value; 20 | } listNode; 21 | 22 | 每个双端链表都被一个 ``list`` 结构包装起来, ``list`` 结构带有两个指针,一个指向双端链表的表头节点,另一个指向双端链表的表尾节点,这个特性使得 Redis 可以很方便地执行像 [RPOPLPUSH](http://redis.readthedocs.org/en/latest/list/rpoplpush.html) 这样的命令: 23 | 24 | typedef struct list { 25 | listNode *head; 26 | listNode *tail; 27 | 28 | void *(*dup)(void *ptr); 29 | void (*free)(void *ptr); 30 | int (*match)(void *ptr, void *key); 31 | 32 | unsigned long len; 33 | } list; 34 | 35 | 链表结构中还有三个函数指针 ``dup`` 、 ``free`` 和 ``match`` ,这些指针指向那些用于处理不同类型值的函数。 36 | 37 | 至于 ``len`` 属性,毫无疑问,就是链表节点数量计数器了。 38 | 39 | 以下是双端链表和节点的一个示意图: 40 | 41 | ![双端链表和节点示意图](https://raw.github.com/redisbook/book/master/image/redis_adlist.png) 42 | 43 | 44 | list 结构和 listNode 结构的 API 45 | -------------------------------------- 46 | 47 | ``list`` 和 ``listNode`` 都有它们自己的一簇 API ,这些 API 的实现都是典型的双端链表 API ,这里就不作详细的分析了。 48 | 49 | 从名字上就可以大概地看出它们的作用: 50 | 51 | list *listCreate(void); 52 | void listRelease(list *list); 53 | 54 | list *listAddNodeHead(list *list, void *value); 55 | list *listAddNodeTail(list *list, void *value); 56 | list *listInsertNode(list *list, listNode *old_node, void *value, int after); 57 | void listDelNode(list *list, listNode *node); 58 | 59 | list *listDup(list *orig); 60 | 61 | listNode *listSearchKey(list *list, void *key); 62 | listNode *listIndex(list *list, long index); 63 | 64 | void listRotate(list *list); 65 | 66 | 为了方便操作列表,源码中还定义了一组宏: 67 | 68 | #define listLength(l) ((l)->len) 69 | #define listFirst(l) ((l)->head) 70 | #define listLast(l) ((l)->tail) 71 | #define listPrevNode(n) ((n)->prev) 72 | #define listNextNode(n) ((n)->next) 73 | #define listNodeValue(n) ((n)->value) 74 | 75 | #define listSetDupMethod(l,m) ((l)->dup = (m)) 76 | #define listSetFreeMethod(l,m) ((l)->free = (m)) 77 | #define listSetMatchMethod(l,m) ((l)->match = (m)) 78 | 79 | #define listGetDupMethod(l) ((l)->dup) 80 | #define listGetFree(l) ((l)->free) 81 | #define listGetMatchMethod(l) ((l)->match) 82 | 83 | 84 | 迭代器 85 | ----------- 86 | 87 | Redis 针对 ``list`` 结构实现了一个[迭代器](http://en.wikipedia.org/wiki/Iterator),用于对链表进行遍历。 88 | 89 | 这个迭代器的实现非常典型,它的结构定义如下: 90 | 91 | typedef struct listIter { 92 | listNode *next; 93 | int direction; // 指定迭代的方向(从前到后还是从后到前) 94 | } listIter; 95 | 96 | ``direction`` 决定迭代器是沿着 ``next`` 指针向后迭代,还是沿着 ``prev`` 指针向前迭代,这个值可以是 ``adlist.h`` 中的 ``AL_START_HEAD`` 常量或 ``AL_START_TAIL`` 常量: 97 | 98 | #define AL_START_HEAD 0 99 | #define AL_START_TAIL 1 100 | 101 | 以下是迭代器所使用的 API : 102 | 103 | listIter *listGetIterator(list *list, int direction); 104 | listNode *listNext(listIter *iter); 105 | 106 | void listReleaseIterator(listIter *iter); 107 | 108 | void listRewind(list *list, listIter *li); 109 | void listRewindTail(list *list, listIter *li); 110 | 111 | 112 | 小结 113 | ----- 114 | 115 | 和以往不同,因为双端链表和链表迭代器都非常常见,所以这篇文章没有像往常一样,对实现源码作详细的分析,而是将注意力集中到数据结构的定义,以及 API 的展示上。 116 | 117 | 如果对源码的细节感兴趣,可以到 GITHUB 上查看带注释的完整源码: [https://github.com/huangz1990/reading_redis_source](https://github.com/huangz1990/reading_redis_source) 。 118 | -------------------------------------------------------------------------------- /redis-ae.md: -------------------------------------------------------------------------------- 1 | 2 | #多路复用的事件分粒器 3 | 4 | 5 | ``reactor`` 6 | 7 | 8 | ##时间事件 9 | 10 | 11 | ##文件事件 12 | 13 | 14 | Redis 处理的比较巧妙。先执行 aeSearchNearestTimer 确定距离下次时间事件执行还有多少时间,假设第一次执行直到下次时间事件还有100ms,先执行文件事件,epool_wait 的超时时间就设置为 100ms,如果 10ms后,有网络交互后经过一系列的处理后消耗 20ms,该次循环结束。aeSearchNearestTimer 会再次计算距离下次时间事件的间隔为 100 - 10 - 20 = 70ms,于是 epoll_wait 的超时时间为 70ms,70ms之内如果没有处理文件事件,则执行时间事件。这样即保证了即时处理文件事件,在文件事件处理完毕后又能按时处理时间事件。 15 | 16 | 17 | 18 | 19 | #其它的第三方库 20 | 21 | 22 | ##libevent 23 | 24 | 25 | ##libev 26 | 27 | -------------------------------------------------------------------------------- /redis-aof.md: -------------------------------------------------------------------------------- 1 | #AOF 2 | 3 | 4 | aof 原理有点类似 redo log。每次执行命令后如果数据发生了变化(server.dirty发生了变化),会接着调用 feedAppendOnlyFile。 5 | 6 | void call(RedisClient *c, struct RedisCommand *cmd) { 7 | long long dirty; 8 | dirty = server.dirty; 9 | cmd->proc(c); //执行命令 10 | dirty = server.dirty-dirty; 11 | if (server.appendonly && dirty) 12 | feedAppendOnlyFile(cmd,c->db->id,c->argv,c->argc); 13 | 14 | feedAppendOnlyFile并非把直接存入 aof 文件,而是先把命令存储到 server.aofbuf 里。 15 | 16 | void feedAppendOnlyFile(struct RedisCommand *cmd, int dictid, robj **argv, int argc) { 17 | 18 | buf = catAppendOnlyGenericCommand(buf,argc,argv); 19 | server.aofbuf = sdscatlen(server.aofbuf,buf,sdslen(buf)); 20 | .... 21 | if (server.bgrewritechildpid != -1) 22 | server.bgrewritebuf = sdscatlen(server.bgrewritebuf,buf,sdslen(buf)); 23 | } 24 | 25 | 在这个过程中,如果存在 bgrewritechild 进程,变化数据还会写到 server.bgrewritebuf 里。 26 | 27 | 待到接下来的循环的 before_sleep 函数会通过 flushAppendOnlyFile 函数把 server.aofbuf 里的数据 write 到 append file 里。 28 | 29 | void flushAppendOnlyFile(void) { 30 | .... 31 | nwritten = write(server.appendfd,server.aofbuf,sdslen(server.aofbuf)); 32 | 33 | redis.conf里配置每次 write 到 append file 后,fsync的规则,fsync的作用大家都知道,把从page cache刷新到disk。 34 | 35 | #appendfsync always 36 | appendfsync everysec 37 | #appendfsync no 38 | 39 | 该参数的原理 MySQL 的 innodb_flush_log_at_trx_commit 一样,是个较影响io的一个参数,需要在高性能和不丢数据之间做 trade-off。软件的优化就是 trade-off的过程,没有银弹,默认选用的是 everysec,每次 fsync 会记录时间,距离上次 fsync 超过1s,则会再次触发fsync。 40 | 41 | /* Fsync if needed */ 42 | now = time(NULL); 43 | if (server.appendfsync == APPENDFSYNC_ALWAYS || 44 | (server.appendfsync == APPENDFSYNC_EVERYSEC && 45 | now-server.lastfsync > 1)) 46 | { 47 | /* aof_fsync is defined as fdatasync() for Linux in order to avoid 48 | * flushing metadata. */ 49 | aof_fsync(server.appendfd); /* Let's try to get this data on the disk */ 50 | server.lastfsync = now; 51 | } 52 | 53 | Redis.conf里的no-appendfsync-on-rewrite参数的意义是,如果在做rdb或者bgrewrite过程中,不会对aof文件进行fsync,这样对磁盘的写入操作不会因为要写 rdb 和 aof 两个文件而快速的摆动磁头,减少了寻道时间,让rdb、bgrewrite可以快速的完成,但这样同时增加了风险,因为生成rdb的时间还是比较长的。如果在这过程中os crash,部分aof数据还在page cache里,但还未写入到disk上。 54 | 55 | if (server.no_appendfsync_on_rewrite && 56 | (server.bgrewritechildpid != -1 || server.bgsavechildpid != -1)) 57 | return; //跳出这个函数,不再进行fsync 58 | 59 | 另外为什么Redis采用这种模式,每次写完内存,再写到server.aofbuf,而不是直接写到aof文件内,这是一个很多很大的性能优化,因为一次循环可能接收多次网络请求,所以的变化都合并到aofbuf里,然后再写入文件里,把多次的小io,转化成一次连续的大io,这也是常规的数据库优化方法。 60 | 61 | 那么既然先写到server.aofbuf,写入aof文件之前,Redis crash会不会丢数据呢?答案是不会,为何?我先看看网络事件库如何处理读写事件 62 | 63 | if (fe->mask & mask & AE_READABLE) { 64 | rfired = 1; 65 | fe->rfileProc(eventLoop,fd,fe->clientData,mask); 66 | } 67 | if (fe->mask & mask & AE_WRITABLE) { 68 | if(!rfired||fe->wfileProc!=fe->rfileProc) 69 | fe->wfileProc(eventLoop,fd,fe->clientData,mask); 70 | } 71 | 72 | rfired变量决定了在同一次文件事件循环内,如果对于某个fd触发了可读事件的函数,不会再次触发写事件。我们来看函数执行的简化步骤: 73 | * readQueryFromClient() 74 | * call() 75 | * feedAppendOnlyFile() 76 | * 因为rfired原因退出本次循环 下一次循环 77 | * beforeSleep()-->flushAppendOnlyFile() 78 | * aeMain()--->sendReplyToClient() 79 | 80 | 只有执行完了flush之后才会通知客户端数据写成功了,所以如果在feed和flush之间crash,客户接会因为进程退出接受到一个fin包,也就是一个错误的返回,所以数据并没有丢,只是执行出了错。 81 | 82 | Redis crash后,重启除了利用 rdb 重新载入数据外,还会读取append file(Redis.c 1561)加载镜像之后的数据。 83 | 84 | 85 | ##如何激活aof 86 | 87 | 88 | 激活aof,可以在Redis.conf配置文件里设置 89 | 90 | appendonly yes 91 | 92 | 也可以通过config命令在运行态启动aof 93 | 94 | cofig set appendonly yes 95 | 96 | 每次激活 aof ,调用函数 startAppendOnly(aof.c)必然的做执行一次 bgrewriteaof ,生成一个 aof 文件,并强制刷 fsync。这样做保证了aof文件在任何时候数据都是完整的。 97 | 98 | int startAppendOnly(void) { 99 | .... 100 | if (rewriteAppendOnlyFileBackground() == REDIS_ERR) { 101 | .... 102 | 103 | 一旦开启 aof,则 Redis 重启后只会读取 aof 文件(Redis.c),而无视rdb文件的存在。 104 | 105 | Redis关闭(Redis.c)之时也会强制的刷一次fsync。 106 | 107 | int prepareForShutdown() { 108 | .... 109 | if (server.appendonly) { 110 | /* Append only file: fsync() the AOF and exit */ 111 | aof_fsync(server.appendfd); 112 | .... 113 | 114 | ##bgrewriteaof 115 | 116 | 117 | aof 的一个问题就是随着时间 append file 会变的很大,比如一个做 incr 的 key,aof 文件里记录的都是从1到N的自增的过程,其实我们只要保存最后的值即可。 118 | 119 | 所以我们需要 bgrewriteaof 命令重新整理文件,只保留最新的key-value数据,会调用 rewriteAppendOnlyFile 这个函数,该函数与 rdbSave 工作原理类似。保存全库的kv数据,但aof数据未压缩,而且是明文存储。 120 | 121 | int rewriteAppendOnlyFile(char *filename) { 122 | snprintf(tmpfile,256,"temp-rewriteaof-%d.aof", (int) getpid()); 123 | fp = fopen(tmpfile,"w"); 124 | for (j = 0; j < server.dbnum; j++) { 125 | if (o->type == REDIS_STRING) { 126 | //set 127 | } else if (o->type == REDIS_LIST) { 128 | //rpush 129 | } else if (o->type == REDIS_SET) { 130 | //sadd 131 | } else if (o->type == REDIS_ZSET) { 132 | //zadd 133 | } else if (o->type == REDIS_HASH) { 134 | //hset 135 | } 136 | 137 | 等 bgrewritecihld 进程完成快照退出之时(Redis.c),再调用 backgroundRewriteDoneHandler 函数 138 | 139 | if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) { 140 | if (pid == server.bgsavechildpid) { 141 | backgroundSaveDoneHandler(statloc); 142 | } else { 143 | backgroundRewriteDoneHandler(statloc); 144 | } 145 | 146 | backgroundRewriteDoneHandler 处理 bgrewriteaof 生成的临时文件,合并 bgrewritebuf 和临时文件两部分数据后,就生成了新的 aof 文件,并做一次强制的 fsync。 147 | 148 | void backgroundRewriteDoneHandler(int statloc) { 149 | .... 150 | fd = open(tmpfile,O_WRONLY|O_APPEND); 151 | .... 152 | write(fd,server.bgrewritebuf,sdslen(server.bgrewritebuf) 153 | .... 154 | rename(tmpfile,server.appendfilename) 155 | .... 156 | if (server.appendfsync != APPENDFSYNC_NO) aof_fsync(fd) 157 | ... 158 | 159 | 合并 bgrewritebuf 很重要,否则最终的 aof 文件里的数据和内存里的数据就不一致了。 160 | 161 | ##aof文件格式 162 | 163 | 164 | 我们来看看aof文件的格式,知道aof的格式可以很方便解析他,可以自己实现更加异步的(Redis的复制已经是异步模式了)的复制技术。aof文件是ascii格式的,意味着可以明文的读取,而rdb文件可能是经过压缩的,所以即便aof文件做过 bgrewriteaof,aof 文件也是远大于rdb 文件。 165 | 166 | *参数的个数\r\n 167 | $参数1的长度\r\n 168 | 参数1\r\n 169 | … 170 | $参数N的长度\r\n 171 | 参数N\r\n 172 | 173 | 例如一个 "set a 1" 的命令放到aof文件里的格式就是这样的。 174 | 175 | *3^M 176 | $3^M 177 | set^M 178 | $1^M 179 | a^M 180 | $1^M 181 | 1^M 182 | 183 | 执行命令前后,server.dirty 发生变化的命令,才会存储到 aof 文件里。 184 | 185 | 如果出现事务,多个命令会在exec后出现在aof文件里,例如一个mulit,set a 1, set b 2,exec命令之后的文件格式如下。 186 | 187 | *1^M 188 | $5^M 189 | MULTI^M 190 | *3^M 191 | $3^M 192 | set^M 193 | $1^M 194 | a^M 195 | $1^M 196 | 1^M 197 | *3^M 198 | $3^M 199 | set^M 200 | $1^M 201 | b^M 202 | $1^M 203 | 2^M 204 | *1^M 205 | $4^M 206 | exec^M 207 | 208 | Redis-check-aof 这个 binary 可以用来检测 aof 文件的合法性。原理简单,先读取×后的数字,确定参数格式,再读取$后参数的长度,再读取参数。对于事务需要额外的处理,出现multi的地方必须要出现exec。 209 | 210 | 当Redis出现crash,Redis重启的时候(Redis.c),如果激活了aof,则会查找aof文件,并载入这个aof文件。 211 | 212 | if (server.appendonly) { 213 | if (loadAppendOnlyFile(server.appendfilename) == REDIS_OK) 214 | RedisLog(REDIS_NOTICE,"DB loaded from append only file: %ld seconds",time(NULL)-start); 215 | } else { 216 | if (rdbLoad(server.dbfilename) == REDIS_OK) 217 | RedisLog(REDIS_NOTICE,"DB loaded from disk: %ld seconds",time(NULL)-start); 218 | } 219 | 220 | 载入 aof 文件的方法很有意思,先是创建一个 fake client ,这个 client 并非通过网络连接上来的客户端,而是伪造的一个对象,把 aof 文件里的命令一一保存在 client 的 arc,argv 里,使得这 client 像是某个网络连接连接上来,发送消息给服务端一样,这样处理 aof 里命令的方法就可以重用,而不需要额外的编写程序了。 221 | 222 | int loadAppendOnlyFile(char *filename) { 223 | .... 224 | fakeClient = createFakeClient(); 225 | while(1) { 226 | fgets(buf,sizeof(buf),fp) 227 | argc = atoi(buf+1); //命令参数格式 228 | argv = zmalloc(sizeof(robj*)*argc); 229 | for (j = 0; j < argc; j++) { 230 | .... 231 | } 232 | cmd = lookupCommand(argv[0]->ptr); 233 | fakeClient->argc = argc; 234 | fakeClient->argv = argv; 235 | cmd->proc(fakeClient); //模拟客户端执行命令 236 | .... 237 | } 238 | freeFakeClient(fakeClient); 239 | ... 240 | } 241 | 242 | -------------------------------------------------------------------------------- /redis-backgroud-thread.md: -------------------------------------------------------------------------------- 1 | #backgroud thread 2 | 3 | 4 | ##为何有新的线程? 5 | 6 | 7 | Redis 终于在 2.4 版本里引入了除主线程之外的后台线程,这个事情由来已久。早在 2010年2月 就有人提出aof的缺陷,提及的问题主要有: 8 | 9 | * 主线程 aof 的每次 fsync(everysecond模式) 在高并发下时常出现 100ms 的延时,这源于 fsync 必不可少的磁盘操作,即便已经优化多次请求的离散小io转化成一次大的连续io(sina的同学也反映过这个问题)。 10 | 11 | * 主线程里 backgroundRewriteDoneHandler 函数在处理 bgrewriteaof 后台进程退出的时候存在一个 rename new-aof-file old-aof-file,然后再 close old-aof-file 的操作, close 是一个 unlink 的操作(最后的引用计数), unlink 消耗的时间取决于文件的大小,是个容易阻塞的系统调用。 12 | 13 | * 当发生 bgsave 或者 bgrewriteaof 的时候主线程和子进程同时写入不同的文件,这改变了原有连续写模式,不同写入点造成了磁盘磁头的寻道时间加长(其实一个台物理机多实例也有这个问题, 要避免同一时间点做bgrewriteaof), 这又加长了fsync时间。 14 | 15 | 经过漫长的设计和交流,antirez 终于在 2.4 版里给出了实现, 这个设计保持了Redis原有的keep it simple的风格,实现的特别简单且有效果,实现的主要原理就是把 fsync 和 close 操作都移动到 background 来执行。 16 | 17 | 18 | ##实现 19 | 20 | 21 | 2.4.1 版本引入新的文件 bio.c,这个文件包含了后台线程的业务逻辑,如图。 22 | 23 | ![backgroud thread](https://raw.github.om/redisbook/book/master/image/redis-aof-backgroud-thread.png) 24 | 25 | bioInit 在 Redis 启动的时候被调用,默认启动 2 个后台线程(如图中的 thread1, thread2),其一负责 fsync fd 的任务(解决缺陷1),其二负责 close fd 的任务(解决缺陷2)。 26 | 27 | 这两个线程条件等待各自独立的2个链表(close job,fsync job)上,看是否有新任务的加入,有则进行 fsync 或者 close。 28 | 29 | ##解决问题1 30 | 31 | 32 | 主线程仅仅把 aofbuf 的数据刷新到 aof 文件里,然后通过 bioCreateBackgroundJob 函数往这队列里插入 fsync job,于是原有主线程的 fsync 工作被转移到后台线程来做,这样主线程阻塞问题就异步的解决了。 33 | 34 | 但这又引发了一个问题,主线程对同一个 fd 如果有 write 操作,后台线程同时在 fsync ,这两个线程会互相影响, antirez为此做了一定研究,并给出了简单的解决方案。 35 | 36 | 为了避免线程的互相影响,主线程每次 write 之前都要检测一下后台线程任务队列里是否有 fsync 操作,如果有则延迟这次 aofbuf 的 flush,延迟 flush 这个功能,当然会增大丢数据的可能,我们来看看实现。 37 | 38 | aof.c 39 | ======= 40 | 78 void flushAppendOnlyFile(int force) { 41 | ..... 42 | 84 if (server.appendfsync == APPENDFSYNC_EVERYSEC) 43 | 85 sync_in_progress = bioPendingJobsOfType(REDIS_BIO_AOF_FSYNC) != 0; 44 | 86 45 | 87 if (server.appendfsync == APPENDFSYNC_EVERYSEC && !force) { 46 | 88 /* With this append fsync policy we do background fsyncing. 47 | 89 * If the fsync is still in progress we can try to delay 48 | 90 * the write for a couple of seconds.*/ 49 | 91 if (sync_in_progress) { 50 | 92 if (server.aof_flush_postponed_start == 0) { 51 | 93 /* No previous write postponinig, remember that we are 52 | 94 * postponing the flush and return. */ 53 | 95 server.aof_flush_postponed_start = server.unixtime; 54 | 96 return; 55 | 97 } else if (server.unixtime - server.aof_flush_postponed_start < 2) { 56 | 98 /* We were already waiting for fsync to finish, but for less 57 | 99 * than two seconds this is still ok. Postpone again. */ 58 | 100 return; 59 | 101 } 60 | 102 /* Otherwise fall trough, and go write since we can't wait 61 | 103 * over two seconds. */ 62 | 104 RedisLog(REDIS_NOTICE,"Asynchronous AOF fsync is taking too long (disk is busy?)Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis."); 63 | 105 } 64 | 106 } 65 | 66 | 我们来解读一下这段代码, force这个参数如果为1,则为强制flush,为0否则允许延迟flush。 67 | 68 | * 85行:这段就是判断后台线程是否有 fsync 任务,如果存在则会出现主线程 write ,后台线程 fsync 的并发行为。 sync_in_process就表示存在冲突的可能性,则开始延迟flush。 69 | 70 | * 92行:如果当前未发生延迟,现在开始延迟 flush ,记录一下时间就立即返回,这就发生了延迟 flush, aofbuf里的信息未被刷出去。 71 | * 97行:当再次进入该函数之后,如果距离开始延迟时间仍然小于 2s,则允许继续延迟。 72 | * 104行:距离开始延迟事件已经超过 2s 了,必须强制 flush 了,否则丢数据可能超过 2s。 73 | 74 | 解决了冲突之后就是加入后台任务了,以前是fsync现在改成了加入队列 75 | 76 | aof.c 77 | ======== 78 | 151 } else if ((server.appendfsync == APPENDFSYNC_EVERYSEC && 79 | 152 server.unixtime > server.lastfsync)) { 80 | 153 if (!sync_in_progress) aof_background_fsync(server.appendfd); 81 | 154 server.lastfsync = server.unixtime; 82 | 155 } 83 | 84 | 好了缺陷1解决了。 85 | 86 | ##解决缺陷2 87 | 88 | 89 | backgroundRewriteDoneHandler 里同样的把 close old-aof-file 的工作交给 backgroud thread 来执行。 90 | 91 | aof.c 92 | ========= 93 | 856 /* Asynchronously close the overwritten AOF. */ 94 | 857 if (oldfd != -1) bioCreateBackgroundJob(REDIS_BIO_CLOSE_FILE,(void*)(long)oldfd, NULL, NULL); 95 | 96 | 这样关闭 old-aof-file 的工作被移交到后台任务执行,不再阻塞主线程了,不过没那么简单,如下的特殊场景需要额外处理。 97 | 98 | aof enabled 99 | bgrewriteaof start 100 | aof disbled 101 | bgrewriteaof stop 102 | bgrewriteaof handler 103 | 104 | 在 bgrewriteaof 触发之后,关闭了 aof 功能,这样由于 server.appendfd 对应 old-aof-file 文件未被打开, 一旦 rename new-aof old-aof, 则会触发一个 unlink old-aof-file 的行为, 而不是上面说的close才触发unlink行为。为了跳过这种状况,如果发现aof被关闭,通过打开old-aof-file文件增加引用计数的方法解决这个问题。 105 | 106 | aof.c 107 | ========== 108 | 810 if (server.appendfd == -1) { 109 | 811 /* AOF disabled */ 110 | 812 111 | 813 /* Don't care if this fails: oldfd will be -1 and we handle that. 112 | 814 * One notable case of -1 return is if the old file does 113 | 815 * not exist. */ 114 | 816 oldfd = open(server.appendfilename, O_RDONLY|O_NONBLOCK); 115 | 817 } else { 116 | 818 /* AOF enabled */ 117 | 819 oldfd = -1; /* We'll set this to the current AOF filedes later. */ 118 | 820 } 119 | 120 | 121 | * 816行:如果处于 aof 关闭状态,则打开 old-aof-file。 122 | * 819行:aof 已经是激活状态,不做任何操作。 123 | 124 | 这样 rename 就不再引发 unlink old-aof-file, 不会再阻塞主线程。 125 | 126 | 824 if (rename(tmpfile,server.appendfilename) == -1) { 127 | 128 | 处理完 rename 之后就要来处理 old-aof-file 了。如果aof是非激活状态,对于 new-aof-file 文件,我们关闭他即可不需要其它操作,这个 close 不会引发阻塞,因为这个文件的已经在生成 new-aof-file 文件的时候做过 fsync了。 129 | 130 | 如果 aof 是激活状态,fsync 行为递给后台去执行,这块的行为和缺陷1一样。 131 | 132 | aof.c 133 | =========== 134 | 840 if (server.appendfsync == APPENDFSYNC_ALWAYS) 135 | 841 aof_fsync(newfd); 136 | 842 else if (server.appendfsync == APPENDFSYNC_EVERYSEC) 137 | 843 aof_background_fsync(newfd); 138 | 139 | ##解决缺陷3 140 | 141 | 引入了延迟 bgrewriteaof 来避免与 bgsave 同时写文件,而 server.no_appendfsync_on_rewrite 参数的设置又避免了 bgrewriteaof 时主线程出现 fsync。 142 | 143 | 测试2.4.1的性能确实较之前版有较大的提升,以后会给出测试数据。 144 | 145 | -------------------------------------------------------------------------------- /redis-dict-implement.md: -------------------------------------------------------------------------------- 1 | 简介 2 | ----- 3 | 4 | 字典是 Redis 的核心数据结构之一,在 Redis 中,每个数据库本身也是一个字典,而且字典也是 Redis 的 Hash 类型的底层实现。 5 | 6 | 本文通过分析 Redis 源码里的 ``dict.h`` 和 ``dict.c`` 文件,了解字典结构的详细实现,籍此加深对 Redis 的理解。 7 | 8 | 由于字典(哈希表)是一种非常常见的数据结构,而 ``dict.c`` 中使用的 [separate chaining 哈希表实现](http://en.wikipedia.org/wiki/Hash_table#Separate_chaining)可以在任何一本算法书上找到,因此,在本文中没有对字典的查找和增删等操作做过多的着墨,而是将重点放到整个字典结构的运作流程,以及哈希表的渐增式 rehash 操作上。 9 | 10 | 11 | 字典实现的数据结构 12 | ---------------------- 13 | 14 | ``dict.h`` 文件里定义了字典实现的数据结构,比如 ``dict`` 、 ``dictht`` 和 ``dictEntry`` 等,它们之间的关系可以用下图来描述: 15 | 16 | ![字典的各个数据结构之间的关系](https://raw.github.com/redisbook/book/master/image/redis_dict_datastruct_overview.png) 17 | 18 | 其中, ``dict`` 结构的定义如下: 19 | 20 | typedef struct dict { 21 | dictType *type; // 为哈希表中不同类型的值所使用的一族操作函数 22 | void *privdata; 23 | dictht ht[2]; // 每个字典使用两个哈希表(用于渐增式 rehash) 24 | int rehashidx; // 指示 rehash 是否正在进行,如果不是则为 -1 25 | int iterators; // 当前正在使用的 iterator 的数量 26 | } dict; 27 | 28 | 代码中的注释基本说明相关属性的作用了,需要补充的一些是: 29 | 30 | 为了实现渐增式 rehash ,每个字典使用两个哈希表,分别为 ``ht[0]`` 和 ``ht[1]`` 。当 rehash 开始进行的时候, Redis 会逐个逐个地将 ``ht[0]`` 哈希表中的元素移动到 ``ht[1]`` 哈希表,直到 ``ht[0]`` 哈希表被清空为止。文章后面会给出 rehash 的相关细节。 31 | 32 | 另一方面, ``rehashidx`` 则是 rehash 操作的计数器,这方面的相关细节也会后面给出。 33 | 34 | 接下来是哈希表结构 ``dictht`` ,这个哈希表是一个典型的 separate chaining hash table 实现,它通过将哈希值相同的元素放到同一个链表中来解决冲突问题: 35 | 36 | typedef struct dictht { 37 | dictEntry **table; // 节点指针数组 38 | unsigned long size; // 桶的大小(最多可容纳多少节点) 39 | unsigned long sizemask; // mask 码,用于地址索引计算 40 | unsigned long used; // 已有节点数量 41 | } dictht; 42 | 43 | 最后要介绍的是链表的节点结构 ``dictEntry`` : 44 | 45 | typedef struct dictEntry { 46 | void *key; // 键 47 | union { 48 | void *val; 49 | uint64_t u64; 50 | int64_t s64; 51 | } v; // 值(可以有几种不同类型) 52 | struct dictEntry *next; // 指向下一个哈希节点(形成链表) 53 | } dictEntry; 54 | 55 | ``dictEntry`` 中的 ``key`` 属性保存字典的键,而 ``v`` 属性则保存字典的值, ``next`` 保存一个指向 ``dictEntry`` 自身的指针,用于构成链表,解决哈希值的冲突问题。 56 | 57 | 58 | 创建字典 59 | ------------- 60 | 61 | 在初步了解了字典实现所使用的结构之后,现在是时候来看看相关的函数是怎样来操作这些结构的了。让我们从创建字典开始,一步步研究字典以及哈希表的运作流程。 62 | 63 | 使用字典的第一步就是创建字典,创建新字典执行这样一个调用链: 64 | 65 | ![首次创建字典时执行的调用序列](https://raw.github.com/redisbook/book/master/image/redis_dict_create.png) 66 | 67 | ``dictCreate`` 函数创建一个新的 ``dict`` 结构,然后将它传给 ``_dictInit`` 函数: 68 | 69 | dict *dictCreate(dictType *type, void *privDataPtr) 70 | { 71 | dict *d = zmalloc(sizeof(*d)); 72 | 73 | _dictInit(d,type,privDataPtr); 74 | return d; 75 | } 76 | 77 | ``_dictInit`` 函数为 ``dict`` 结构的各个属性设置默认值,并调用 ``_dictReset`` 函数为两个哈希表进行初始化设置: 78 | 79 | int _dictInit(dict *d, dictType *type, void *privDataPtr) 80 | { 81 | _dictReset(&d->ht[0]); // 初始化字典内的两个哈希表 82 | _dictReset(&d->ht[1]); 83 | 84 | d->type = type; // 设置函数指针 85 | d->privdata = privDataPtr; 86 | d->rehashidx = -1; // -1 表示没有在进行 rehash 87 | d->iterators = 0; // 0 表示没有迭代器在进行迭代 88 | 89 | return DICT_OK; // 返回成功信号 90 | } 91 | 92 | ``_dictReset`` 函数为字典的几个属性值赋值,但并不为这两个哈希表的链表数组分配空间: 93 | 94 | static void _dictReset(dictht *ht) 95 | { 96 | ht->table = NULL; // 未分配空间 97 | ht->size = 0; 98 | ht->sizemask = 0; 99 | ht->used = 0; 100 | } 101 | 102 | 103 | 哈希表链表的创建流程 104 | ------------------------ 105 | 106 | 每个 ``dict`` 结构都使用两个哈希表,分别是 ``dict->h1[0]`` 和 ``dict->ht[1]`` ,为了称呼方便,从现在开始,我们将它们分别叫做 0 号哈希表和 1 号哈希表。 107 | 108 | 从上一节的介绍可以知道,创建一个新的字典并不为哈希表的链表数组分配内存,也即是 ``dict->ht[0]->table`` 和 ``dict->ht[1]->table`` 都被设为 ``NULL`` 。 109 | 110 | 只有当首次调用 ``dictAdd`` 向字典中加入元素的时候, 0 号哈希表的链表数组才会被创建, ``dictAdd`` 执行这样一个调用序列: 111 | 112 | 113 | ![首次添加元素到字典时执行以下调用序列](https://raw.github.com/redisbook/book/master/image/redis_dict_add_element.png) 114 | 115 | ``dictAddRaw`` 是向字典加入元素这一动作的底层实现,为了计算新加入元素的 ``index`` 值,它会调用 ``_dictKeyIndex`` : 116 | 117 | dictEntry *dictAddRaw(dict *d, void *key) 118 | { 119 | // 被省略的代码... 120 | 121 | // 计算 key 的 index 值 122 | // 如果 key 已经存在,_dictKeyIndex 返回 -1 123 | if ((index = _dictKeyIndex(d, key)) == -1) 124 | return NULL; 125 | 126 | // 被省略的代码... 127 | } 128 | 129 | ``_dictKeyIndex`` 会在计算 ``index`` 值之前,先调用 ``_dictExpandIfNeeded`` ,检查两个哈希表是否有足够的空间容纳新元素: 130 | 131 | static int _dictKeyIndex(dict *d, const void *key) 132 | { 133 | // 被省略的代码... 134 | 135 | /* Expand the hashtable if needed */ 136 | if (_dictExpandIfNeeded(d) == DICT_ERR) 137 | return -1; 138 | 139 | // 被省略的代码... 140 | } 141 | 142 | 进行到 ``_dictExpandIfNeeded`` 这一步,一些有趣的事情就开始发生了, ``_dictExpandIfNeeded`` 会检测到 0 号哈希表还没有分配任何空间,于是它调用 ``dictExpand`` ,传入 ``DICT_HT_INITIAL_SIZE`` 常量,作为哈希表链表数组的初始大小(在当前版本中, ``DICT_HT_INITIAL_SIZE`` 的默认值为 ``4`` ): 143 | 144 | static int _dictExpandIfNeeded(dict *d) 145 | { 146 | // 被省略的代码... 147 | 148 | /* If the hash table is empty expand it to the intial size. */ 149 | if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); 150 | 151 | // 被省略的代码... 152 | } 153 | 154 | ``dictExpand`` 会创建一个分配了链表数组的新哈希表,然后进行判断,决定是应该将新哈希表赋值给 0 号哈希表,还是 1 号哈希表: 155 | 156 | int dictExpand(dict *d, unsigned long size) 157 | { 158 | // 创建带链表数组的新哈希表 159 | dictht n; /* the new hash table */ 160 | unsigned long realsize = _dictNextPower(size); 161 | 162 | /* the size is invalid if it is smaller than the number of 163 | * elements already inside the hash table */ 164 | if (dictIsRehashing(d) || d->ht[0].used > size) 165 | return DICT_ERR; 166 | 167 | /* Allocate the new hash table and initialize all pointers to NULL */ 168 | n.size = realsize; 169 | n.sizemask = realsize-1; 170 | n.table = zcalloc(realsize*sizeof(dictEntry*)); 171 | n.used = 0; 172 | 173 | /* Is this the first initialization? If so it's not really a rehashing 174 | * we just set the first hash table so that it can accept keys. */ 175 | if (d->ht[0].table == NULL) { 176 | d->ht[0] = n; // 将新哈希表赋值给 0 号哈希表 177 | return DICT_OK; // 然后返回 178 | } 179 | 180 | // 被省略的代码 ... 181 | } 182 | 183 | 到了这一步, 0 号哈希表已经从无到有被创建出来了。 184 | 185 | 186 | 字典的扩展,以及 1 号哈希表的创建 187 | -------------------------------------- 188 | 189 | 在 0 号哈希表创建之后,字典就可以支持增加、删除和查找等操作了。 190 | 191 | 唯一的问题是,这个最初创建的 0 号哈希表非常小,它很快就会被添加进来的元素填满,这时候,字典的扩展(expand)机制就会被激活,它执行一系列动作,为字典分配更多空间,从而使得字典可以继续正常运作。 192 | 193 | 因为字典的的底层实现是哈希表,所以对字典的扩展,实际上就是对(字典的)哈希表做扩展。这个过程可以分为两步进行: 194 | 195 | 1) 创建一个比现有的 0 号哈希表更大的 1 号哈希表 196 | 197 | 2) 将 0 号哈希表的所有元素移动到 1 号哈希表去 198 | 199 | ``_dictExpandIfNeeded`` 函数检查字典是否需要扩展,每次往字典里添加新元素之前,这个函数都会被执行: 200 | 201 | static int _dictExpandIfNeeded(dict *d) 202 | { 203 | // 被省略的代码... 204 | 205 | // 当 0 号哈希表的已用节点数大于等于它的桶数量, 206 | // 且以下两个条件的其中之一被满足时,执行 expand 操作: 207 | // 1) dict_can_resize 变量为真,正常 expand 208 | // 2) 已用节点数除以桶数量的比率超过变量 dict_force_resize_ratio ,强制 expand 209 | // (目前版本中 dict_force_resize_ratio = 5) 210 | if (d->ht[0].used >= d->ht[0].size && 211 | (dict_can_resize || 212 | d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)) 213 | { 214 | return dictExpand(d, ((d->ht[0].size > d->ht[0].used) ? 215 | d->ht[0].size : d->ht[0].used)*2); 216 | } 217 | 218 | // 被省略的代码... 219 | } 220 | 221 | 可以看到,当代码注释中所说的两种情况的其中一种被满足的时候, ``dictExpand`` 函数就会被调用: 0 号哈希表的桶数量和节点数量两个数值之间的较大者乘以 2 ,就会被作为第二个参数传入 ``dictExpand`` 函数。 222 | 223 | 这次调用 ``dictExpand`` 函数执行的是和之前创建 0 号哈希表时不同的路径 —— 这一次,程序执行的是 else case —— 它将新哈希表赋值给 1 号哈希表,并将字典的 ``rehashidx`` 属性从 ``-1`` 改为 ``0``: 224 | 225 | int dictExpand(dict *d, unsigned long size) 226 | { 227 | // 创建带链表数组的新哈希表 228 | dictht n; /* the new hash table */ 229 | unsigned long realsize = _dictNextPower(size); 230 | 231 | /* the size is invalid if it is smaller than the number of 232 | * elements already inside the hash table */ 233 | if (dictIsRehashing(d) || d->ht[0].used > size) 234 | return DICT_ERR; 235 | 236 | /* Allocate the new hash table and initialize all pointers to NULL */ 237 | n.size = realsize; 238 | n.sizemask = realsize-1; 239 | n.table = zcalloc(realsize*sizeof(dictEntry*)); 240 | n.used = 0; 241 | 242 | /* Is this the first initialization? If so it's not really a rehashing 243 | * we just set the first hash table so that it can accept keys. */ 244 | if (d->ht[0].table == NULL) { 245 | d->ht[0] = n; 246 | return DICT_OK; 247 | } 248 | 249 | /* Prepare a second hash table for incremental rehashing */ 250 | // 这次执行这个动作 251 | d->ht[1] = n; // 赋值新哈希表到 d->ht[1] 252 | d->rehashidx = 0; // 将 rehashidx 设置为 0 253 | return DICT_OK; 254 | } 255 | 256 | 257 | 渐进式 rehash ,以及平摊操作 258 | -------------------------------- 259 | 260 | 在前一节的最后, ``dictExpand`` 的代码中,当字典扩展完毕之后,字典会同时使用两个哈希表( ``d->ht[0]`` 和 ``d->ht[1]`` 都不为 ``NULL`` ),并且字典 ``rehash`` 属性的值为 ``0`` 。这意味着,可以开始对 0 号哈希表进行 rehash 操作了。 261 | 262 | Redis 对字典的 rehash 操作是通过将 0 号哈希表中的所有数据移动到 1 号哈希表来完成的,当移动完成, 0 号哈希表的数据被清空之后, 0 号哈希表的空间就会被释放,接着 Redis 会将原来的 1 号哈希表设置为新的 0 号哈希表。如果将来这个 0 号哈希表也不能满足储存需要,那么就再次执行 rehash 过程。 263 | 264 | 需要说明的是,对字典的 rehash 并不是一次性地完成的,因为 0 号哈希表中的数据可能非常多,而一次性移动大量的数据必定对系统的性能产生严重影响。 265 | 266 | 为此, Redis 采取了一种更平滑的 rehash 机制,Redis 文档里称之为渐增式 rehash (incremental rehashing):它将 rehash 操作平摊到 ``dictAddRaw`` 、 ``dictGetRandomKey`` 、 ``dictFind`` 和 ``dictGenericDelete`` 这四个函数里面,每当上述这些函数执行的时候(或者其他函数调用它们的时候), ``_dictRehashStep`` 函数就会被执行,它每次将 1 个元素从 0 号哈希表移动到 1 号哈希表: 267 | 268 | ![调用_dictRehashStep的那些函数](https://raw.github.com/redisbook/book/master/image/redis_dict_incremental_rehashing.png) 269 | 270 | 作为展示渐增式 rehash 的一个例子,以下是 ``dictFind`` 函数的定义: 271 | 272 | dictEntry *dictFind(dict *d, const void *key) 273 | { 274 | // 被省略的代码... 275 | 276 | // 检查字典(的哈希表)能否执行 rehash 操作 277 | // 如果可以的话,执行平摊 rehash 操作 278 | if (dictIsRehashing(d)) _dictRehashStep(d); 279 | 280 | // 被省略的代码... 281 | } 282 | 283 | 其中 ``dictIsRehashing`` 是一个宏,它检查字典的 ``rehashidx`` 属性是否不为 ``-1`` : 284 | 285 | #define dictIsRehashing(ht) ((ht)->rehashidx != -1) 286 | 287 | 如果条件成立成立的话, ``_dictRehashStep`` 就会被执行,将一个元素从 0 号哈希表转移到 1 号哈希表: 288 | 289 | static void _dictRehashStep(dict *d) { 290 | if (d->iterators == 0) dictRehash(d,1); 291 | } 292 | 293 | ``_dictRehashStep`` 定义中的 ``iterators == 0`` 检查表示,当有迭代器在处理字典的时候,不能进行 rehash ,因为迭代器可能会修改字典中的元素,从而造成 rehash 错误。 294 | 295 | 就这样,如同愚公移山一般, 0 号哈希表的元素被逐个逐个地移动到 1 号哈希表,最终整个 0 号哈希表被清空,当 ``_dictRehashStep`` 再调用 ``dictRehash`` 时,被清空的 0 号哈希表就会被删除,然后原来的 1 号哈希表成为新的 0 号哈希表。 296 | 297 | 当有需要再次进行 rehash 的时候,这个循环就会再次开始。 298 | 299 | 以下是 ``dictRehash`` 函数的完整实现,它清晰地说明了如何轮换 0 号哈希表和 1 号哈希表,以及,如何将 0 号哈希表的元素 rehash 到 1 号哈希表: 300 | 301 | /* Performs N steps of incremental rehashing. Returns 1 if there are still 302 | * keys to move from the old to the new hash table, otherwise 0 is returned. 303 | * Note that a rehashing step consists in moving a bucket (that may have more 304 | * thank one key as we use chaining) from the old to the new hash table. */ 305 | int dictRehash(dict *d, int n) { 306 | if (!dictIsRehashing(d)) return 0; 307 | 308 | while(n--) { 309 | dictEntry *de, *nextde; 310 | 311 | // 如果 0 号哈希表为空,使用 1 号哈希表代替它 312 | /* Check if we already rehashed the whole table... */ 313 | if (d->ht[0].used == 0) { 314 | zfree(d->ht[0].table); 315 | d->ht[0] = d->ht[1]; 316 | _dictReset(&d->ht[1]); 317 | d->rehashidx = -1; 318 | return 0; 319 | } 320 | 321 | // 进行 rehash 322 | /* Note that rehashidx can't overflow as we are sure there are more 323 | * elements because ht[0].used != 0 */ 324 | assert(d->ht[0].size > (unsigned)d->rehashidx); 325 | while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++; 326 | de = d->ht[0].table[d->rehashidx]; 327 | /* Move all the keys in this bucket from the old to the new hash HT */ 328 | while(de) { 329 | unsigned int h; 330 | 331 | nextde = de->next; 332 | /* Get the index in the new hash table */ 333 | h = dictHashKey(d, de->key) & d->ht[1].sizemask; 334 | de->next = d->ht[1].table[h]; 335 | d->ht[1].table[h] = de; 336 | d->ht[0].used--; 337 | d->ht[1].used++; 338 | de = nextde; 339 | } 340 | d->ht[0].table[d->rehashidx] = NULL; 341 | d->rehashidx++; 342 | } 343 | return 1; 344 | } 345 | 346 | 另外,还有一个确保 rehash 得以最终完成的重要条件,那就是 —— 当 ``rehashidx`` 不等于 ``-1`` ,也即是 ``dictIsRehashing`` 为真时,所有新添加的元素都会直接被加到 1 号数据库,这样 0 号哈希表的大小就会只减不增,最终 rehash 总会有完成的一刻(假如新加入的元素还继续被放进 0 号哈希表,那么尽管平摊 rehash 一直在努力地进行,但说不定 rehash 还是永远也完成不了): 347 | 348 | dictEntry *dictAddRaw(dict *d, void *key) 349 | { 350 | // 被省略的代码... 351 | 352 | // 如果字典正在进行 rehash ,那么将新元素添加到 1 号哈希表, 353 | // 否则,使用 0 号哈希表 354 | ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0]; 355 | 356 | // 被省略的代码... 357 | } 358 | 359 | 另外,除了 ``_dictRehashStep`` 以及 ``dictAddRaw`` 的特殊处理之外,Redis 还会在每次事件中断器运行的时候,执行一个为时一毫秒的 ``rehash`` 操作,在文件 ``redis.c`` 中的 ``serverCron`` 函数中记录了这一点。 360 | 361 | 362 | 哈希表的大小 363 | ------------------- 364 | 365 | 在介绍完哈希表的使用流程和 rehash 机制之后,最后一个需要探索的地方就是哈希表的大小了。 366 | 367 | 我们知道哈希表最初的大小是由 ``DICT_HT_INITIAL_SIZE`` 常量决定的,而当 rehash 开始之后,根据给定的条件,哈希表的大小就会发生变动: 368 | 369 | static int _dictExpandIfNeeded(dict *d) 370 | { 371 | // 被省略的代码... 372 | 373 | if (d->ht[0].used >= d->ht[0].size && 374 | (dict_can_resize || 375 | d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)) 376 | { 377 | return dictExpand(d, ((d->ht[0].size > d->ht[0].used) ? 378 | d->ht[0].size : d->ht[0].used)*2); 379 | } 380 | 381 | // 被省略的代码... 382 | } 383 | 384 | 可以看到, ``d->ht[0].size`` 和 ``d->ht[0].used`` 两个数之间的较大者乘以 ``2`` ,会作为 ``size`` 参数的值被传入 ``dictExpand`` 函数。 385 | 386 | 但是,尽管如此,这个数值仍然还不是哈希表的最终大小,因为在 ``dictExpand`` 里面,真正的哈希表大小需要 ``_dictNextPower`` 函数根据传入的 ``size`` 参数计算之后才能得出: 387 | 388 | int dictExpand(dict *d, unsigned long size) 389 | { 390 | // 被省略的代码... 391 | 392 | // 计算哈希表的(真正)大小 393 | unsigned long realsize = _dictNextPower(size); 394 | 395 | // 被省略的代码... 396 | } 397 | 398 | ``_dictNextPower`` 不断计算 2 的乘幂,直到遇到大于等于 ``size`` 参数的乘幂,就返回这个乘幂作为哈希表的大小: 399 | 400 | static unsigned long _dictNextPower(unsigned long size) 401 | { 402 | unsigned long i = DICT_HT_INITIAL_SIZE; 403 | 404 | if (size >= LONG_MAX) return LONG_MAX; 405 | while(1) { 406 | if (i >= size) 407 | return i; 408 | i *= 2; 409 | } 410 | } 411 | 412 | 虽然桶的元素个数 ``d->ht[0].size`` 刚开始是固定的( ``DICT_HT_INITIAL_SIZE`` ),但是,因为我们没有办法预知 ``d->ht[0].used`` 的值,所以我们没有办法准确预估新哈希表的大小,不过,我们可以确定以下两个关于哈希表大小的性质: 413 | 414 | 1) 哈希表的大小总是 2 的乘幂(也即是 2^N,此处 N 未知) 415 | 416 | 2) 1 号哈希表的大小总比 0 号哈希表大 417 | 418 | 419 | 哈希表的缩小 420 | -------------------------------------- 421 | Redis 有可能存在大量数据被删除的情况,尤其是 expires 库存在过期数据被清理,此时的哈希表仅有少量的键值,却占用了大量的空间,于是很自然的就有缩小哈希表的行为。 422 | 423 | 在 serverCron 函数里每 10 个轮回会调用 tryResizeHashTables 函数一次来判断哈希表是否需要缩小: 424 | 425 | if (server.bgsavechildpid == -1 && server.bgrewritechildpid == -1) { 426 | if (!(loops % 10)) tryResizeHashTables(); 427 | if (server.activerehashing) incrementallyRehash(); 428 | } 429 | 430 | tryResizeHashTables 函数会检查 Redis 所有的数据库(包括 expires 库)是否存在空间浪费的现象: 431 | 432 | void tryResizeHashTables(void) { 433 | int j; 434 | 435 | for (j = 0; j < server.dbnum; j++) { 436 | if (htNeedsResize(server.db[j].dict)) 437 | dictResize(server.db[j].dict); 438 | if (htNeedsResize(server.db[j].expires)) 439 | dictResize(server.db[j].expires); 440 | } 441 | } 442 | 443 | htNeedsResize 判断是否要缩小哈希表:当键个数 * 100 / 桶的大小小于``REDIS_HT_MINFILL``(默认为10),即使用比低于 10%,则认为哈希表需要缩小于是调用 dictResize 函数: 444 | 445 | int htNeedsResize(dict *dict) 446 | { 447 | long long size, used; 448 | 449 | size = dictSlots(dict); 450 | used = dictSize(dict); 451 | return (size && used && size > DICT_HT_INITIAL_SIZE && 452 | (used*100/size < REDIS_HT_MINFILL)); 453 | } 454 | 455 | dictResize 用于确定了哈希表缩小后的大小,这里可见桶大小缩小到和键个数一致,当然必须大于桶的最小值``DICT_HT_INITIAL_SIZE``,缩小的过程还是调用 dictExpand 函数对哈希表进行 rehash: 456 | 457 | int dictResize(dict *d) 458 | { 459 | int minimal; 460 | 461 | if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR; 462 | minimal = d->ht[0].used; 463 | if (minimal < DICT_HT_INITIAL_SIZE) 464 | minimal = DICT_HT_INITIAL_SIZE; 465 | return dictExpand(d, minimal); 466 | } 467 | 468 | 这就是哈希表缩小的过程。 469 | 470 | 471 | 小结 472 | -------- 473 | 474 | 以上就是 Redis 字典结构的实现分析了,因为边幅所限,这里展示的函数多数都只贴出了主要部分的代码,如果对所有代码的细节感兴趣,可以到我的 GITHUB 上去找带有完整注释的代码: [https://github.com/huangz1990/reading_redis_source](https://github.com/huangz1990/reading_redis_source) 475 | -------------------------------------------------------------------------------- /redis-directory-intro.md: -------------------------------------------------------------------------------- 1 | #源码目录介绍 2 | 3 | 4 | hoterran@~/Projects/redis-2.4.16$ tree -L 1 5 | . 6 | |-- 00-RELEASENOTES 7 | |-- BUGS 8 | |-- CONTRIBUTING 9 | |-- COPYING 10 | |-- deps 11 | |-- INSTALL 12 | |-- Makefile 13 | |-- README 14 | |-- redis.conf 15 | |-- runtest 16 | |-- src 17 | |-- tests 18 | `-- utils 19 | 20 | 21 | ##Makefile 22 | 23 | 连``automake``都未使用,够简洁的。 24 | 25 | ``redis-server``,``redis-client``是编译之后的关键二进制,前者是服务端,后者是官方出品的客户端软件。 26 | 27 | ##redis.conf 28 | 29 | redis.conf 是``redis-server``的启动配置文件, 后面有一章节会单独讲解这个文件。 30 | 31 | ##deps 32 | 33 | hoterran@~/Projects/redis-2.4.16$ tree deps/ -L 1 34 | deps/ 35 | |-- hiredis 36 | |-- jemalloc 37 | `-- linenoise 38 | 39 | 40 | ###hiredis 41 | 42 | Redis 的 c api,编译 Redis 官方客户端``redis-client``,工具``redis-checkaof``都需要使用它。 43 | 44 | api 包含了处理网络的``net.c``,包含多种多路复用的``adapters``目录,动态字符串``sds.c``,处理哈希结构的``dict.c``。 45 | 46 | 47 | ###jemalloc 48 | 49 | 2.4 版本之后,Redis 开始使用了``facebook``工程师出品的``jemalloc`` 来做内存管理,``jemalloc``从各方评测的结果可见与``google``工程师出品的``tcmalloc``都不相伯仲,皆为内存管理器领域最高水平。 50 | 51 | 52 | ###linenoise 53 | 54 | 命令行行编辑管理工具。 55 | 56 | 57 | ##src 58 | 59 | 源码目录 60 | 61 | 62 | ##tests 63 | 64 | tcl吐槽 65 | 66 | 67 | ##utils 68 | 69 | Redis 辅助工具,例如打包,初始化脚本。 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /redis-hash-table.md: -------------------------------------------------------------------------------- 1 | ## B 树和哈希 2 | 3 | key-value 数据库的 kv 查询的实现有很多种, 4 | 比如功能全面的 btree ,而 Redis 的作者选择了简单的 hash 来实现,使用 hash 就意味着无法使用范围查询等功能,但选择更好的 hash 函数可以达到更快的速度,而且代码的实现更简单。 5 | 6 | 7 | ## Redis 在哪里使用哈希 8 | 9 | 在 Redis 里哈希无处不在, 10 | 11 | * server->db 全局的键值。 12 | * server->command 命令与函数指针的关系。 13 | * 哈希数据结构 14 | 15 | 哈希的实现在 src/dict.c,src/dict.h 里。 16 | 17 | 18 | ##hash源码分析 19 | 20 | ![Redis hash table ](https://raw.github.com/redisbook/book/master/image/redis_dict.png) 21 | 22 | dict 为哈希表的主结构体,dictht 是为 rehash 而存在的中间数据结构(在客户端的hash table实现中是没有 dictht,见附录3),bucket 就是哈希算法里的桶,而 dictEntry 就为每个 key-value 结构体。 23 | 24 | dictht->ht 指向 2 个 dictht。 存在 2 个 ht 的目的是为了在 rehash 的时候可以平滑的迁移桶里的数据,而不像client的dict要把老的哈希表里的一次性的全部数据迁移到新的哈希表,这种密集型的操作,在业务高峰期不可取。 25 | 26 | 每次的key-value查询过程就是,把要查询的键(Key),经过哈希函数运算后得到值(value)再次与 dictht->sizemask 求位与,这样就获得一个大于等于 0 小于等于 sizemask 的值,这个值决定了桶数组的索引。桶数组的元素是一个 dictEntry 的指针。而 dictEntry 包含一个 next 指针,这就形成了一个 dictEntry 的链表。 27 | 28 | 发生哈希冲突之时,解决冲突使用的是 seperate chaining(http://en.wikipedia.org/wiki/Hash_table#Separate_chaining),把新的 dictEntry 加到链表的头部,所以插入是一个O(1)的操作,对于查询则是一个O(N)的操作,需要遍历这个 dictEntry 链表。 29 | 30 | dictht->used 表示这个哈希表里已经插入的键值个数,也就是 dictEntry 的个数,每次 dictAdd 成功会对该值 +1,dictDel 成功会对该值 -1。 随着键值不断的添加,每个桶后面的单链表越来越长,查找、删除效率就变得越来越低。 31 | 32 | 33 | ### 触发 rehash 的条件 34 | 35 | 当dict->used/dict->size >= dict_force_resize_ratio(默认是5)的时候,就认为链表较长了。 36 | 37 | 于是就有了expand和rehash的,创建一个新的hash table(ht\[1\]),expand ht[1]的bucket数组的长度为ht[0]上的两倍,rehash会把ht[0]上所有的key移动到ht[1]上。 38 | 39 | 随着 bucket 数量的增多,每个 dictEntry链表的长度就缩短了。而 hash 查找是 O(1) 不会因为 bucket 数组大小的改变而变化,而遍历链表从 O(N) 变为 O(N/2) 的时间复杂度。 40 | 41 | ## rehash 42 | 43 | 当桶后面的链表越来越长,访问目标键值变慢,就需要 rehash 来加快访问速度。 44 | 45 | rehash 并不是一次性的迁移所有的 key,而是随着 dictAdd,dictFind 函数的执行过程调度_dictRehashStep 函数一次一个 bucket 下的 key 从 ht[0] 迁移到 ht[1]。dict->rehashidx 决定哪个 bucket 需要被迁移。当前 bucket 下的 key 都被迁移后,dict->rehashidx++,然后迁移下一个 bucket,直到所有的 bucket下的key被迁走。 46 | 47 | 除了 dict_add、dict_find 出发 rehash,另外在 serverCron 里也会调用 incrementallyRehash 函数,针对每个库的哈希表进行一次最大耗时 1s 的增量哈希,这样使得即使没有发生查询行为也会进行 rehash 的迁移。 48 | 49 | void incrementallyRehash(void) { 50 | int j; 51 | 52 | for (j = 0; j < server.dbnum; j++) { 53 | if (dictIsRehashing(server.db[j].dict)) { 54 | dictRehashMilliseconds(server.db[j].dict,1); 55 | break; /* already used our millisecond for this loop... */ 56 | } 57 | } 58 | } 59 | 60 | dictRehashMilliseconds 一次 rehash 100 个桶。 61 | 62 | int dictRehashMilliseconds(dict *d, int ms) { 63 | long long start = timeInMilliseconds(); 64 | int rehashes = 0; 65 | 66 | while(dictRehash(d,100)) { 67 | rehashes += 100; 68 | if (timeInMilliseconds()-start > ms) break; 69 | } 70 | return rehashes; 71 | } 72 | 73 | 74 | 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]。 75 | 76 | 在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]还有数据的状况。 77 | 78 | 79 | ### resize 80 | 81 | 82 | -------------------------------------------------------------------------------- /redis-how-to-use-dict.md: -------------------------------------------------------------------------------- 1 | # 如何使用``dict`` 2 | 3 | ``dict.c``是个写的非常好的哈希表操作的库,值得学习,值得复用,下面讲讲要如何使用这个库。 4 | 5 | 首先需要注意这里说的``dict.c`` 是客户端 deps/dict.c,而不是服务端的 src/dict.c,两者有一些区别: 6 | 7 | * 服务端的 rehash 是增量形式完成的,所以有 ht[0],ht[1] 两个桶指针用于切换。而客户端的 rehash 是一次性的行为,所以 dictht 这个结构在客户端``dict.c``就没有。另外 rehash 相关的函数,在客户端里也不提供。 8 | * 对于服务端,存在空间浪费的问题,所以引入了 dictResize 函数来对内存空间进行清空,这点在客户端里也没有提供。 9 | 10 | 11 | 先来解读一下``dictType``这个结构各个字段的作用,这非常的重要。 12 | 13 | 14 | ## dictType 解释 15 | 16 | typedef struct dictType { 17 | unsigned int (*hashFunction)(const void *key); 18 | void *(*keyDup)(void *privdata, const void *key); 19 | void *(*valDup)(void *privdata, const void *obj); 20 | int (*keyCompare)(void *privdata, const void *key1, const void *key2); 21 | void (*keyDestructor)(void *privdata, void *key); 22 | void (*valDestructor)(void *privdata, void *obj); 23 | } dictType; 24 | 25 | 以上六个回调函数是在哈希表创建之时,由使用者存入 dict 结构内,第一个``hashFunction``是必须的,另外的回调如果存在会在适当的时候被调用。 26 | 27 | 28 | ### hashFunction 29 | 30 | 当进行哈希转换之时,会调用``hashFunction``,把用户的键转化成一个整型数字,使用者应该根据自己的键的类型给出响应的哈希函数。 31 | 32 | Redis 里的键为一个 sds 的字符串,所以客户端选择了 dictGenHashFunction 作为其的``hashFunction``,Redis 服务端另外还提供了一下对整型(dictIntHashFunction)和对大小写敏感(dictGenCaseHashFunction)的字符串哈希函数。 33 | 34 | 35 | ### keyCompare 36 | 37 | 在进行键值查找之时,会调用 keyCompare 来判断两个键是否相等,返回值为 1,表明键值相同。 38 | 39 | Redis 里的 Key 为一个 sds 的字符串,只要比较一下字符串是否相等(dictSdsKeyCompare)。 40 | 41 | #define dictCompareHashKeys(d, key1, key2) \ 42 | (((d)->type->keyCompare) ? \ 43 | (d)->type->keyCompare((d)->privdata, key1, key2) : \ 44 | (key1) == (key2)) 45 | 46 | 47 | ### keyDup & valDup 48 | 49 | 如果没有设置 Dup 函数,那么哈希表里存入的仅仅是键值对应的指针,如果在此之后修改了键值,自然会影响到哈希表里值。 50 | 51 | 如果设置了 Dup 函数,则在 dictAdd 之时,会额外的调用 Dup 函数,对键值进行拷贝工作,那么插入之后键值就可以 free 掉了,Redis 并没有设置使用这两个回调函数,也不推荐使用。 52 | 53 | 如果你的键值是栈上空间,那么在开始的 dictType 一定要设置 Dup 函数,一般我们也不推荐使用栈上的空间。 54 | 55 | #define dictSetHashKey(d, entry, _key_) do { \ 56 | if ((d)->type->keyDup) \ 57 | entry->key = (d)->type->keyDup((d)->privdata, _key_); \ 58 | else \ 59 | entry->key = (_key_); \ 60 | } while(0) 61 | 62 | 63 | ### keyDestructor, valDestructor 64 | 65 | 当调用 dictDelete 函数时,如果使用者额外的设置了 Destructor 函数,则删除之于还会调用这个函数进行键值的内存释放。 66 | 67 | 对于全局的哈希键值表,键为 sds,值为 RedisObject,所以会调用 dictSdsDestructor 和 dictRedisObjectDestructor,自动的释放键值占用的空间。 68 | 69 | #define dictFreeEntryVal(d, entry) \ 70 | if ((d)->type->valDestructor) \ 71 | (d)->type->valDestructor((d)->privdata, (entry)->val) 72 | 73 | #define dictFreeEntryKey(d, entry) \ 74 | if ((d)->type->keyDestructor) \ 75 | (d)->type->keyDestructor((d)->privdata, (entry)->key) 76 | 77 | 78 | ## 几个 api 的使用说明 79 | 80 | dict *dictCreate(dictType *type, void *privDataPtr); 81 | 82 | 创建一个 dict 结构体,使用者需要先定义好 dictType,然后存入, privDataPtr 很少使用。 83 | 84 | int dictAdd(dict *d, void *key, void *val); 85 | 86 | 往哈希表里添加一对键值,键值是拷贝还是仅仅是指针赋值,取决于 Dup 回调函数是否设置。 87 | 88 | int dictDelete(dict *d, const void *key); 89 | 90 | 从哈希表里删除一对键值,如果 dictType 里设置 Destructor 函数,那么会自动调度这两个函数来释放内存,另外有个 dictDeleteNoFree 函数 无论是否设置了 Destructor 函数都不会调用。 91 | 92 | dictEntry * dictFind(dict *d, const void *key); 93 | 94 | 在哈希表里找到指定的键对应的 dictEntry,要拿到键还需要调用宏 dictGetEntryKey,拿到值还要调用宏 dictGetEntryVal。 95 | 96 | int dictReplace(dict *d, void *key, void *val); 97 | 98 | 对哈希表里的键值进行更换,如果键已存在返回 1,不存在返回 0。 99 | 100 | int dictExpand(dict *d, unsigned long size); 101 | 102 | 对哈希表进行扩展,size 就是扩展后桶的大小,这里要注意一下,对于服务端的这个函数,仅仅是吧 rehashidx 设置为 0,表明从 0 号桶开始增量的 rehash行为,而在客户端里,则是在函数内部一次性的弄完整个 rehash。 103 | 104 | void dictRelease(dict *d); 105 | 106 | 释放整个哈希表,自然的会释放内部所有的键值。 107 | 108 | 109 | ## 几个使用的例子 110 | 111 | 我们举几个使用 dict 的例子,一下是几个步骤: 112 | 113 | * 确定键值类型。 114 | * 确定 Hash,Compare,Dup,Destructor 函数,其中 Hash 和 Compare 是必须的。 115 | 116 | 117 | ### 键值皆为整数的例子: 118 | 119 | 键值类型: 120 | 121 | typedef struct Key_t 122 | { 123 | int k; 124 | } Key_t; 125 | 126 | typedef struct Val_t 127 | { 128 | int v; 129 | } Val_t; 130 | 131 | 由于是整型,hash 函数就设置为值本身,比较函数就设置为整数的比较: 132 | 133 | unsigned int testHashFunction(const void *key) 134 | { 135 | Key_t *k1 = (Key_t*)key; 136 | return k1->k; 137 | }; 138 | 139 | unsigned int testHashFunction(const void *key) 140 | { 141 | Key_t *k1 = (Key_t*)key; 142 | return k1->k; 143 | }; 144 | 145 | 不设置 Dup 函数,Destructor 函数就是简单的 free: 146 | 147 | void testHashKeyDestructor(void *privdata, void *key) 148 | { 149 | free(key); 150 | }; 151 | 152 | void testHashValDestructor(void *privdata, void *val) 153 | { 154 | free(val); 155 | }; 156 | 157 | 于是我们的 dictType 就是如此: 158 | 159 | dictType testDictType = { 160 | testHashFunction, /* hash */ 161 | NULL, 162 | NULL, 163 | testHashKeyCompare, /* key compare */ 164 | testHashKeyDestructor, /* key destructor */ 165 | testHashValDestructor /* value destructor */ 166 | }; 167 | 168 | 好吧,看下面主函数吧,简单的插入和查询: 169 | 170 | int main(int argc, char *argv[]) 171 | { 172 | int ret; 173 | dict *d = dictCreate(&testDictType, NULL); 174 | assert(d); 175 | Key_t *k = (Key_t*)malloc(sizeof(*k)); 176 | k->k = 1; 177 | Val_t *v = (Val_t*)malloc(sizeof(*v)); 178 | v->v = 2; 179 | 180 | ret = dictAdd(d, k, v); 181 | assert(ret == DICT_OK); 182 | 183 | Val_t *v2 = dictFetchValue(d, k); 184 | 185 | assert(v2->v == v->v); 186 | 187 | printf("%d-%d-%d\n", ret, v->v, v2->v); 188 | return 0; 189 | } 190 | 191 | 192 | ### 值为字符串的例子: 193 | 194 | 键与上个例子相同,值为字符串: 195 | 196 | typedef struct Key_t 197 | { 198 | int k; 199 | } Key_t; 200 | 201 | typedef struct Val_t 202 | { 203 | char *v; 204 | } Val_t; 205 | 206 | 要注意的是这类值的 Destructor 需要特别小心,需要额外处理字段 v 的内存释放: 207 | 208 | void testHashValDestructor(void *privdata, void *val) 209 | { 210 | Val_t *v1 = (Val_t*) val; 211 | free(v1->v); 212 | v1->v = NULL; 213 | free(v1); 214 | }; 215 | 216 | 来看看主函数: 217 | 218 | int main(int argc, char *argv[]) 219 | { 220 | int ret; 221 | dict *d = dictCreate(&testDictType, NULL); 222 | assert(d); 223 | Key_t *k = (Key_t*)malloc(sizeof(*k)); 224 | k->k = 1; 225 | 226 | Val_t *v = (Val_t*)malloc(sizeof(*v)); 227 | v->v = malloc(100); 228 | snprintf(v->v, 100, "%s", "abcdefg"); 229 | 230 | ret = dictAdd(d, k, v); 231 | assert(ret == DICT_OK); 232 | 233 | Val_t *v2 = dictFetchValue(d, k); 234 | 235 | assert(0 == strcmp(v2->v, v->v)); 236 | 237 | printf("%d-%s-%s\n", ret, v->v, v2->v); 238 | 239 | dictRelease(d); 240 | 241 | return 0; 242 | } 243 | 244 | 245 | ### 键为复合结构的例子: 246 | 247 | 假设键值是一个复合结构,例如 ip 四元表: 248 | 249 | typedef struct Key_t 250 | { 251 | uint32_t laddr, raddr; 252 | uint16_t lport, rport; 253 | } Key_t; 254 | 255 | typedef struct Val_t 256 | { 257 | char *v; 258 | } Val_t; 259 | 260 | 那么我们就要为此设置特殊的哈希函数: 261 | 262 | static unsigned long 263 | hash_fun(uint32_t laddr, uint32_t raddr, uint16_t lport, uint16_t rport) 264 | { 265 | unsigned long ret; 266 | 267 | ret = laddr ^ raddr; 268 | ret ^= (lport << 16) | rport; 269 | 270 | return ret; 271 | } 272 | 273 | unsigned int testHashFunction(const void *key) 274 | { 275 | Key_t *k1 = (Key_t*)key; 276 | return hash_fun(k1->laddr, k1->raddr, k1->lport, k1->rport); 277 | }; 278 | 279 | 280 | 来看主函数: 281 | 282 | int main(int argc, char *argv[]) 283 | { 284 | int ret; 285 | dict *d = dictCreate(&testDictType, NULL); 286 | assert(d); 287 | Key_t *k = (Key_t*)malloc(sizeof(*k)); 288 | k->laddr = 112; 289 | k->raddr = 112; 290 | k->lport = 1123; 291 | k->rport = 3306; 292 | 293 | Val_t *v = (Val_t*)malloc(sizeof(*v)); 294 | v->v = malloc(100); 295 | snprintf(v->v, 100, "%s", "abcdefg"); 296 | 297 | ret = dictAdd(d, k, v); 298 | assert(ret == DICT_OK); 299 | 300 | Val_t *v2 = dictFetchValue(d, k); 301 | 302 | assert(0 == strcmp(v2->v, v->v)); 303 | 304 | printf("%d-%s-%s\n", ret, v->v, v2->v); 305 | 306 | return 1; 307 | }; 308 | 309 | 310 | 好了就是 dict.c 的使用过程。 311 | -------------------------------------------------------------------------------- /redis-intro.md: -------------------------------------------------------------------------------- 1 | #简介 2 | 3 | 4 | Redis(REmote DIctionary Server)是一个开源的键-值内存数据库,与它类似的有``memcached``,``Tokyo Cabinet``等。Redis 以支持丰富的数据结构著称,同时兼具主从复制、持久化等高可用特性,与程序无缝的结合。 5 | 6 | 7 | ##第三方库依赖 8 | 9 | ``memcached`` 使用``libevent``这个已经不那么轻量级的网络事件库,而 Redis 本身不依赖任何第三方的函数库,无论是网络事件、哈希表,数据结构都是自己实现的,全部代码只有 2w 行,算是一个小型的项目,代码清晰,阅读起来非常的流畅,甚至都无须 debug 调试来辅助理解。 10 | 11 | 12 | ##从哪里开始 13 | 14 | 本书将对其的源代码进行分析,版本为 2.4.16。下载源代码解压 redis-2.4.16.tar.gz 包后,进入``redis``目录,我们从``src/Redis.c``的主函数开始我们代码旅行。 15 | 16 | 推荐读者使用``cscope``这样可以很方便的从函数之间跳转,如何使用``cscope``可见附录1,如何设置``vim``的快捷键可见附录2。 17 | 18 | 19 | ##为何redis是单线程? 20 | 21 | 由于支持复杂的数据结构,所以如果采用多线程,将非常的麻烦,试想一个双链表,要支持多线程将多么的复杂。 22 | 23 | Redis 并不是一个``fit all``的键-值数据库,单线程意味着任何一个客户端连接的处理速度影响的全局的性能,所以一些较消耗性能的操作(set的操作或者zset的排序操作)都尽量放到备库。 24 | 25 | 受内存限制的特点使得目前 Redis 不能成为处理海量数据的 total solutions,而仅仅是一个复杂大型系统里的一部分。 26 | 27 | 28 | ##缺陷 29 | 30 | Redis 有很多缺陷,比如无法做到双主库,无法进行同步复制(当然这些都是可以改进的),复制无法做增量复制,复制容易受网络的影响, 无完美的集群方案,但这不影响 Redis 成为一个优秀的可信赖的组件。 31 | 32 | 33 | #同类项目 34 | 35 | 36 | ##memcached 37 | 38 | ##tc 39 | 40 | #趋势 41 | 42 | 43 | ##scripting 44 | 45 | ##cluster 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /redis-memory.md: -------------------------------------------------------------------------------- 1 | #内存分配 2 | 3 | Redis对 malloc、free、calloc、realloc 等库函数进行了包装(zmalloc.c, zmalloc.h),把需要申请的内存的大小放在申请内存的前端,free的时候就知道这次free的内存大小。 4 | 5 | (以下两个函数去掉编译宏,仅仅适合linux环境使用原生malloc) 6 | 7 | #define PREFIX_SIZE (sizeof(size_t)) 8 | 9 | void *zmalloc(size_t size) { 10 | void *ptr = malloc(size+PREFIX_SIZE); 11 | *((size_t*)ptr) = size; 12 | update_zmalloc_stat_alloc(size+PREFIX_SIZE,size); 13 | return (char*)ptr+PREFIX_SIZE; 14 | } 15 | 16 | void zfree(void *ptr) { 17 | void *realptr; 18 | size_t oldsize; 19 | if (ptr == NULL) return; 20 | realptr = (char*)ptr-PREFIX_SIZE; 21 | oldsize = *((size_t*)realptr); 22 | update_zmalloc_stat_free(oldsize+PREFIX_SIZE); 23 | free(realptr); 24 | } 25 | 26 | update_zmalloc_stat_alloc 会记录全局的内存申请状况 (used_memory),与 redis.conf 里的 maxmmory 就能够控制全局的内存使用。另外还会并对内存划分的大小分组记录(zmalloc_allocations),这样你就对key-value的大小分布非常的清楚,便于接下来的迁移、合并工作。 27 | 28 | ## Sharedobjects 29 | 30 | 对于一些程序常见的字符串(例如协议内的\r\n,OK,error,pong),Redis 提前为我们产生了对象,这样再用的使用就不会额外的申请内存。 31 | 32 | struct sharedObjectsStruct shared; 33 | 34 | struct sharedObjectsStruct 35 | { 36 | robj *crlf, *ok, *err, *emptybulk, *czero, *cone, *cnegone, *pong, *space, 37 | *colon, *nullbulk, *nullmultibulk, *queued, 38 | *emptymultibulk, *wrongtypeerr, *nokeyerr, *syntaxerr, *sameobjecterr, 39 | *outofrangeerr, *loadingerr, *plus, 40 | *select[REDIS_SHARED_SELECT_CMDS], 41 | *messagebulk, *pmessagebulk, *subscribebulk, *unsubscribebulk, *mbulk3, 42 | *mbulk4, *psubscribebulk, *punsubscribebulk, 43 | *integers[REDIS_SHARED_INTEGERS]; 44 | }; 45 | 46 | 还有从 1 到 REDIS_SHARED_INTEGERS (一般为1000)的数字都已经预分配好了。 47 | 48 | for (j = 0; j < REDIS_SHARED_INTEGERS; j++) { 49 | shared.integers[j] = createObject(REDIS_STRING,(void*)(long)j); 50 | shared.integers[j]->encoding = REDIS_ENCODING_INT; 51 | } 52 | 53 | 当使用这个数字,不需要在栈,或者堆上申请而是引用计数的使用这些不变对象,在很多虚拟机语言里也常被使用,利用 Python,java。 54 | 55 | 56 | ##如何评估内存的使用大小? 57 | 58 | redis是个内存全集的kv数据库,不存在部分数据在磁盘部分数据在内存里的情况,所以提前预估和节约内存非常重要.本文将以最常用的string和zipmap两类数据结构在jemalloc内存分配器下的内存容量预估和节约内存的方法. 59 | 60 | 61 | 先说说jemalloc,传说中解决firefox内存问题freebsd的默认malloc分配器,area,thread-cache功能和tmalloc非常的相识.在2.4版本被redis引入,在antirez的博文中提到内节约30%的内存使用.相比glibc的malloc需要在每个内存外附加一个额外的4字节内存块,jemalloc可以通过je_malloc_usable_size函数获得指针实际指向的内存大小,这样redis里的每个key或者value都可以节约4个字节,不少阿. 62 | 63 | 下面是jemalloc size class categories,左边是用户申请内存范围,右边是实际申请的内存大小.这张表后面会用到. 64 | 65 | 1 - 4 size class:4 66 | 5 - 8 size class:8 67 | 9 - 16 size class:16 68 | 17 - 32 size class:32 69 | 33 - 48 size class:48 70 | 49 - 64 size class:64 71 | 65 - 80 size class:80 72 | 81 - 96 size class:96 73 | 97 - 112 size class:112 74 | 113 - 128 size class:128 75 | 129 - 192 size class:192 76 | 193 - 256 size class:256 77 | 257 - 320 size class:320 78 | 321 - 384 size class:384 79 | 385 - 448 size class:448 80 | 449 - 512 size class:512 81 | 513 - 768 size class:768 82 | 769 - 1024 size class:1024 83 | 1025 - 1280 size class:1280 84 | 1281 - 1536 size class:1536 85 | 1537 - 1792 size class:1792 86 | 1793 - 2048 size class:2048 87 | 2049 - 2304 size class:2304 88 | 2305 - 2560 size class:2560 89 | STRING 90 | string类型看似简单,但还是有几个可优化的点.先来看一个简单的set命令所添加的数据结构. 91 | 92 | 93 | 94 | 一个set hello world命令最终(中间会malloc,free的我们不考虑)会产生4个对象,一个dictEntry(12字节),一个sds用于存储key,还有一个redisObject(12字节),还有一个存储string的sds.sds对象除了包含字符串本生之外,还有一个sds header和额外的一个字节作为字符串结尾共9个字节. 95 | 96 | sds.c 97 | ======== 98 | 51 sds sdsnewlen(const void *init, size_t initlen) { 99 | 52 struct sdshdr *sh; 100 | 53 101 | 54 sh = zmalloc(sizeof(struct sdshdr)+initlen+1); 102 | 103 | sds.h 104 | ======= 105 | 39 struct sdshdr { 106 | 40 int len; 107 | 41 int free; 108 | 42 char buf[]; 109 | 43 110 | }; 111 | 根据jemalloc size class那张表,这个命令最终申请的内存为16(dictEtnry) + 16 (redisObject) + 16(“hello”) + 16(“world”),一共64字节.注意如果key或者value的字符串长度+9字节超过16字节,则实际申请的内存大小32字节. 112 | 113 | 提一下string常见的优化方法 114 | 115 | 尽量使VALUE为纯数字 116 | 117 | 这样字符串会转化成int类型减少内存的使用. 118 | 119 | redis.c 120 | ========= 121 | 37 void setCommand(redisClient *c) { 122 | 38 c->argv[2] = tryObjectEncoding(c->argv[2]); 123 | 39 setGenericCommand(c,0,c->argv[1],c->argv[2],NULL); 124 | 40 } 125 | object.c ======= 126 | 275 o->encoding = REDIS_ENCODING_INT; 127 | 276 sdsfree(o->ptr); 128 | 277 o->ptr = (void*) value; 129 | 可以看到sds被释放了,数字被存储在指针位上,所以对于set hello 1111111就只需要48字节的内存. 130 | 131 | 调整REDIS_SHARED_INTEGERS 132 | 133 | 如果value数字小于宏REDIS_SHARED_INTEGERS(默认10000),则这个redisObject也都节省了,使用redis Server启动时的share Object. 134 | 135 | object.c 136 | ======= 137 | 269 if (server.maxmemory == 0 && value >= 0 && value < REDIS_SHARED_INTEGERS && 138 | 270 pthread_equal(pthread_self(),server.mainthread)) { 139 | 271 decrRefCount(o); 140 | 272 incrRefCount(shared.integers[value]); 141 | 273 return shared.integers[value]; 142 | 274 } 143 | 这样一个set hello 111就只需要32字节,连redisObject也省了.所以对于value都是小数字的应用,适当调大REDIS_SHARED_INTEGERS这个宏可以很好的节约内存. 144 | 145 | 出去kv之外,dict的bucket逐渐变大也需要消耗内存,bucket的元素是个指针(dictEntry**), 而bucket的大小是超过key个数向上求整的2的n次方,对于1w个key如果rehash过后就需要16384个bucket. 146 | 147 | 开始string类型的容量预估测试, 脚本如下 148 | 149 | #! /bin/bash 150 | 151 | redis-cli info|grep used_memory: 152 | 153 | for (( start = 10000; start < 30000; start++ )) 154 | do 155 | redis-cli set a$start baaaaaaaa$start > /dev/null 156 | done 157 | 158 | redis-cli info|grep used_memory: 159 | 根据上面的总结我们得出string公式 160 | 161 | string类型的内存大小 = 键值个数 * (dictEntry大小 + redisObject大小 + 包含key的sds大小 + 包含value的sds大小) + bucket个数 * 4 162 | 163 | 下面是我们的预估值 164 | 165 | >>> 20000 * (16 + 16 + 16 + 32) + 32768 * 4 166 | 1731072 167 | 运行一下测试脚本 168 | 169 | hoterran@~/Projects/redis-2.4.1$ bash redis-mem-test.sh 170 | used_memory:564352 171 | used_memory:2295424 172 | 计算一下差值 173 | 174 | >>> 2295424 - 564352 175 | 1731072 176 | 都是1731072,说明预估非常的准确, ^_^ 177 | 178 | ZIPMAP 179 | 这篇文章已经解释zipmap的效果,可以大量的节约内存的使用.对于一个普通的subkey和value,只需要额外的3个字节(keylen,valuelen,freelen)来存储,另外的hash key也只需要额外的2个字节(zm头尾)来存储subkey的个数和结束符. 180 | 181 | 182 | 183 | zipmap类型的内存大小 = hashkey个数 * (dictEntry大小 + redisObject大小 + 包含key的sds大小 + subkey的总大小) + bucket个数 * 4 184 | 185 | 开始容量预估测试,100个hashkey,其中每个hashkey里包含300个subkey, 这里key+value的长度为5字节 186 | 187 | #! /bin/bash 188 | 189 | redis-cli info|grep used_memory: 190 | 191 | for (( start = 100; start < 200; start++ )) 192 | do 193 | for (( start2 = 100; start2 < 400; start2++ )) 194 | do 195 | redis-cli hset test$start a$start2 "1" > /dev/null 196 | done 197 | done 198 | 199 | redis-cli info|grep used_memory: 200 | 这里subkey是同时申请的的,大小是300 * (5 + 3) + 2 =2402字节,根据上面jemalloc size class可以看出实际申请的内存为2560.另外100hashkey的bucket是128.所以总的预估大小为 201 | 202 | >>> 100 * (16 + 16 + 16 + 2560) + 128 * 4 203 | 261312 204 | 运行一下上面的脚本 205 | 206 | hoterran@~/Projects/redis-2.4.1$ bash redis-mem-test-zipmap.sh 207 | used_memory:555916 208 | used_memory:817228 209 | 计算一下差值 210 | 211 | >>> 817228 - 555916 212 | 261312 213 | 是的完全一样,预估很准确. 214 | 215 | 另外扯扯zipmap的一个缺陷,zipmap用于记录subkey个数的zmlen只有一个字节,超过254个subkey后则无法记录,需要遍历整个zipmap才能获得subkey的个数.而我们现在常把hash_max_zipmap_entries设置为1000,这样超过254个subkey之后每次hset效率都很差. 216 | 217 | 354 if (zm[0] < ZIPMAP_BIGLEN) { 218 | 355 len = zm[0]; //小于254,直接返回结果 219 | 356 } else { 220 | 357 unsigned char *p = zipmapRewind(zm); //遍历zipmap 221 | 358 while((p = zipmapNext(p,NULL,NULL,NULL,NULL)) != NULL) len++; 222 | 359 223 | 360 /* Re-store length if small enough */ 224 | 361 if (len < ZIPMAP_BIGLEN) zm[0] = len; 225 | 362 } 226 | 简单把zmlen设置为2个字节(可以存储65534个subkey)可以解决这个问题,今天和antirez聊了一下,这会破坏rdb的兼容性,这个功能改进推迟到3.0版本,另外这个缺陷可能是weibo的redis机器cpu消耗过高的原因之一. 227 | 228 | 229 | 230 | ## 当内存达到上限后的策略 231 | 232 | 当达到使用内存 userd_memory 达到 maxmemory,就要通过删除键值来减少内存的使用,否则命令无法执行。 233 | 234 | * volatile-lru -> 用 LRU 算法,删除过期的键值来达到节约内存的目的。 235 | * allkeys-lru -> 用 LRU 算法,删除任意的键值,这里的任意包括过期的键值,和非过期正在使用的键值。 236 | * volatile-random -> 用随机算法,删除过期的键值。 237 | * allkeys->random -> 用随机算法,删除任意键值。 238 | * volatile-ttl -> 删除马上就要过期的键值 239 | * noeviction -> 不做任何操作,直接报错。 240 | 241 | 这里的 LRU 算法,和 mintor TTL 算法,严格按照算法来说,需要遍历所有的键值才能知道谁才是应该被删除的,但这样效率太差了。 242 | 于是有另外一个参数,取样次数来决定,在几次的范围内,使用 LRU,或者 TTL算法阿。 243 | 244 | 默认的策略是 volatile-lru,我们看这种算法的实现。 245 | 246 | 247 | for (k = 0; k < server.maxmemory_samples; k++) { 248 | sds thiskey; 249 | long thisval; 250 | robj *o; 251 | 252 | de = dictGetRandomKey(dict); 253 | thiskey = dictGetEntryKey(de); 254 | /* When policy is volatile-lru we need an additonal lookup 255 | * to locate the real key, as dict is set to db->expires. */ 256 | if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU) 257 | de = dictFind(db->dict, thiskey); 258 | o = dictGetEntryVal(de); 259 | thisval = estimateObjectIdleTime(o); 260 | 261 | /* Higher idle time is better candidate for deletion */ 262 | if (bestkey == NULL || thisval > bestval) { 263 | bestkey = thiskey; 264 | bestval = thisval; 265 | } 266 | } 267 | 268 | maxmemory_samples 就是取样次数。这里的 dict 是 db->expires,从过期库里取出一个dictEntry,拿到他的键 thiskey。 269 | 再拿到这个键值在正常库里的dictEntry,再通过 estimateObjectIdleTime 拿到这个键值的 LRU 时间。 270 | 取 maxmemory_samples 次后,LRU 时间最大的键 thiskey,再把其删除 271 | 272 | 273 | ### estimateObjectIdleTime 274 | 275 | estimateObjectIdleTime 是如何拿到键的 LRU 时间内? 276 | 277 | 我们知道 server.lruclock 类似于时间戳,在 serverCron 里每 100ms 被更新一次。 278 | 279 | int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) 280 | { 281 | ... 282 | updateLRUClock(); 283 | ... 284 | } 285 | 286 | 而每个键值,再被访问的时候,会把 server.lruclock 写入键值内,这就是键值的 lruclock 了,我们就是通过键值的 lruclock 来判断 LRU 时间的。 287 | 288 | robj *lookupKey(redisDb *db, robj *key) { 289 | .... 290 | robj *val = dictGetEntryVal(de); 291 | if (server.bgsavechildpid == -1 && server.bgrewritechildpid == -1) 292 | val->lru = server.lruclock; 293 | 294 | 上面的代码有个有趣的地方,当在做快照的时候,就不要更新 lruclock,因为这会造成做快照的子进程有大量的 copy on write 的行为。 295 | 296 | -------------------------------------------------------------------------------- /redis-misc.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | struct redisServer { 4 | 5 | redisDb *db; /* 多个数据库的指针数组,db[0]表示第一个数据库,默认有16个 */ 6 | list *clients; /* 客户端对连接链表 这个链表常被遍历发现 idle 的连接 */ 7 | dict *commands; /* 命令与函数对应的哈希表, readonlyCommandTable 里的信息会写入这个哈希表 */ 8 | 9 | /* RDB|AOF 加载信息 */ 10 | 11 | int loading; /* 如果是1表示在加载 RDB 或者 AOF 文件,除了 info 命令不能对外提供服务 */ 12 | 13 | off_t loading_total_bytes; /* 加载前会判断 RDB 或者 AOF 文件的大小,记录于此 */ 14 | off_t loading_loaded_bytes; /* 已经加载的字节数,通过它能了解加载的进度 */ 15 | time_t loading_start_time; /* 加载开始的时间点 */ 16 | 17 | /* 常用命令的指针 */ 18 | struct redisCommand *delCommand, *multiCommand; /* 某些命令经常被使用,那么保存一下指针,可以减少一个 commands 哈希表的查找工作 */ 19 | 20 | list *slaves, *monitors; /* slave 连接,monitor 连接的链表 */ 21 | 22 | 23 | aeEventLoop *el; /* 事件循环管理器 */ 24 | 25 | int cronloops; 26 | 27 | time_t lastsave; /* 最后一次做快照的时间点 */ 28 | 29 | /* 慢日志相关 */ 30 | list *slowlog; /* 慢日志链表 */ 31 | long long slowlog_entry_id; /* 慢日志记录的自增序列 */ 32 | long long slowlog_log_slower_than; /* 响应时间大于这个值的操作,会被记录到慢日志 */ 33 | unsigned long slowlog_max_len; /* 慢日志链表的最大长度 */ 34 | 35 | 36 | 37 | 38 | /* 备库相关信息 */ 39 | char *masterauth; /* 备库节点访问主库使用的密码 */ 40 | char *masterhost; /* 主库的 ip*/ 41 | int masterport; /* 主库的端口*/ 42 | int repl_ping_slave_period; /* */ 43 | int repl_timeout; 44 | redisClient *master; /* 主库的连接 */ 45 | int repl_syncio_timeout; /* timeout for synchronous I/O calls */ 46 | int replstate; /* 备库的状态 */ 47 | off_t repl_transfer_left; /* bytes left reading .rdb */ 48 | int repl_transfer_s; /* slave -> master SYNC socket */ 49 | int repl_transfer_fd; /* slave -> master SYNC temp file descriptor */ 50 | char *repl_transfer_tmpfile; /* slave-> master SYNC temp file name */ 51 | time_t repl_transfer_lastio; /* unix time of the latest read, for timeout */ 52 | int repl_serve_stale_data; /* Serve stale data when link is down? */ 53 | time_t repl_down_since; /* unix time at which link with master went down */ 54 | 55 | 56 | } 57 | 58 | 59 | 60 | #测试内存 61 | 62 | redis-server --test-memory 4096 63 | 64 | 65 | #配置文件 66 | 除了配置文件之外,还有一种隐蔽的参数配置方式 67 | 68 | hoterran@~/Projects/redis-2.4.16$ redis-server - < port 8000 70 | > maxmemory 20000 71 | > eof 72 | 73 | 老的配置文件方式,信息会泄漏,这种配置的好处就是文件不落地,没有密码是不能知道 redis-server的配置信息的。 74 | 75 | 76 | 配置文件可以嵌套 77 | include 78 | 79 | 80 | 81 | 82 | #rename command 83 | 84 | redis 安全方面仅有一个 requirepass 参数,作为密码校验,更是没有权限的概念(其实连用户的概念都没有),这意味着一个用户既可以执行 get 命令,他也可以执行 slave of ,shutdown 命令,对于做运维的同学来说,这变的非常的不安全,天知道开发同学代码里有些啥玩意。 85 | 86 | 所有为了屏蔽那些危险的命令,redis 允许对命令进行重命名或者屏蔽,例如我对一个 get 重命名,不知道新名称的话你就不能执行该命令了。 87 | 88 | 当然 rename 命令本身应该最先被重命名。新的名称只有运维的同学自己知道,配置在 redis.conf 里。 89 | 90 | 常见的重命名的命令如下: 91 | 92 | 93 | } else if (!strcasecmp(argv[0],"rename-command") && argc == 3) { 94 | struct redisCommand *cmd = lookupCommand(argv[1]); 95 | int retval; 96 | 97 | if (!cmd) { 98 | err = "No such command in rename-command"; 99 | goto loaderr; 100 | } 101 | 102 | /* If the target command name is the emtpy string we just 103 | * remove it from the command table. */ 104 | retval = dictDelete(server.commands, argv[1]); 105 | redisAssert(retval == DICT_OK); 106 | 107 | /* Otherwise we re-add the command under a different name. */ 108 | if (sdslen(argv[2]) != 0) { 109 | sds copy = sdsdup(argv[2]); 110 | 111 | retval = dictAdd(server.commands, copy, cmd); 112 | if (retval != DICT_OK) { 113 | sdsfree(copy); 114 | err = "Target command name already exists"; goto loaderr; 115 | } 116 | } 117 | 118 | 119 | 原理就是把 server.command 里的命令给删除掉,如果存在新的名称,把新的名字和函数指针再次加入到 server.command 里。 120 | 121 | 122 | 123 | 124 | ##setupSignalHandlers 125 | 126 | sigsegvHandler 127 | 128 | 129 | 130 | 131 | ##freeMemoryIfNeeded 132 | 133 | 134 | 去掉slave 135 | 去掉 AOFBUF 暂用的临时内存大小,这部分空间是会扩展收缩的。 136 | 137 | ##info的解读 138 | 139 | "loading_start_time:%ld\r\n" 加载开始时间 140 | "loading_total_bytes:%llu\r\n" 需要加载的字节数目(RDB或者AOF文件的大小) 141 | "loading_loaded_bytes:%llu\r\n" 已经加载的字节数目 142 | "loading_loaded_perc:%.2f\r\n" 已经加载的比率, 为前两者之商 143 | "loading_eta_seconds:%ld\r\n" 需要多久秒才能加载完毕 144 | 145 | 146 | 147 | 148 | 149 | 150 | ## 151 | 152 | _addReplyObjectToList 写到c->reply 里。 153 | 154 | 找到c->reply 最末尾的obj,往他的ptr上继续append,直到 REDIS_REPLY_CHUNK_BYTES 155 | 156 | 157 | 158 | ## 159 | 160 | * lookupKey ,最标准全局键值哈希表查找,避免copy on write 161 | 162 | * lookupKeyRead,读查找 163 | 164 | 1. 先判断是否 expire 165 | 2. lookupKey 166 | 3. 会触发 stat_keyspace 的更新 167 | 168 | * lookupKeyWrite,写查找 169 | 170 | 1. 先判断是否 expire 171 | 2. lookupKey 172 | 173 | * lookupKeyReadOrReply 读查找,没找到就返回第三个参数 174 | 175 | 1. lookupKeyRead 176 | 2. addReply 177 | 178 | * lookupKeyWriteOrReply 写查找,没找到就返回第三个参数 179 | 180 | 1. lookupKeyWrite 181 | 2. addReply 182 | 183 | 184 | 185 | 186 | 187 | ## 188 | 189 | type 190 | 191 | 192 | encoding 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | ##解读命令数组 201 | 202 | 常见的命令(例如get,set等)会调用的函数指针,这个数据结构也是以hash table的形式存储的。 203 | 每次客户端输入”set aa bb”等数据的时候,解析得到字符串”set”后,会根据“set”作为一个key,查找到value,一个函数指针(setCommand),然后再把“aa”、“bb“作为参数传给这个函数。这个hash table存储在redisServer->command里,每次redis-server启动的时候会对``readonlyCommandTable`` 这个数组进行加工(populateCommandTable)转化成 redisServer->command 这个 hash table 方便查询,而非遍历``readonlyCommandTable``查找要执行的函数。 204 | 205 | 我们知道``redis-server``启动的时候会调用``populateCommandTable``函数(src/redis.c 830)把``readonlyCommandTable``数组转化成``server.commands``这个哈希表,``lookupCommand``就是一个简单的哈希取值过程,通过 key(get)找到相应的命令函数指针``getCommand``(t_string.c 437)。 206 | 207 | 我们来解读一下``readonlyCommandTable``这个结构体。 208 | 209 | struct redisCommand 210 | { 211 | char *name; //函数名,对应与哈希表的键 212 | redisCommandProc *proc; //函数指针,对应哈希表的值 213 | int arity; //函数的参数个数,可以提前对参数个数进行判断 214 | int flags; //是否会引发内存的变化 215 | /* Use a function to determine which keys need to be loaded 216 | * in the background prior to executing this command. Takes precedence 217 | * over vm_firstkey and others, ignored when NULL */ 218 | redisVmPreloadProc *vm_preload_proc; 219 | /* What keys should be loaded in background when calling this command? */ 220 | int vm_firstkey; /* The first argument that's a key (0 = no keys) */ 221 | int vm_lastkey; /* THe last argument that's a key */ 222 | int vm_keystep; /* The step between first and last key */ 223 | }; 224 | 225 | 226 | flags 如果为 REDIS_CMD_DENYOOM,则表示这个命令的执行会触发内存的变化,所以如果达到最大使用内存会报错;如果为 REDIS_CMD_FORCE_REPLICATION 则表示这个命令一定要传播到背库。 227 | 228 | 229 | ###expire 230 | 231 | activeExpireCycle 232 | 233 | REDIS_EXPIRELOOKUPS_PER_CRON 234 | 235 | 255次,或者 消耗的时间 > REDIS_EXPIRELOOKUPS_TIME_LIMIT 236 | 237 | 238 | 239 | ### shutdown 240 | 241 | 在进程退出之前,会做两件事情。 242 | 243 | * fsync aof 文件,强制的aof 文件 fd 对应的文件系统级的缓存写入到磁盘里。 244 | * 做一次全库的快照,这样下次启动的时候,会自动载入这个快照文件,还能继续该次的数据。 245 | 246 | 247 | -------------------------------------------------------------------------------- /redis-network.md: -------------------------------------------------------------------------------- 1 | #网络框架简介 2 | 3 | 4 | ![network](https://raw.github.com/redisbook/book/master/image/redis_network_arch.png) 5 | 6 | 我们知道`initServer`这个函数创建了网络监听,并``epoll_wait``在这个监听上,等待新的连接。 7 | 8 | 一旦有新的连接,则``redis-server``主线程从``epoll_wait``处返回,然后调用``acceptTcpHandler``函数。来处理新的连接。 9 | 10 | ##acceptTcpHandler 11 | 12 | 对于新的连接,调用`createClient`(src/networking.c)函数创建``strcut redisClient``结构,``redisClient``伴随这个连接的生命周期所存在,记录这个连接的所有信息。 13 | 14 | 下面是这个结构体的简要说明,仅仅包含读写缓冲区的字段。 15 | 16 | typedef struct redisClient { 17 | int fd; //socket 18 | sds querybuf; //读缓冲 19 | int argc; //读缓冲解析后的单元个数 20 | robj *argv; //读缓冲解析后的对象数组。 21 | 22 | list *reply; //multi replies 写缓冲链表 23 | int reply_bytes //multi replies 写缓冲链表内字符串的总长度,好像没作用 24 | 25 | int sentlen; //写缓冲数组的已经写出的位置 26 | int bufpos; //写缓冲数组的末尾 27 | char buf[REDIS_REPLY_CHUNK_BYTES]; //单回应写缓冲数组 28 | } 29 | 30 | 连接建立好之后,把该连接,加入到全局的事件管理器里,当有读事件发生的时候,调用回调函数``readQueryFromClient``(src/networking.c)。 31 | 32 | 33 | ##readQueryFromClient 34 | 35 | 事件管理器发现这个连接有数据可读时,就会调用``readQueryFromClient``函数从``socket``里读取数据。 36 | 37 | 读取后的数据暂存于``querybuf``里,注意由于是非阻塞io,所以``querybuf``里的数据有可能是不完整的。 38 | 39 | 读取数据之后,就开始处理``querybuf``里的内容了,来到``processInputBuffer``函数。 40 | 41 | 42 | ##processInputBuffer 43 | 44 | 该函数会根据``querybuf``里的内容,进行字符串解析,存入``argv``内,然后通过``lookupCommand``确定``argv[1]``是哪个命令。 45 | 46 | 再根据``redisServer->command``这个哈希表找到命令相应的函数。然后把``argv``里的参数传入相应的函数。 47 | 48 | ## call 49 | 50 | 这是 Redis 最核心函数。执行完相应的命令之后,还有几步工作要做。s 51 | 52 | * 记录命令是否导致键值的变化,如果有变化则需要把变化传播到备库。 53 | 54 | if ((dirty > 0 || c->cmd->flags & REDIS_CMD_FORCE_REPLICATION) && 55 | listLength(server.slaves)) 56 | replicationFeedSlaves(server.slaves,c->db->id,c->argv,c->argc); 57 | 58 | * 如果激活 AOF,则还会把变化写入到 AOF 文件。 59 | 60 | if (server.appendonly && dirty > 0) 61 | feedAppendOnlyFile(c->cmd,c->db->id,c->argv,c->argc); 62 | 63 | * 如果存在监控客户连接,则把命令发送给该客户连接 64 | 65 | if (listLength(server.monitors)) 66 | replicationFeedMonitors(server.monitors,c->db->id,c->argv,c->argc); 67 | 68 | * 判断命令的执行时间是否超过慢日志的阀值,是否需要写入满日志 69 | 70 | slowlogPushEntryIfNeeded(c->argv,c->argc,duration); 71 | 72 | 执行完函数之后,把执行的结果存储在``buf``里,然后再注册一个写事件函数``sendReplyToClient``。 73 | 74 | 75 | ## c->argc, c->argv 76 | 77 | 例如一个``set a 1``的命令,解析后结果如下。 78 | 79 | argc = 2 80 | argv[0] = "set" 81 | argv[1] = "a" 82 | argv[2] = "1" 83 | 84 | 85 | ##sendReplyToClient 86 | 87 | 写事件比较简单,把``buf``里的内容通过连接统统写回去就算完成了,由于是非阻塞io,所以要判断返回值循环处理,直到``bufpos``为零。 88 | 最后再删除这个写事件。 89 | 90 | 91 | 好了这就是一个处理命令的全过程,简单吧,下面还会详细介绍。 92 | 93 | 94 | -------------------------------------------------------------------------------- /redis-protocol.md: -------------------------------------------------------------------------------- 1 | #协议 2 | 3 | 协议解析这章先以``get a``这样的一个命令作为例子讲解以方便理解。 4 | 5 | Redis 的协议是纯文本协议,没有任何二进制,牺牲了效率,牺牲了解析代码量,但方便了诊断,方便理解。 6 | 7 | 由于是文本协议,你可以通过``telnet``发送命令给``redis-server``。 8 | 9 | 与之不同的是通过``redis-cli``、利用``api``库发送的协议格式是更利于服务端解析的格式,对协议组装(常见的是长度放到前头,还有添加阿协议类型)。 10 | 11 | 12 | #处理协议 13 | 14 | Redis 的网络事件库,我们在前面的文章已经讲过,``readQueryFromClient``先从连接里中读取数据,先存储在``c->querybuf``里。 15 | 16 | 接下来函数``processInputBuffer``来解析``querybuf``,上面说过如果是``telnet``发送的裸协议数据是没有任何辅助信息,针对``telnet``的数据跳到 ``processInlineBuffer``函数,而其他则通过函数``processMultibulkBuffer``来处理。 17 | 18 | 这两个函数的作用一样,解析``querybuf``的字符串,分解成多参数到``argc``和``argv``里面,``argc``表示参数的个数,``argv``是个 Redis_object 的指针数组,每个指针指向一个``redisObject``, ``redisObject``的ptr里存储具体的内容,对于”get a“的请求转化后,``argc``就是2,``argv``就是 19 | 20 | (gdb) p (char\*)(\*c->argv[0])->ptr 21 | $28 = 0x80ea5ec "get" 22 | (gdb) p (char*)(*c->argv[1])->ptr 23 | $26 = 0x80e9fc4 "a" 24 | 25 | 协议解析后就执行命令。``processCommand``首先调用``lookupCommand``找到``get``对应的函数。 26 | 27 | getCommand 比较简单,通过另一个全局的 server.db 这个 hash table 来查找 key,并返回 Redis object ,然后通过 addReplyBulk 函数返回结果。 28 | 29 | ##Requests格式 30 | 31 | 参数的个数 CRLF 32 | $第一个参数的长度CRLF 33 | 第一个参数CRLF 34 | ... 35 | $第N个参数的长度CRLF 36 | 第N个参数CRLF 37 | 38 | 例如在Redis_cli里键入get a,经过协议组装后的请求为 39 | 40 | 2\r\n$3\r\nget\r\n$1\r\na\r\n 41 | 42 | ## Reply格式 43 | 44 | 45 | ### bulk replies 46 | 47 | bulk replies 是以$打头消息体,格式$值长度\r\n值\r\n,一般的 get 命令返回的结果就是这种格式。 48 | 49 | Redis>get aaa 50 | 51 | $3\r\nbbb\r\n 52 | 53 | 对应的的处理函数 addReplyBulk 54 | 55 | addReplyBulkLen(c,obj); //$3 56 | addReply(c,obj); 57 | addReply(c,shared.crlf); 58 | 59 | 60 | ### error message 61 | 62 | 是以-ERR 打头的消息体,后面跟着出错的信息,以\r\n结尾,针对命令出错。 63 | 64 | Redis>d 65 | 66 | -ERR unknown command 'd'\r\n 67 | 68 | 处理的函数是 addReplyError 69 | 70 | addReplyString(c,"-ERR ",5); 71 | addReplyString(c,s,len); 72 | addReplyString(c,"\r\n",2); 73 | 74 | 75 | ### integer reply 76 | 77 | 是以:打头,后面跟着数字和\r\n。 78 | 79 | Redis>incr a 80 | :2\r\n 81 | 82 | 处理函数是 83 | 84 | addReply(c,shared.colon); 85 | addReply(c,o); 86 | addReply(c,shared.crlf); 87 | 88 | ### status reply 89 | 90 | 以+打头,后面直接跟状态内容和\r\n 91 | 92 | Redis>ping 93 | +PONG\r\n 94 | 95 | 处理函数是 addReplyStatus 96 | 97 | addReplyString(c,"+",1); //+ 98 | addReplyString(c,s,len); 99 | addReplyString(c,"\r\n",2); 100 | 101 | 102 | 103 | 这里要注意reply经过协议加工后,都会先保存在 c->buf 里,c->bufpos 表示 buf 的长度。待到事件分离器转到写出操作(sendReplyToClient)的时候,就把 c->buf 的内容写入到 fd 里,c->sentlen 表示写出长度。当 c->sentlen = c->bufpos 才算写完。 104 | 105 | ### Multi-bulk 106 | 107 | 复合应答,对于sinter,config get,keys,zrangebyscore,slowlog,hgetall 这类函数通常需要返回多个值,这类消息结构与请求的格式一模一样。 108 | 这类回复的一个特点,只有命令函数执行结束后,才能准确的知道 replies 的个数。 109 | 110 | lrangeCommand 为什么不在此列?我们知道 Redis 的双链表的头部保留了一个链表长度字段,所以 lrange 命令在遍历链表之前,就能准确的知道应答的个数。 111 | 112 | 为什么要使用 c->reply 这个链表存储返回的值,c—>buf 数组不能满足需求么,bulk repies 就是使用 c->buf 的。 113 | 114 | replies 的个数放在协议的最前面。只有链表,哈希,集合,数组遍历完毕之后我们才能知道 replies 的个数,如果使用 c->buf,遍历完毕后需要产生一个新的字符串,写入 replies 个数,再 strcpy c->buf 到新的字符串。我们不知道内容有多大,所以这里数组实在不适合存储临时回包数据。 115 | 116 | 所以 redis 在此处对回包数据进行分段,每段为一个字符串对象,存储在 c->reply 链表的上,每个字符串最大为 REDIS_REPLY_CHUNK_BYTES。 117 | 118 | 步骤如下: 119 | 120 | * addDeferredMultiBulkLength 121 | 往 c->reply 链表尾部添加一个空的字符串对象,从此 addReply 不再往 c->buf 里写数据了,而是走到 addReply*ToList 等函数。 122 | 123 | * addReply*ToList 124 | 125 | tail = listNodeValue(listLast(c->reply)); 126 | 127 | /* Append to this object when possible. */ 128 | if (tail->ptr != NULL && 129 | sdslen(tail->ptr)+sdslen(o->ptr) <= REDIS_REPLY_CHUNK_BYTES) 130 | { 131 | c->reply_bytes -= zmalloc_size_sds(tail->ptr); 132 | tail = dupLastObjectIfNeeded(c->reply); 133 | tail->ptr = sdscatlen(tail->ptr,o->ptr,sdslen(o->ptr)); 134 | c->reply_bytes += zmalloc_size_sds(tail->ptr); 135 | } else { 136 | incrRefCount(o); 137 | listAddNodeTail(c->reply,o); 138 | c->reply_bytes += zmalloc_size_sds(o->ptr); 139 | } 140 | 141 | 找到链表最末尾的对象,因为回包都是字符串,所以肯定是 sds 字符串对象,判断现有长度和新增长度是否分段的上限?否,则继续写入这个对象。是,则链表尾部插入这个一个新的对象。 142 | 143 | * setDeferredMultiBulkLength 144 | 把 multi replies 的个数,写入 addDeferredMultiBulkLength 创建的字符串内部,然后再和链表下一个字符串内容进行结合,可以看到这里的内存拷贝最大就是 REDIS_REPLY_CHUNK_BYTES 字节。 145 | 146 | len->ptr = sdscatlen(len->ptr,next->ptr,sdslen(next->ptr)); 147 | 148 | 149 | -------------------------------------------------------------------------------- /redis-pubsub.md: -------------------------------------------------------------------------------- 1 | #pubsub 2 | 3 | ![pubsub]() 4 | -------------------------------------------------------------------------------- /redis-replication.md: -------------------------------------------------------------------------------- 1 | #复制 2 | 3 | Redis的复制的原理和使用都非常简单。只需要在slave端键入 4 | 5 | slaveof masterip port 6 | 7 | 取消复制,从slave状态的转换回master状态,切断与原master的数据同步。 8 | 9 | slaveof no one 10 | 11 | 一个master可以有多个slave,不可以 dual master。 12 | 13 | master 有变化的时候会主动的把命令传播给每个slave。slave同时可以作为其他的slave的master,前提条件是这个slave已经处于稳定状态(REDIS_REPL_CONNECTED)。 14 | 15 | slave在复制的开始阶段处于阻塞状态(sync_readline)无法对外提供服务。 16 | 17 | 18 | #复制部分源码分析 19 | 20 | 21 | ![replication](https://raw.github.com/redisbook/book/master/image/redis_replication.png) 22 | 23 | slave 端接收到客户端的 "slaveof masterip port" 命令之后,调度 slaveofCommand 保存的 masterip、port,修改 server.replstate为 REDIS_REPL_CONNECT,然后返回给客户端 OK,复制的行为是异步的,返回给用户OK只是。 24 | 25 | slave 端主线程在时间事件 serverConn(redis.c 518行)里执行replicationCron(redis.c 646行)开始与master的连接。syncWithMaster 函数与 master 的通信。经过校验之后(如果需要),会发送一个"SYNC" command 给 master 端,然后打开一个临时文件用于接收接下来master发过来的 rdb 文件数据。再添加一个文件事件注册 readSyncBulkPayload 函数,这个就是接下来用于接收rdb文件的数据的函数,然后修改状态为 REDIS_REPL_TRANSFER。 26 | 27 | master 接收到 "SYNC" command 后,跳转到 syncCommand 函数(replication.c 556行)。syncCommand 会调度 rdbSaveBackground 函数,启动一个子进程做一个全库的快照,并把状态改为 REDIS_REPL_WAIT_BGSAVE_END。master 的主线程的 serverCron 会检查这个持久化的子进程是否退出。 28 | 29 | if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) { 30 | if (pid == server.bgsavechildpid) { 31 | backgroundSaveDoneHandler(statloc); 32 | } 33 | 34 | 如果bgsave子进程正常退出,会调用backgroundSaveDoneHandler函数继续复制的工作,该函数打开刚刚产生的rdb文件。然后注册一个sendBulkToSlave函数用于发送rdb文件,状态切换至REDIS_REPL_SEND_BULK。sendBulkToSlave作用就是根据上面打开的rdb文件,读取并发送到slave端,当文件全部发送完毕之后修改状态为REDIS_REPL_ONLINE。 35 | 36 | 我们回到slave,上面讲到slave通过readSyncBulkPayload接收rdb数据,接收完整个rdb文件后,会清空整个数据库emptyDb()(replication.c 374)。然后就通过rdbLoad函数装载接收到的rdb文件,于是slave和master数据就一致了,然后把状态修改为REDIS_REPL_CONNECTED。 37 | 接下来就是master和slave之间增量的传递的增量数据,另外slave和master在应用层有心跳检测(replication.c 543)和超时退出(replication.c 511)。 38 | 39 | 介绍一些replication相关的几个数据结构和状态。 40 | slave端的server变量与复制相关的变量。 41 | 42 | struct redisServer{ 43 | ... 44 | char *masterauth; //主库的密码 45 | char *masterhost; //主库的ip 46 | int masterport; //主库的port 47 | redisClient *master; //slave连到master的连接,由slave主动发起 48 | int replstate; //slave端的状态 49 | off_t repl_transfer_left; //从master读取.rdb,还需要读取多少字节, 50 | // 开始是-1,然后会获得一个.rdb的大小,每次读取部分.rdb部分后,这个值会递减 51 | 52 | int repl_transfer_s; //slave连到master的fd 53 | int repl_transfer_fd; //接收.rdb时,写入临时文件的fd 54 | char *repl_transfer_tmpfile; //接收.rdb的临时文件名(temp-xxx.pid.rdb) 55 | time_t repl_transfer_lastio; //最近一次读取的时间,防止超时 56 | int repl_serve_stale_data; //当slave与master的连接状态不正常(不是REDIS_REPL_CONNECTED状态)的时候,是否提供服务,还是只能使用info、slaveof命令。 57 | ... 58 | } 59 | 60 | slave端的server->master 61 | 62 | redisClient { 63 | 64 | int flags; //slave连接的是什么角色,这里是REDIS_MASTER 65 | 66 | } 67 | 68 | 服务端的redisClient的信息 69 | 70 | redisClient { 71 | int flags; //slave连接上来的client,这里是REDIS_SLAVE 72 | int repldbfd; //传送.rdb数据给slave的时候的fd 73 | int slaveseldb; //slave的当前db id 74 | int repldboff; //传送.rdb数据给slave的偏移量,直到等于repldbsize才传送完毕 75 | int repldbsize; //.rdb文件的大小 76 | int replstate; // 77 | } 78 | 79 | 80 | 81 | REDIS_CMD_FORCE_REPLICATION 哪些命令必须传输给 slave 端。 82 | 83 | ## slave-serve-stale-data 84 | 85 | 该参数如果设置为 no,表示如果备库与主库尚未完全建立连接之前,仅能接收 info 和 slave 命令。 86 | 87 | 设置为 no 是表合理的,因为此时备库尚不能称之为备库,因为主库的数据还未同步至此。 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /redis-sds-implement.md: -------------------------------------------------------------------------------- 1 | 本文内容 2 | ------------- 3 | 4 | Sds (Simple Dynamic Strings)是 Redis 中最基本的底层数据结构, 5 | 它既是 Redis 的 String 类型的底层实现, 6 | 也是实现 Hash 、 List 和 Set 等复合类型的基石。 7 | 8 | 除此之外,sds 还是 Redis 内部实现所使用的字符串类型, 9 | 经过 ``robj`` 结构包装之后的 sds 被广泛用于 Redis 自身的构建当中: 10 | 比如用作 KEY 、作为函数参数、保存 Redis 命令和用作命令的回复(reply),等等。 11 | 12 | 本文通过分析源码文件 ``sds.c`` 和 ``sds.h`` ,了解 sds 数据结构的实现,籍此加深对 Redis 的理解。 13 | 14 | 15 | 数据类型定义 16 | --------------- 17 | 18 | 与 sds 实现有关的数据类型有两个,一个是 ``sds`` : 19 | 20 | // 字符串类型的别名 21 | typedef char *sds; 22 | 23 | 另一个是 ``sdshdr`` : 24 | 25 | // 持有 sds 的结构 26 | struct sdshdr { 27 | // buf 中已被使用的字符串空间数量 28 | int len; 29 | // buf 中预留字符串空间数量 30 | int free; 31 | // 实际储存字符串的地方 32 | char buf[]; 33 | }; 34 | 35 | 其中, ``sds`` 只是字符数组类型 ``char*`` 的别名, 36 | 而 ``sdshdr`` 则用于持有和保存 ``sds`` 的信息。 37 | 38 | 比如 ``sdshdr.len`` 可以用于在 O(1) 复杂度下获取 ``sdshdr.buf`` 中储存的字符串的实际长度,而 ``sdshdr.free`` 则用于保存 ``sdshdr.buf`` 中还有多少预留空间。 39 | 40 | (虽然文档和源码中都没有说明,但 ``sdshdr`` 应该是 sds handler 的缩写。) 41 | 42 | 43 | 将 sdshdr 用作 sds 44 | ------------------------------ 45 | 46 | Sds 模块对 ``sdshdr`` 结构使用了一点小技巧(trick):通过指针运算,它使得 ``sdshdr`` 结构可以像 ``sds`` 类型一样被传值和处理,并在需要的时候恢复成 ``sdshdr`` 类型。 47 | 48 | 理解这一小技巧的方法就是看以下一组函数的定义和它们的代码示例。 49 | 50 | ``sdsnewlen`` 函数返回一个新的 ``sds`` 值,实际上,它创建的却是一个 ``sdshdr`` 结构: 51 | 52 | // 根据给定初始化值和初始化长度 53 | // 创建或重分配一个 sds 54 | sds sdsnewlen(const void *init, size_t initlen) { 55 | struct sdshdr *sh; 56 | 57 | if (init) { 58 | // 创建 59 | sh = zmalloc(sizeof(struct sdshdr)+initlen+1); 60 | } else { 61 | // 重分配 62 | sh = zcalloc(sizeof(struct sdshdr)+initlen+1); 63 | } 64 | 65 | if (sh == NULL) return NULL; 66 | 67 | sh->len = initlen; 68 | sh->free = 0; // 刚开始时 free 为 0 69 | 70 | // 设置字符串值 71 | if (initlen && init) 72 | memcpy(sh->buf, init, initlen); 73 | sh->buf[initlen] = '\0'; 74 | 75 | // 只返回 sh->buf 这个字符串部分 76 | return (char*)sh->buf; 77 | } 78 | 79 | 通过使用变量持有一个 ``sds`` 值,在遇到那些只处理 ``sds`` 值本身的函数时,可以直接将 ``sds`` 传给它们。比如说, ``sdstoupper`` 函数就是其中的一个例子: 80 | 81 | sds s = sdsnewlen("hello moto", 10); 82 | sdstolower(s); 83 | // 现在 s 的值应该是 "HELLO MOTO" 84 | 85 | ``sdstoupper`` 函数将字符串内的字符全部转换为大写: 86 | 87 | void sdstoupper(sds s) { 88 | int len = sdslen(s), j; 89 | 90 | for (j = 0; j < len; j++) s[j] = toupper(s[j]); 91 | } 92 | 93 | 但是,有时候,我们不仅需要处理 ``sds`` 值本身 (也即是 ``sdshdr.buf`` 属性),还需要对 ``sdshdr`` 中其他属性,比如 ``sdshdr.len`` 和 ``sdshdr.free`` 进行处理。 94 | 95 | 使用指针运算,可以从 ``sds`` 值中计算出相应的 ``sdshdr`` 结构: 96 | 97 | // s 是一个 sds 值 98 | struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr))); 99 | 100 | ``s - (sizeof(struct sdshdr))`` 表示将指针向前移动到 ``struct sdshdr`` 的起点,从而得出一个指向 ``sdshdr`` 结构的指针: 101 | 102 | ![指针运算图示](https://raw.github.com/redisbook/book/master/image/redis_sdshdr.png) 103 | 104 | ``sdslen`` 函数是使用这种技巧的其中一个例子: 105 | 106 | // 返回字符串内容的实际长度 107 | static inline size_t sdslen(const sds s) { 108 | 109 | // 从 sds 中计算出相应的 sdshdr 结构 110 | struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr))); 111 | 112 | return sh->len; 113 | } 114 | 115 | 116 | 函数实现 117 | ----------- 118 | 119 | Sds 模块中的大部分函数都是对常见字符串处理函数的重新实现或包装,这些函数的实现都非常直观,这里就不一一详细介绍了,需要了解实现细节的话,可以直接看[带注释的源码](https://github.com/huangz1990/reading_redis_source)。 120 | 121 | 唯一一个需要提及的,和 Redis 的实现决策相关的函数是 ``sdsMakeRoomFor`` : 122 | 123 | /* Enlarge the free space at the end of the sds string so that the caller 124 | * is sure that after calling this function can overwrite up to addlen 125 | * bytes after the end of the string, plus one more byte for nul term. 126 | * 127 | * Note: this does not change the *size* of the sds string as returned 128 | * by sdslen(), but only the free buffer space we have. */ 129 | // 扩展 sds 的预留空间, 确保在调用这个函数之后, 130 | // sds 字符串后的 addlen + 1 bytes(for NULL) 可写 131 | sds sdsMakeRoomFor(sds s, size_t addlen) { 132 | struct sdshdr *sh, *newsh; 133 | size_t free = sdsavail(s); 134 | size_t len, newlen; 135 | 136 | // 预留空间可以满足本次拼接 137 | if (free >= addlen) return s; 138 | 139 | len = sdslen(s); 140 | sh = (void*) (s-(sizeof(struct sdshdr))); 141 | 142 | // 设置新 sds 的字符串长度 143 | // 这个长度比完成本次拼接实际所需的长度要大 144 | // 通过预留空间优化下次拼接操作 145 | newlen = (len+addlen); 146 | if (newlen < SDS_MAX_PREALLOC) 147 | newlen *= 2; 148 | else 149 | newlen += SDS_MAX_PREALLOC; 150 | 151 | // 重分配 sdshdr 152 | newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1); 153 | if (newsh == NULL) return NULL; 154 | 155 | newsh->free = newlen - len; 156 | 157 | // 只返回字符串部分 158 | return newsh->buf; 159 | } 160 | 161 | 从 ``newlen`` 变量的设置可以看出,如果 ``newlen`` 小于 ``SDS_MAX_PREALLOC`` ,那么 ``newlen`` 的实际值会比所需的长度多出一倍;如果 ``newlen`` 的值大于 ``SDS_MAX_PREALLOC`` ,那么 ``newlen`` 的实际值会加上 ``SDS_MAX_PREALLOC`` (目前 2.9.7 版本的 ``SDS_MAX_PREALLOC`` 默认值为 ``1024 * 1024`` )。 162 | 163 | 这种内存分配策略表明, 在对 sds 值进行扩展(expand)时,总会预留额外的空间,通过花费更多的内存,减少了对内存进行重分配(reallocate)的次数,并优化下次扩展操作的处理速度。 164 | 165 | 优化扩展操作的一个例子就是 [APPEND](http://redis.readthedocs.org/en/latest/string/append.html) 命令: ``APPEND`` 命令在执行时会调用 ``sdsMakeRoomFor`` ,多预留一部分空间。当下次再执行 ``APPEND`` 的时候,如果要拼接的字符串长度 ``addlen`` 不超过 ``sdshdr.free`` (上次 ``APPEND`` 时预留的空间),那么就可以略过内存重分配操作,直接进行字符串拼接操作。 166 | 167 | 相反,如果不使用这种策略,那么每次进行 ``APPEND`` 都要对内存进行重分配。 168 | 169 | 注意,初次创建 ``sds`` 值时并不会预留多余的空间(查看前面给出的 ``sdsnewlen`` 定义),只有在调用 ``sdsMakeRoomFor`` 起码一次之后, ``sds`` 才会有预留空间,而且 sds 模块中也有相应的紧缩空间函数 ``sdsRemoveFreeSpace`` 。因此,Redis 对 ``sds`` 值的这种扩展策略实际上不会浪费多少内存,但它对一些需要多次执行字符串拼接的 Redis 模式来说,却会获得不错的优化效果(因为频繁的内存重分配是一种比较昂贵的工作)。 170 | 171 | 172 | 结语 173 | ------ 174 | 175 | 以上就是本篇文章的全部内容了,文章首先介绍了 ``sds`` 类型和 ``sdshdr`` 结构,接着说明 Redis 是如何通过指针运算,从而将 ``sdshdr`` 当作 ``sds`` 来处理的,最后介绍了 Redis 的 sds 重分配策略是如何优化字符串拼接操作的。 176 | 177 | 如果对 ``sds.h`` 和 ``sds.c`` 源码的全部细节感兴趣,可以在 github 查看带有详细注释的源码: [https://github.com/huangz1990/reading_redis_source](https://github.com/huangz1990/reading_redis_source) 。 178 | 179 | 180 | 参考资料 181 | --------------- 182 | 183 | 本文部分内容引用自 Redis 官网上的 [Hacking String 文章](http://redis.io/topics/internals-sds) 。 184 | -------------------------------------------------------------------------------- /redis-server-data-structure.md: -------------------------------------------------------------------------------- 1 | ##数据结构 2 | 3 | ![Redis data structure](https://raw.github.com/Redisbook/book/master/image/Redis_db_data_structure.png) 4 | 5 | 每个 key-value 的数据都会存储在 RedisDb 这个结构里,而 RedisDb 就是一个 hash table。 6 | 7 | 8 | ###尽力节省空间 9 | 10 | REDIS_STRING 和REDIS_ENCODING_RAW,假设这些数据都要存储在 Redis 内部了,这个时候全字符串肯定不是最优的存储方法。于是需要尝试的转换格式,比如“1”就应该转化成long或者longlong类型 11 | 12 | /* Try to encode a string object in order to save space */ 13 | robj *tryObjectEncoding(robj *o) { 14 | /* Check if we can represent this string as a long integer */ 15 | if (isStringRepresentableAsLong(s,&value) == REDIS_ERR) return o; 16 | /* Ok, this object can be encoded... 17 | if (server.maxmemory == 0 && value >= 0 && value < REDIS_SHARED_INTEGERS && 18 | pthread_equal(pthread_self(),server.mainthread)) { 19 | decrRefCount(o); 20 | incrRefCount(shared.integers[value]); 21 | return shared.integers[value]; 22 | } else { 23 | o->encoding = REDIS_ENCODING_INT; 24 | sdsfree(o->ptr); 25 | o->ptr = (void*) value; 26 | return o; 27 | } 28 | } 29 | 30 | 如果处于共享区域,则自增加1,否则转化成INT类型。释放老的string类型,指向新的long或者longlong类型。 31 | 32 | 33 | ###如何存储 34 | 35 | 36 | 例如 "set a 1" 会创建3 个 argv, 如果数据保留了,则 1 都会incrRefCount,而不set,a 都会被删除掉 37 | 在前面可以看到全局的key是以sds形式存储的,dictAdd的时候会拷贝一份,所以a对应的object也可以删除掉,而1对应的object必须保存,这就是数据阿。 38 | 39 | 40 | ###伪代码 41 | 42 | processInputBuffer 43 | ProcessMultibulkBuffer 44 | while 45 | c->argv[c->argc++] = createStringObject(c->querybuf+pos,c->bulklen); 46 | 47 | call 48 | c->argv[2] = tryObjectEncoding(c->argv[2]); 49 | incrRefCount(val); 50 | 51 | resetClient 52 | freeClientArgv 53 | for 54 | decrRefCount(c->argv[j]); 55 | c->argc = 0; 56 | 57 | ###表格? 58 | 59 | type 60 | encoding1 61 | encoding2 62 | condtion 63 | REDIS_STRING 64 | REDIS_ENCODING_RAW 65 | REDIS_ENCODING_INT 66 | 67 | REDIS_LIST 68 | REDIS_ENCODING_ZIPLIST 69 | REDIS_ENCODING_LINKEDLIST 70 | 71 | REDIS_SET 72 | REDIS_ENCODING_INTSET 73 | REDIS_ENCODING_HT 74 | 75 | REDIS_ZSET 76 | 77 | 78 | 79 | REDIS_HASH 80 | REDIS_ENCODING_HT 81 | REDIS_ENCODING_ZIPMAP 82 | 83 | 84 | ziplist是用来代替双链表,非常的节省内存 85 | .... 86 | zlbytes是到zlend的距离 87 | zllen entry的个数 88 | zltail是最后一个entry的offset 89 | zlend是个单字节的值,等于255,暗示链表的结尾。 90 | 91 | String 92 | List 93 | Set 94 | zset 95 | Hash 96 | 97 | 98 | ###字符串 99 | 100 | 从图上我们可以看出 key 为”hello”,value 为 ”world” 的存储格式。 101 | 102 | ###列表 103 | key 为 ”list“,value为一个字符串链表(“aaa”,”bbb”,”ccc”)的存储型式, 104 | 105 | 106 | ###zset 107 | 108 | 109 | -------------------------------------------------------------------------------- /redis-server-start.md: -------------------------------------------------------------------------------- 1 | #redis-server启动的主流程 2 | 3 | 我们从 redis-server(src/redis.c) 启动说起,随着 main 函数浏览一下各个关键函数,了解一下 Redis 的主要程序流程。 4 | 5 | ![Redis-server-start](https://raw.github.com/redisbook/book/master/image/redis_server.png) 6 | 7 | 8 | ##initServerConfig 9 | 10 | ``initServerConfig``函数会给 server(RedisServer) 这个全局变量设置一些默认的参数,比如监听端口为 6379 ,默认的内置 db 个数为 16 等。默认值对于一个用户友好的软件非常重要,谁愿意第一次使用软件还要设置一大堆云里雾里的参数呢?所有的参数后面会详细讲述。 11 | 12 | RedisServer 这个结构非常重要,是 Redis 服务端程序唯一的一个结构体,稍后我们会详细介绍这个结构体成员的作用。 13 | 14 | server.commands = dictCreate(&commandTableDictType,NULL); 15 | populateCommandTable(); 16 | server.delCommand = lookupCommandByCString("del"); 17 | server.multiCommand = lookupCommandByCString("multi"); 18 | 19 | ``PopulateCommandTable``会把命令与函数数组``readonlyCommandTable``数组结构保存到哈希表结构 server.commands,方便跨速查找。例如用户键入了``set a 1``这个命令到服务端,服务端解析协议后知道了``set`` 这个命令,就可以找到``setCommand``这个相应的处理函数,这个后面会详细描述。 20 | 21 | 另外好保存 del,multi命令的指针,方便更快速的使用。 22 | 23 | server.slowlog_log_slower_than = REDIS_SLOWLOG_LOG_SLOWER_THAN; 24 | server.slowlog_max_len = REDIS_SLOWLOG_MAX_LEN; 25 | 26 | 慢日志参数的配置,凡是超过 REDIS_SLOWLOG_LOG_SLOWER_THAN 时间的命令会被记录到慢日志里,慢日志最多能存储 REDIS_SLOWLOG_MAX_LEN 条记录。 27 | 28 | 29 | ##loadServerConfig 30 | 31 | 如果``redis-server``启动参数里有指定``redis.conf``,``LoadServerConfig``函数就会读入``redis.conf``里的参数,覆盖之前的默认值。 32 | 33 | 34 | ##initServer 35 | 36 | * 利用``signal``屏蔽一些信号,设置一些信号处理函数``setupSignalHandlers``。 37 | 38 | * 再次对 redis-server 这个数据结构做初始化。 39 | 40 | * createSharedObjects 41 | 提前产生一些常用的对象,避免临时 malloc。 42 | 43 | * aeCreateEventLoop 创建多路服务的文件、事件事件管理器。 44 | 45 | * anetTcpServer 启动 6379 端口的监听。 46 | 47 | * anetUnixServer 启动unix socket 的监听。 48 | 49 | * 添加文件和时间事件。 50 | 1. 添加一个时间事件,函数是``serverCron``。这个函数会每 100ms 执行一次,后面会详细描述这个函数的作用。 51 | 2. 添加一个监听的文件事件,把 accept 行为注册到只读的监听文件描述符上,回凋函数是``acceptTcpHandler``。 52 | 53 | * slowlogInit 启动慢日志功能,发现比较慢的命令。 54 | 55 | * bioInit 启动后台线程来处理系统调用耗时的操作。 56 | 57 | 58 | ##aeMain(el) 59 | 60 | 进入 Redis 的主循环。 61 | 62 | 每次循环之前还会执行``beforeSleep`` 63 | 64 | 然后开始循环,这个循环目前每隔 100ms 会执行一次``serverCron``函数,并仅仅盯着监听的 fd,等待外部的连接,有连接则调用``acceptTcpHandler``。 65 | 66 | 67 | ##beforeSleep 68 | 69 | * 清理unblocked_clients?? 70 | * 处理尚未处理的数据,调用 processInputBuffer 为什么还会有数据呢? 71 | * flushAppendOnlyFile 72 | 73 | 74 | ##serverCron 75 | 76 | 时间事件``serverCron``需要做很多事情。 77 | 78 | * 更新lruclock 79 | * 每50次,打印出库内键值状况 80 | * 每10次,resize 哈希表?? 81 | * 调用 incrementallyRehash 增量哈希 82 | * 关闭长时间不工作的 client。 83 | * 处理 bgsave 或者 bgrewriteaof 的子进程退出后的收尾工作。 84 | * rewriteAppendOnlyFileBackground ?? 85 | * 根据键值的变化判断是否需要启动子进程做快照,或者根据AOF文件的当前状况判断是否需要启动子进程执行 AOF 文件重整理工作。 86 | * 如果激活 AOF 延迟刷到磁盘机制,则执行一次 AOF 文件的刷到磁盘 87 | * 如果是主库,清理过期(expire)的 键值。 88 | * 每10次,也就是1s,执行 replicationCron,如果自己是主库,会检测备库节点的状况,如果自己是备库,会连接主库。 89 | 90 | 91 | ##acceptTcpHandler 92 | 93 | 现在来看``redis-server``如何处理网络连接,见下一章。 94 | -------------------------------------------------------------------------------- /redis-snapshot.md: -------------------------------------------------------------------------------- 1 | #持久化 2 | 3 | 4 | Redis有全量(save/bgsave)持久化和增量(aof)的持久化命令。 5 | 6 | ##全量持久化, 快照 7 | 8 | 9 | 遍历里所有的 RedisDb ,读取每个 bucket 里链表的 key 和 value 并写入 dump.rdb 文件(rdb.c 405)。 10 | 11 | save 命令直接调度 rdbSave 函数,这会阻塞主线程的工作,通常我们使用bgsave。 12 | 13 | bgsave 命令调度 rdbSaveBackground 函数启动了一个子进程然后调度了rdbSave函数,子进程的退出状态由 serverCron的 backgroundSaveDoneHandler 来判断,这个在前面复制章节已经提及。 14 | 15 | 除了直接的save、bgsave命令之外,还有几个地方还调用到 rdbSaveBackground 和 rdbSave 函数。 16 | 17 | * shutdown:Redis 关闭调度的 prepareForShutdown 会做一次持久化工作,保证重启后数据依然存在,会调用 rdbSave。 18 | 19 | * flushallCommand:清空 Redis 数据后,如果不做立即执行一个 rdbSave,生成一个空的快照出现 crash 后,可能会载入含有老数据的快照。 20 | 21 | 22 | void flushallCommand(RedisClient *c) { 23 | touchWatchedKeysOnFlush(-1); 24 | server.dirty += emptyDb(); // 清空数据 25 | addReply(c,shared.ok); 26 | if (server.bgsavechildpid != -1) { 27 | Kill(server.bgsavechildpid,SIGKILL); 28 | rdbRemoveTempFile(server.bgsavechildpid); 29 | } 30 | rdbSave(server.dbfilename); //没有数据的dump.db 31 | server.dirty++; 32 | } 33 | 34 | * sync:当master接收到slave发来的该命令的时候,会执行 rdbSaveBackground,这个以前也有提过。 35 | 36 | 37 | ###redis.conf 相关的参数 38 | 39 | 数据发生变化:在多少秒内出现了多少次变化则触发一次 bgsave,这个可以在 redis.conf 里配置。 40 | 41 | for (j = 0; j < server.saveparamslen; j++) { 42 | struct saveparam *sp = server.saveparams+j; 43 | if (server.dirty >= sp->changes && now-server.lastsave > sp->seconds) { 44 | rdbSaveBackground(server.dbfilename); 45 | break; 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /redis-transaction.md: -------------------------------------------------------------------------------- 1 | #事务 2 | 3 | 4 | ##multi commands 5 | 6 | Redis 使用 multi、exec、discard 等命令实现简单事务,可以同时提交或者同时回滚,但不能处理部分的事务,事务过程中进程crash会导致部分数据写入Redis,而部分数据失败。实例重启并不会回滚本不该写入的数据。 7 | 8 | 不是传统意义上的事务,只是一次性执行多个语句,所以你无法读数据,也应用无法交互,你只能一股脑的写或者修改数据, 这个在 scripting 有所改观。 9 | 10 | 另外使用watch命令观察某些key,如果exec之前,这些key出现修改则回滚整个事务,否则提交事务,来达到乐观锁的作用。Watch配合setnx可以设计出媲美关系型数据库的分布式锁机制。 11 | 12 | 我们先来看multi,这个命令发送到服务端后,会修改 RedisClient 对象,把 client->flag 设置为 REDIS_MULTI。 13 | 14 | void multiCommand(RedisClient *c) { 15 | if (c->flags & REDIS_MULTI) { 16 | addReplyError(c,"MULTI calls can not be nested"); 17 | return; 18 | } 19 | c->flags |= REDIS_MULTI; 20 | addReply(c,shared.ok); 21 | } 22 | 23 | 从此之后,原本要执行的命令不再调用 call 函数,反而执行了 queueMultiCommand 函数,把命令保存到一个管道里。 24 | 25 | int processCommand(RedisClient *c) { 26 | ... 27 | if (c->flags & REDIS_MULTI && 28 | cmd->proc != execCommand && cmd->proc != discardCommand && 29 | cmd->proc != multiCommand && cmd->proc != watchCommand) 30 | { 31 | queueMultiCommand(c,cmd); 32 | addReply(c,shared.queued); 33 | } else { 34 | if (server.vm_enabled && server.vm_max_threads > 0 && 35 | blockClientOnSwappedKeys(c,cmd)) return REDIS_ERR; 36 | call(c,cmd); 37 | } 38 | 39 | queueMultiCommand 函数是将接下来的命令和参数塞入 RedisClient 对象的一个命令数组 mstate 里。命令都是数组的形式添加到command里,count表示命令的个数,扩展使用的是realloc整个数组。 40 | 41 | ![multi commands](https://raw.github.com/redisbook/book/master/image/redis_multi_command.png) 42 | 43 | 当需要回滚的时候我们键入 discard 命令,该命令会清空上面的 multistate 并把count设置为 0,然后修改 c->flags 去掉 REDIS_MULTI状态。 44 | 45 | 如果提交命令则键入EXEC命令,EXEC会把multistate的命令拿出来依次执行。等会我们结合watch一块来讲解EXEC。 46 | 47 | ##watch 48 | 49 | 来看 watch 命令,这个命令非常有用。我们假象一个这样的场景。如果没有 INCR 命令我们要自增加一个 key,我们会如何做? 50 | 51 | a = Redis.get(key) 52 | Redis.set(key, a+1) 53 | 54 | 看起来是个不错的方法,假设初值是 10,我们修改后应该为 11 的。如果在get、set之间另外一个client也执行了同样的操作也把 key 加 1。这样 key 本应该等于12,结果等于了 11。如何解决这种问题? 55 | 56 | watch就是为此而生的。从watch开始到exec之间,一旦watch的key发生了变化,则提交失败,否则提交成功,从返回的结果里可以看出提交是否成功。代码如下 57 | 58 | >>> r = Redis.Redis("127.0.0.1", 6379, password="aliyundba") 59 | >>> r.watch("a") 60 | True 61 | >>> z = r.pipeline("a") 62 | >>> z.set("a", 4) 63 | 64 | >>> z.execute() 65 | [True] 66 | 67 | 我们做依次中 68 | 69 | >>> r.watch("a") 70 | True 71 | >>> z = r.pipeline("a") 72 | >>> z.set("a", 5) 73 | 74 | >>> z.execute() 75 | Redis.exceptions.WatchError: Watched variable changed. 76 | 77 | -------------------------------------------------------------------------------- /resource.md: -------------------------------------------------------------------------------- 1 | # 资源列表 2 | 3 | ## 项目 4 | 5 | 消息队列 6 | 7 | * [RQ](http://python-rq.org/) 使用 Python 编写 8 | * [Resque](https://github.com/defunkt/resque/) 使用 Ruby 编写 9 | -------------------------------------------------------------------------------- /style.md: -------------------------------------------------------------------------------- 1 | # 风格规范 2 | 3 | 4 | ## 格式 5 | 6 | 文本使用 markdown 格式来书写。 7 | 8 | Markdown 的具体细节可以参考 [Markdown 语法说明](http://wowubuntu.com/markdown/) 。 9 | 10 | 11 | ## 标题 12 | 13 | Markdown 的标题可以用两种格式来书写: 14 | 15 | # 标题 16 | 17 | 标题 18 | ----- 19 | 20 | 我们选用 ``# 标题`` 风格。 21 | 22 | 23 | ## 空行 24 | 25 | 为了方便识别,每个标题和上一段正文之间,需要空出至少两行。 26 | 27 | 比如: 28 | 29 | 上一段正文... 30 | 31 | 32 | # 标题 33 | 34 | 35 | ## 列表 36 | 37 | 列表使用 ``*`` 符号来标识: 38 | 39 | * 列表项 1 40 | * 列表项 2 41 | * 列表项 3 42 | 43 | 44 | ## 代码 45 | 46 | 代码块使用 4 个空格进行缩进: 47 | 48 | void greet(void) 49 | { 50 | printf("hello world!\n"); 51 | } 52 | 53 | 省略代码时,要在注释中进行说明: 54 | 55 | void longFunction(void) 56 | { 57 | // 省略 ... 58 | printf("hello world\n"); 59 | // 省略 ... 60 | } 61 | 62 | 63 | ## 行内代码 64 | 65 | 语法关键字、文件路径、变量等文字,需要用行内样式标示。 66 | 67 | 比如: 68 | 69 | ``/home/user/someone`` 70 | 71 | ``struct redisClient`` 的 ``lua`` 属性表示 Lua 环境实例 72 | 73 | 74 | ## 数字和英文 75 | 76 | 夹杂中英文和数字的文本,需要在数字或英文的左右两边放一个空格,左右两边有标点符号包裹除外。 77 | 78 | 比如: 79 | 80 | Redis 数据库是由 Salvatore Sanfilippo (antirez)开发的一款高性能数据库。 81 | 82 | Redis 的最新稳定版本是 2.4.16 。 83 | 84 | 85 | ## 标点符号 86 | 87 | 所有文本正文使用中文、全角标点符号。 88 | 89 | 应该是: 90 | 91 | Redis 的作者是 Salvatore Sanfilippo (antirez)。 92 | 93 | 而不是: 94 | 95 | Redis 的作者是 Salvatore Sanfilippo (antirez). 96 | 97 | 唯一一个例外是,当用大 O 表示算法复杂度的时候,使用英文括号。 98 | 99 | 应该是: 100 | 101 | 这个算法的复杂度为 O(1) 。 102 | 103 | 而不是: 104 | 105 | 这个算法的复杂度为 O(1) 。 106 | 107 | 108 | ## 英文大小写 109 | 110 | 在文本中,英文品牌和名称应该使用正确的大小写。 111 | 112 | 应该是: 113 | 114 | Redis 数据库内嵌了对 Lua 环境的支持。 115 | 116 | 而不是: 117 | 118 | redis 数据库内嵌了对 lua 环境的支持。 119 | -------------------------------------------------------------------------------- /usage/automatic_cache_hot_data.md: -------------------------------------------------------------------------------- 1 | # 自动缓存热数据 2 | 3 | TODO: 使用 EXPIRE 和其他相关命令,参考 http://v2ex.com/t/49284 4 | -------------------------------------------------------------------------------- /usage/beyond_redis.md: -------------------------------------------------------------------------------- 1 | # 超越 Redis 2 | 3 | 目标:通过使用 Lua 脚本,以 Redis 的数据结构为基础,构建更复杂的数据结构和应用。 4 | -------------------------------------------------------------------------------- /usage/cache.md: -------------------------------------------------------------------------------- 1 | # 缓存 2 | 3 | 缓存是 Redis 最常见的用法之一,常用的键-值缓存可以用字符串或者哈希来实现。 4 | 5 | 6 | ## API 7 | 8 | 一个最基本的缓存系统应该有以下两个基本操作: 9 | 10 | ``set(key, value, timeout=nil)`` 11 | 12 | 设置缓存 ``key`` 的值为 ``value`` 。参数 ``timeout`` 是可选的,它决定缓存的生存时间,以秒为单位。 13 | 14 | ``get(key)`` 15 | 16 | 获取缓存 ``key`` 的值。 17 | 18 | 一些更复杂的缓存系统可能会有 ``get_multi_key`` 、 ``set_if_not_exists`` 、 ``delete`` 或者 ``set_timeout`` 和 ``get_timeout`` 等操作。 19 | 20 | 以下两个小节分别介绍字符串缓存和哈希缓存的实现。 21 | 22 | 23 | ## 字符串缓存 24 | 25 | 字符串的实现非常简单和直观,只需将相应的 Redis 命令,比如 [SET](http://redis.readthedocs.org/en/latest/string/set.html) 、 [SETEX](http://redis.readthedocs.org/en/latest/string/setex.html) 和 [GET](http://redis.readthedocs.org/en/latest/string/get.html) 分别用函数包裹起来就可以了: 26 | 27 | require 'redis' 28 | 29 | $redis = Redis.new 30 | 31 | def set(key, value, timeout=nil) 32 | if timeout == nil 33 | return $redis.set(key, value) 34 | else 35 | return $redis.setex(key, timeout, value) 36 | end 37 | end 38 | 39 | def get(key) 40 | return $redis.get(key) 41 | end 42 | 43 | 测试: 44 | 45 | irb(main):001:0> load 'string_cache.rb' 46 | => true 47 | irb(main):002:0> set('key', 'value') 48 | => "OK" 49 | irb(main):003:0> get('key') 50 | => "value" 51 | irb(main):004:0> $redis.ttl('key') # 没有给定 timeout, 所以没有设置 ttl 52 | => -1 53 | irb(main):005:0> set('another-key', 'another-value', 10086) 54 | => "OK" 55 | irb(main):006:0> get('another-key') 56 | => "another-value" 57 | irb(main):007:0> $redis.ttl('another-key') # 给定了 timeout 参数 58 | => 10074 59 | 60 | 61 | ## 哈希缓存 62 | 63 | 哈希缓存将多个缓存保存到同一个哈希中,使用者需要指定哈希的名字,这可以通过全局变量或者添加多一个参数来完成。 64 | 65 | 除此之外,因为 Redis 并不提供为哈希中的单个 ``key`` 设置过期时间的功能,所以在这个实现中,我们去掉 ``set`` 操作设置过期时间的功能,而单独使用一个 ``expire`` 操作来设置哈希的过期时间。 66 | 67 | 修改之后的 API 如下: 68 | 69 | ``set(hash_name, key, value)`` 70 | 71 | ``get(hash_name, key)`` 72 | 73 | ``expire(hash_name, timeout)`` 74 | 75 | 实现的定义如下: 76 | 77 | require 'redis' 78 | 79 | $redis = Redis.new 80 | 81 | def set(hash_name, key, value) 82 | return $redis.hset(hash_name, key, value) 83 | end 84 | 85 | def get(hash_name, key) 86 | return $redis.hget(hash_name, key) 87 | end 88 | 89 | def expire(hash_name, timeout) 90 | return $redis.expire(hash_name, timeout) 91 | end 92 | 93 | 测试: 94 | 95 | irb(main):001:0> load 'hash_cache.rb' 96 | => true 97 | irb(main):002:0> set('greeting', 'morning', 'good morning!') 98 | => true 99 | irb(main):003:0> get('greeting', 'morning') 100 | => "good morning!" 101 | irb(main):004:0> set('greeting', 'night', 'good night!') 102 | => true 103 | irb(main):005:0> get('greeting', 'night') 104 | => "good night!" 105 | irb(main):006:0> $redis.hgetall('greeting') 106 | => {"morning"=>"good morning!", "night"=>"good night!"} 107 | irb(main):007:0> $redis.ttl('greeting') 108 | => -1 109 | irb(main):008:0> expire('greeting', 10086) 110 | => true 111 | irb(main):009:0> $redis.ttl('greeting') 112 | => 10085 113 | 114 | 115 | ## 两种缓存方式之间的功能对比 116 | 117 | 字符串缓存为每个缓存设置一个单独的 ``key`` ,因此每个字符串缓存可以单独控制自己的过期时间。哈希缓存将多个缓存保存到同一个哈希 ``key`` 中,因此整个哈希共享同一个过期时间。 118 | 119 | 一般来说,对于单个缓存操作来说,字符串缓存更灵活,但是在一些情况下,哈希缓存提供的对多个缓存的操作也非常有用。 120 | 121 | 举个例子,你可以将多个 Redis 相关的文章缓存到 ``Redis`` 哈希下,如果你对多个 Redis 文章进行了批量修改,之后只要删掉 ``Redis`` 哈希,就可以激活缓存的更新: 122 | 123 | set('Redis', 'Redis 源码分析(1)', '...') 124 | set('Redis', 'Redis 源码分析(2)', '...') 125 | set('Redis', 'Redis 源码分析(3)', '...') 126 | 127 | # 对多个 Redis 文章进行批量修改 128 | 129 | $redis.del('Redis') 130 | 131 | # 之后所有 Redis 文章的缓存都要重新设置 132 | 133 | 另一方面,使用字符串缓存来做同样的事情却复杂得多: 134 | 135 | # 需要保证每个字符串 key 都使用同样的前缀 136 | 137 | set('Redis 源码分析(1)', '...') 138 | set('Redis 源码分析(2)', '...') 139 | set('Redis 源码分析(3)', '...') 140 | 141 | # 对多个 Redis 文章进行批量修改 142 | 143 | # 遍历并删除所有相关文章的缓存 144 | $redis.keys('Redis *').each do |key| 145 | $redis.del(key) 146 | end 147 | 148 | # 之后所有 Redis 文章的缓存都要重新设置 149 | 150 | 从以上的代码也可以看出对多个字符串缓存进行操作的限制:如果某篇 Redis 文章不是以 'Redis ' 开头的话,那么它就不会出现在 [KEYS 命令](http://redis.readthedocs.org/en/latest/key/keys.html) 的输出结果中。 151 | 152 | 另一个关于字符串缓存和哈希缓存的重要区别是,使用哈希缓存更节省内存,Redis 的官方网站介绍了如何使用哈希结构来代替字符串结构,从而节省大量内存的例子: [memory-optimization topic](http://redis.io/topics/memory-optimization) 。 153 | -------------------------------------------------------------------------------- /usage/counter.md: -------------------------------------------------------------------------------- 1 | # 计数器 2 | 3 | 在日常的生活中,我们每天都和各式各样的计数器打交道。很大一类常见应用都可以归类为计数器,比如网页阅览量、帖子的回复数、邮件的未读计数等等,甚至是一些初看上去不那么像计数器的功能,比如自增 id 、收藏和投票,实际上都是计数器的一种。 4 | 5 | 在以下部分,文章会分别介绍简单计数器和唯一计数器。 6 | 7 | 8 | ## 简单计数器 9 | 10 | 就像名字所描述的一样,简单计数器主要用于计数逻辑非常简单直接的应用上,比如访问计数和网页阅览量统计,这类应用的最常见用法是,当某个给定时间发生的时候,对计数器加一。 11 | 12 | 简单计数器一般包含以下 API : 13 | 14 | ``incr(counter, increment)`` 将给定增量增加到计数器上,并返回(增加操作执行之后)计数器的值。 15 | 16 | ``decr(counter, decrement)`` 将给定减量应用到计数器上,并返回(减法操作执行之后)计数器的值。 17 | 18 | ``get(counter)`` 返回计数器的当前值。 19 | 20 | ``reset(counter)`` 将计数器的值重置为 ``0`` 。 21 | 22 | 简单计数器可以用字符串和哈希两种方式实现,字符串的 API 相对简单一些: 23 | 24 | require 'redis' 25 | 26 | $redis = Redis.new 27 | 28 | def incr(counter, increment=1) 29 | return $redis.incrby(counter, increment) 30 | end 31 | 32 | def decr(counter, decrement=1) 33 | return $redis.decrby(counter, decrement) 34 | end 35 | 36 | def get(counter) 37 | value = $redis.get(counter) 38 | return value.to_i if value != nil 39 | end 40 | 41 | def reset(counter) 42 | value = $redis.set(counter, 0) 43 | return 0 if value == "OK" 44 | end 45 | 46 | 哈希实现将多个计数器放在同一个哈希上,因此需要一个额外的参数 ``hash`` 指定保存计数器的哈希: 47 | 48 | require 'redis' 49 | 50 | $redis = Redis.new 51 | 52 | def incr(hash, counter, increment=1) 53 | return $redis.hincrby(hash, counter, increment) 54 | end 55 | 56 | def decr(hash, counter, decrement=1) 57 | return $redis.hincrby(hash, counter, -decrement) 58 | end 59 | 60 | def get(hash, counter) 61 | value = $redis.hget(hash, counter) 62 | return value.to_i if value != nil 63 | end 64 | 65 | def reset(hash, counter) 66 | value = $redis.hset(hash, counter, 0) 67 | return 0 68 | end 69 | 70 | 71 | ## 实例:阅览量统计 72 | 73 | 以下是一个使用 [Sinatra 框架](http://www.sinatrarb.com/) 构建的网页阅览量统计的例子,每当有用户访问某本书的页面时,计数器就会给这本书的阅览量加上一: 74 | 75 | get '/book/:id' do 76 | pv = incr("page-view #{params[:id]}") 77 | # ... 78 | end 79 | 80 | 现在,要查看某个图书页面的点击量,调用 ``get("page-view #{id}")`` 就可以了。 81 | 82 | 83 | ## 实例:顺序自增标识符(sequential auto increment id) 84 | 85 | 在一些分布式数据库如 MongoDB 中,生成的键总是一个哈希值,比如 ``4da070180d03918e09fe7dad`` ,可以通过计数器生成一系列顺序自增标识符,来作为对用户友好的标识符。 86 | 87 | 顺序自增标识符的实现包装了计数器实现,它的完整定义如下: 88 | 89 | load 'string_simple_counter.rb' 90 | 91 | def generate_id(tag) 92 | return incr(tag) 93 | end 94 | 95 | 以下实例说明了如何生成一系列连续用户标识符: 96 | 97 | irb(main):001:0> load 'auto_id.rb' 98 | => true 99 | irb(main):002:0> generate_id('user') 100 | => 1 101 | irb(main):003:0> generate_id('user') 102 | => 2 103 | irb(main):004:0> generate_id('user') 104 | => 3 105 | 106 | 107 | ## 唯一计数器 108 | 109 | 和简单计数器不同,唯一计数器对每个记录实体只计数一次。 110 | 111 | 比如说,在 [StackOverflow](http://stackoverflow.com/) 网站上,用户可以对问题和答案进行提升和下沉投票,而且每个用户只能投票一次: 112 | 113 | ![投票示例](https://raw.github.com/redisbook/book/bb003b6a7ec203fed21e64c392997fdbc440ad11/image/usage/vote.png) 114 | 115 | 这种投票系统就是唯一计数器的一个例子。 116 | 117 | 以下是唯一计数器的基本 API : 118 | 119 | ``add(counter, obj)`` 将对象加入到计数器中,如果对象已经存在,返回 ``false`` ;添加成功返回 ``true`` 。 120 | 121 | ``remove(counter, obj)`` 从计数器中移除对象 ``obj`` ,如果 ``obj`` 并不是计数器的对象,那么返回 ``false`` ;移除成功返回 ``true`` 。 122 | 123 | ``is_member?(counter, obj)`` 检查 ``obj`` 是否已经存在于计数器。 124 | 125 | ``members(counter)`` 返回计数器包括的所有成员对象。 126 | 127 | ``count(counter)`` 返回计数器所有成员对象的数量。 128 | 129 | 唯一计数器的 API 和简单计数器的 API 有些不同,而且唯一计数器的底层是使用 Redis 的集合来实现的,它的完整定义如下: 130 | 131 | require 'redis' 132 | 133 | $redis = Redis.new 134 | 135 | def add(counter, member) 136 | return $redis.sadd(counter, member) 137 | end 138 | 139 | def remove(counter, member) 140 | return $redis.srem(counter, member) 141 | end 142 | 143 | def is_member?(counter ,member) 144 | return $redis.sismember(counter, member) 145 | end 146 | 147 | def members(counter) 148 | return $redis.members(counter) 149 | end 150 | 151 | def count(counter) 152 | return $redis.scard(counter) 153 | end 154 | 155 | 156 | ## 实例:提升/下沉投票 157 | 158 | 有了详细的 API 和实现之后,现在完成一个完整的提升/下沉投票实现了(有些网站也将这个功能称为有用/没用,或者喜欢/不喜欢)。 159 | 160 | 对于每个问题,我们使用两个唯一计数器,分别计算提升和下沉投票,并且在每次投票前检查唯一计数器,确保不会出现重复投票。 161 | 162 | 以下是投票系统的详细实现: 163 | 164 | load 'unique_counter.rb' 165 | 166 | def vote_up(question_id, user_id) 167 | if voted?(question_id, user_id) 168 | raise "alread voted" 169 | end 170 | return add("question-vote-up #{question_id}", user_id) 171 | end 172 | 173 | def vote_down(question_id, user_id) 174 | if voted?(question_id, user_id) 175 | raise "alread voted" 176 | end 177 | return add("question-vote-down #{question_id}", user_id) 178 | end 179 | 180 | def voted?(question_id, user_id) 181 | return (is_member?("question-vote-up #{question_id}", user_id) or \ 182 | is_member?("question-vote-down #{question_id}", user_id)) 183 | end 184 | 185 | def count_vote_up(question_id) 186 | return count("question-vote-up #{question_id}") 187 | end 188 | 189 | def count_vote_down(question_id) 190 | return count("question-vote-down #{question_id}") 191 | end 192 | 193 | 测试: 194 | 195 | irb(main):001:0> load 'vote_question.rb' 196 | => true 197 | irb(main):002:0> vote_up(10086, 123) # 投票 198 | => true 199 | irb(main):003:0> vote_up(10086, 456) 200 | => true 201 | irb(main):004:0> vote_down(10086, 789) 202 | => true 203 | irb(main):005:0> count_vote_up(10086) # 计数 204 | => 2 205 | irb(main):006:0> count_vote_down(10086) 206 | => 1 207 | irb(main):007:0> vote_up(10086, 123) # 不能重复投票 208 | RuntimeError: alread voted 209 | from vote_question.rb:5:in `vote_up' 210 | from (irb):7 211 | from /usr/bin/irb:12:in `
' 212 | -------------------------------------------------------------------------------- /usage/indexing_and_searching.md: -------------------------------------------------------------------------------- 1 | # 索引和搜索 2 | 3 | 4 | ## 输入补全 5 | 6 | TODO 7 | 8 | 9 | ## 关键字搜索 10 | 11 | TODO 12 | -------------------------------------------------------------------------------- /usage/limiter.md: -------------------------------------------------------------------------------- 1 | # 限速器 2 | 3 | TODO: 4 | 5 | ## 实例:阅览限速器 6 | 7 | 以下代码就强制某个用户在一分钟里最多只能访问 30 次图书页面: 8 | 9 | get '/book/*' do 10 | key = '#{user_id} book-page-view' 11 | pv = incr(key) 12 | if pv == 1 13 | # 首次访问,设置过期时间 14 | $redis.expire(key, 60) 15 | elsif pv > 30 16 | # 访问次数过多 17 | error_message('visit too much time') 18 | else 19 | # ... 正常显示页面 20 | end 21 | end 22 | 23 | 这个访问限制器并不完美,因为它带有一个竞争条件:客户端可能会因为失败而忘记设置过期时间,从而导致每个用户只能访问图书页面 30 次,这真的会非常糟糕! [INCR 命令的文档](http://redis.readthedocs.org/en/latest/string/incr.html) 详细地说明了如何构建一个正确的访问限制器。 24 | -------------------------------------------------------------------------------- /usage/lock.md: -------------------------------------------------------------------------------- 1 | # 锁 2 | 3 | 当多个请求争抢同一资源的情况出现时,就需要使用锁来进行访问控制。 4 | 5 | Redis 并没有直接提供锁操作原语,但是我们可以通过现有的命令来实现锁。 6 | 7 | 编写锁的实现必须非常小心,因为一个不起眼的缺陷就可能导致[竞争条件](http://en.wikipedia.org/wiki/Race_condition)发生,作为例子,稍后我们就会看到一些不正确的、或是带有竞争条件的锁实现。 8 | 9 | 除此之外,锁还需要一些强制机制,比如超时限制,以避免发生[死锁](http://en.wikipedia.org/wiki/Deadlock)。 10 | 11 | 12 | ## API 13 | 14 | 最简单的锁只有两个操作,一个是请求锁,另一个是释放锁,这两个操作的 API 如下: 15 | 16 | acquire(key, timeout, uid) 17 | 18 | release(key, uid) 19 | 20 | ``key`` 是锁的名称,一个安全的锁要确保在每个时间点上,只能有一个客户端持有名称为 ``key`` 的锁。 21 | 22 | ``timeout`` 决定了加锁的最长时间,当一个客户端持有锁超过 ``timeout`` 秒之后,这个锁就会自动释放。 23 | 24 | 在一些实现中, ``timeout`` 并不是必须的,客户端可以持有锁任意长的时间。 25 | 为了防止客户端在持有锁之后失败,从而导致死锁,这个实现强制客户端必须指定一个最长加锁时间。 26 | 27 | 另外要说明的是,虽然锁可以自动被释放,但客户端有时候也会需要手动释放锁。 28 | 举个例子,一个客户端可能申请了 10 秒钟的加锁时间,但是只用了 5 秒钟就完成了工作,那么它就可以调用 ``release`` 手动释放锁,把剩下的 5 秒加锁时间节约下来给下一个加锁客户端。这就是将 ``release`` 操作包含在 API 中的原因。 29 | 30 | ``uid`` 由客户端给出,在执行 ``release`` 时需要比对给定的 ``uid`` 是否就是加锁客户端的 ``uid`` ,从而实现身份验证,确保只有持有锁的客户端可以释放锁。 31 | 32 | 33 | ## 实现 34 | 35 | 初看上去, ``acquire`` 操作似乎可以用 [SETEX](http://redis.readthedocs.org/en/latest/string/setex.html) 命令实现,调用 ``acquire(key, timeout, uid)`` 等同于执行: 36 | 37 | SETEX name timeout uid 38 | 39 | 这种实现的问题是, ``SETEX`` 命令会直接覆盖已有的值,因此多个客户端的加锁请求会互相覆盖,所以这个实现是不安全的。 40 | 41 | 另一种可能的办法是,组合使用 [SETNX](http://redis.readthedocs.org/en/latest/string/setnx.html) 命令和 [EXPIRE](http://redis.readthedocs.org/en/latest/key/expire.html) 命令,其中 ``SETNX`` 命令用于加锁,它的返回值决定了客户端是否成功获取锁,如果锁获取成功,就用 ``EXPIRE`` 命令给锁加上超时时间: 42 | 43 | if SETNX key uid == "OK" 44 | EXPIRE key timeout 45 | end 46 | 47 | 不幸的是,这种实现也不安全:因为在 ``SETNX`` 执行之后、 ``EXPIRE`` 命令执行之前的这段时间内,客户端可能会失败,造成 ``EXPIRE`` 命令没办法执行,从而形成死锁。 48 | 49 | 解决问题的关键是,让 ``acquire`` (当然还有 ``release`` )成为一个[原子操作](http://en.wikipedia.org/wiki/Atomic_operation),可以用事务或脚本两种方法来实现这一点,以下两个小节分别讲解这两种实现方式。 50 | 51 | 52 | ## 事务实现 53 | 54 | 这个实现通过使用事务和 [WATCH](http://redis.readthedocs.org/en/latest/transaction/watch.html) 命令来保证 ``acquire`` 和 ``release`` 的原子性。 55 | 56 | ``acquire`` 操作定义如下: 57 | 58 | # coding: utf-8 59 | 60 | require "redis" 61 | 62 | $redis = Redis.new 63 | 64 | def acquire(key, timeout, uid) 65 | 66 | $redis.watch(key) 67 | 68 | # 已经被其他客户端加锁? 69 | if $redis.exists(key) 70 | $redis.unwatch 71 | return false 72 | end 73 | 74 | # 尝试加锁 75 | result = $redis.multi do |t| 76 | t.setex(key, timeout, uid) 77 | end 78 | 79 | # 加锁成功? 80 | return result != nil 81 | 82 | end 83 | 84 | 函数首先使用 ``WATCH`` 命令监视 ``key`` ,然后查看这个 ``key`` 是否已经有值,也即是,这个锁是否已经被其他客户端加锁。 85 | 86 | 如果 ``key`` 没有值的话,它就执行一个事务,事务里使用 ``SETEX`` 命令设置锁的 ``uid`` 和最长加锁时间 ``timeout`` 。 87 | 88 | ``WATCH`` 命令的效果保证,如果在 ``WATCH`` 执行之后、事务执行之前,有其他别的客户端修改了 ``key`` 的话,那么这个客户端的事务执行就会失败。 89 | 90 | Ruby 客户端通过返回 ``nil`` 来表示事务失败,因此函数的最后通过验证事务结果是否不为 ``nil`` ,来判断加锁是否成功。 91 | 92 | ``release`` 函数的定义和 ``acquire`` 函数类似,也同样使用了事务和 ``WATCH`` 命令来保证原子性,并且它在解锁之前,会先验证 ``key`` 的值(也即是 ``uid`` ),确保只有持有锁的客户端可以释放锁: 93 | 94 | def release(key, uid) 95 | 96 | $redis.watch(key) 97 | 98 | # 锁不存在或已经释放? 99 | if $redis.exists(key) == false 100 | $redis.unwatch 101 | return true 102 | end 103 | 104 | # 比对 uid ,如果匹配就删除 key 105 | if uid == $redis.get(key) 106 | result = $redis.multi do |t| 107 | t.del(key) 108 | end 109 | # 删除成功? 110 | return result != nil 111 | else 112 | return false 113 | end 114 | 115 | end 116 | 117 | 测试: 118 | 119 | irb(main):001:0> load 'lock_transaction_implement.rb' 120 | => true 121 | irb(main):002:0> acquire('lock', 10086, 'moto') 122 | => true 123 | irb(main):003:0> acquire('lock', 123, 'nokia') # 加锁失败 124 | => false 125 | irb(main):004:0> release('lock', 'nokia') # 释放锁失败, uid 不匹配 126 | => false 127 | irb(main):005:0> release('lock', 'moto') 128 | => true 129 | 130 | 131 | ## 脚本实现 132 | 133 | 在 2.6 或以上版本的 Redis 中,更好的实现锁的办法是使用 Lua 脚本:Redis 确保 [EVAL](http://redis.readthedocs.org/en/latest/script/eval.html) 命令执行的脚本总是原子性的,因此我们可以直接在 Lua 脚本里执行加锁和释放锁的操作,而不必担心任何竞争条件,只要考虑脚本程序的正确性就可以了。 134 | 135 | 以下是这一实现的完整源码: 136 | 137 | # coding: utf-8 138 | 139 | require "redis" 140 | 141 | $redis = Redis.new 142 | 143 | def acquire(key, timeout, uid) 144 | 145 | script = " 146 | if redis.call('exists', KEYS[1]) == 0 then 147 | return redis.call('setex', KEYS[1], ARGV[1], ARGV[2]) 148 | end 149 | " 150 | 151 | return "OK" == $redis.eval(script, :keys => [key], :argv => [timeout, uid]) 152 | 153 | end 154 | 155 | def release(key, uid) 156 | 157 | script = " 158 | if redis.call('get', KEYS[1]) == ARGV[1] then 159 | return redis.call('del', KEYS[1]) 160 | end 161 | " 162 | 163 | return 1 == $redis.eval(script, :keys => [key], :argv => [uid]) 164 | 165 | end 166 | 167 | 注意这里 ``Redis.eval`` 方法的 API 和 Redis ``EVAL`` 命令稍有不同, ``EVAL`` 命令要求给出 ``keys`` 参数的个数,而 ``Redis.eval`` 方法会在执行时,通过计算 ``keys`` 数组的长度,自动将这个参数加上,因此不必显式地指定 ``keys`` 参数的个数。 168 | 169 | 测试: 170 | 171 | irb(main):001:0> load 'lock_scripting_implement.rb' 172 | => true 173 | irb(main):002:0> acquire('lock', 10086, 'moto') 174 | => true 175 | irb(main):003:0> acquire('lock', 123, 'nokia') 176 | => false 177 | irb(main):004:0> release('lock', 'nokia') 178 | => false 179 | irb(main):005:0> release('lock', 'moto') 180 | => true 181 | 182 | 183 | ## 两种实现的对比 184 | 185 | 事务实现的好处是,它可以用在 2.2 或以上版本的 Redis 里,缺点是事务和 ``WATCH`` 的处理非常复杂,容易出错。 186 | 187 | 脚本实现只能运行在 Redis 2.6 或以上的版本,但脚本实现相比事务实现要简单得多。 188 | 189 | 目前两种实现都可以在最新的 Redis 2.6 和 Redis 3.0 上使用,但根据 [Redis 官网上的介绍](http://redis.io/topics/transactions),未来可能会去掉事务功能,只保留脚本。 190 | 191 | 因此,如果没有其他别的原因,请优先使用脚本实现。 192 | 193 | 194 | ## 参考资料 195 | 196 | [维基百科 Lock(computer science) 词条](http://en.wikipedia.org/wiki/Lock_\(computer_science\)) 197 | -------------------------------------------------------------------------------- /usage/log.md: -------------------------------------------------------------------------------- 1 | # 日志 2 | 3 | 一般来说,操作系统都会提供详细的日志功能,不过在一些情况下,也可以考虑将日志保存到 Redis 当中: 4 | 5 | - 你需要将多个机器的日志集中保存到一个日志服务器中 6 | 7 | - 你需要借助 Redis 提供的数据结构来对日志内容进行操作,查看或者用于数据分析 8 | 9 | 有至少三种的方法在 Redis 中实现日志功能,它们的主要功能都一样,但也有各自不同的特色,以下几个小节就会分别介绍这些实现。 10 | 11 | 12 | ## API 13 | 14 | 一个日志系统应该(至少)包含以下几个基本操作: 15 | 16 | ``write(category, content)`` 写分类 ``category`` 的新日志,内容为 ``content`` 。 17 | 18 | ``read(category, n)`` 返回 ``category`` 分类的第 n 条日志, ``n`` 以 ``0`` 为开始。 19 | 20 | ``read_all(category)`` 返回 ``category`` 分类的所有日志。 21 | 22 | ``count(category)`` 返回 ``category`` 分类的日志数量。 23 | 24 | ``flush(category)`` 清空所有分类为 ``category`` 的日志。 25 | 26 | 27 | ## 定长日志 28 | 29 | 定长日志的想法来自 Redis 的 [APPEND 命令文档](http://redis.readthedocs.org/en/latest/string/append.html):它将数据保存在一个字符串中,新日志通过 ``APPEND`` 命令追加到字符串的最后,因为日志的长度是固定的,所以给定一个日志号码 ``n`` ,可以根据 ``n`` 和日志长度来算出日志在字符串中的起始索引和结尾索引,然后用 [GETRANGE](http://redis.readthedocs.org/en/latest/string/getrange.html) 命令取出日志的内容。 30 | 31 | 以下是一个保存年份的定长日志定义,它假设所有日志的长度都为 ``4`` : 32 | 33 | require 'redis' 34 | 35 | LENGTH = 4 36 | 37 | $redis = Redis.new 38 | 39 | def write(category, content) 40 | raise "Content's length must equal to #{LENGTH}" unless content.length == LENGTH 41 | return $redis.append(category, content) 42 | end 43 | 44 | def read(category, n) 45 | return $redis.getrange(category, n*LENGTH, (n+1)*LENGTH-1) 46 | end 47 | 48 | def read_all(category) 49 | all_log = $redis.get(category) 50 | total_log_length = count(category) 51 | 52 | arr = Array.new 53 | 0.upto(total_log_length-1) do |i| 54 | arr << all_log[i*LENGTH ... (i+1)*LENGTH] 55 | end 56 | 57 | return arr 58 | end 59 | 60 | def count(category) 61 | total_log_length = $redis.strlen(category) 62 | if total_log_length == 0 63 | return 0 64 | else 65 | return total_log_length / LENGTH 66 | end 67 | end 68 | 69 | def flush(category) 70 | return $redis.del(category) 71 | end 72 | 73 | 测试: 74 | 75 | irb(main):001:0> load 'fixed_size_log.rb' 76 | => true 77 | irb(main):002:0> write('year-log', '2012') 78 | => 4 79 | irb(main):003:0> write('year-log', '2015') 80 | => 8 81 | irb(main):004:0> write('year-log', '123456789') # 长度必须符合要求 82 | RuntimeError: Content's length must equal to 4 83 | from fixed_size_log.rb:8:in `write' 84 | from (irb):4 85 | from /usr/bin/irb:12:in `
' 86 | irb(main):005:0> read('year-log', 0) 87 | => "2012" 88 | irb(main):006:0> read('year-log', 1) 89 | => "2015" 90 | irb(main):007:0> read_all('year-log') 91 | => ["2012", "2015"] 92 | irb(main):008:0> count('year-log') 93 | => 2 94 | irb(main):009:0> flush('year-log') 95 | => 1 96 | irb(main):010:0> count('year-log') 97 | => 0 98 | 99 | 100 | ## 列表日志 101 | 102 | 列表日志的功能和定长日志差不多,它和定长日志的主要区别有以下两个: 103 | 104 | 1. 列表日志将日志内容保存在列表中,通过 [RPUSH](http://redis.readthedocs.org/en/latest/list/rpush.html) 命令添加日志、 [LINDEX](http://redis.readthedocs.org/en/latest/list/lindex.html) 命令和 [LRANGE](http://redis.readthedocs.org/en/latest/list/lrange.html) 命令读取日志。 105 | 106 | 2. 列表日志不对日志长度进行要求。 107 | 108 | 列表日志的定义如下: 109 | 110 | 111 | require 'redis' 112 | 113 | $redis = Redis.new 114 | 115 | def write(category, content) 116 | return $redis.rpush(category, content) 117 | end 118 | 119 | def read(category, n) 120 | return $redis.lindex(category, n) 121 | end 122 | 123 | def read_all(category) 124 | return $redis.lrange(category, 0, -1) 125 | end 126 | 127 | def count(category) 128 | return $redis.llen(category) 129 | end 130 | 131 | def flush(category) 132 | return $redis.del(category) 133 | end 134 | 135 | 测试: 136 | 137 | irb(main):001:0> load 'list_log.rb' 138 | => true 139 | irb(main):002:0> write('greet-log', 'good morning!') 140 | => 1 141 | irb(main):003:0> write('greet-log', 'hello world!') 142 | => 2 143 | irb(main):004:0> write('greet-log', 'moto moto!') 144 | => 3 145 | irb(main):005:0> read('greet-log', 2) 146 | => "moto moto!" 147 | irb(main):006:0> read_all('greet-log') 148 | => ["good morning!", "hello world!", "moto moto!"] 149 | irb(main):007:0> count('greet-log') 150 | => 3 151 | irb(main):008:0> flush('greet-log') 152 | => 1 153 | irb(main):009:0> count('greet-log') 154 | => 0 155 | 156 | 157 | ## 时间日志 158 | 159 | 时间日志保存在 Redis 的有序集合中,它将内容和时间信息一起保存在日志里,通过 [ZADD](http://redis.readthedocs.org/en/latest/sorted_set/zadd.html) 、 [ZRANGE](http://redis.readthedocs.org/en/latest/sorted_set/zrange.html) 、 [ZCARD](http://redis.readthedocs.org/en/latest/sorted_set/zcard.html) 等命令进行操作: 160 | 161 | require 'redis' 162 | 163 | $redis = Redis.new 164 | 165 | def write(category, content) 166 | return $redis.zadd(category, Time.now.to_f, content) 167 | end 168 | 169 | def read(category, n) 170 | return $redis.zrange(category, n, n, :with_scores => true) 171 | end 172 | 173 | def read_all(category) 174 | return $redis.zrange(category, 0, -1, :with_scores => true) 175 | end 176 | 177 | def count(category) 178 | return $redis.zcard(category) 179 | end 180 | 181 | def flush(category) 182 | return $redis.del(category) 183 | end 184 | 185 | 以下代码段模拟了一次服务器从链接失败到下线的过程,每个事件发生时的详细时间都被记录了下来: 186 | 187 | irb(main):001:0> load 'time_log.rb' 188 | => true 189 | irb(main):002:0> write('server-log', 'db connect fail') 190 | => true 191 | irb(main):003:0> write('server-log', 'db reconnect fail') 192 | => true 193 | irb(main):004:0> write('server-log', 'db server down') 194 | => true 195 | irb(main):005:0> read('server-log', 0) 196 | => ["db connect fail", 1344786364.5974884] 197 | irb(main):006:0> read_all('server-log') 198 | => [["db connect fail", 1344786364.5974884], ["db reconnect fail", 1344786375.6293638], ["db server down", 1344786389.518898]] 199 | irb(main):007:0> count('server-log') 200 | => 3 201 | irb(main):008:0> flush('server-log') 202 | => 1 203 | irb(main):009:0> count('server-log') 204 | => 0 205 | 206 | 207 | ## 实例:时间线 208 | 209 | 日志除了应用在系统的内部之外,还可在程序外部作为一个单独的功能来使用。 210 | 211 | 以时间日志为例子,很多网站都有自己的功能更新记录,这些记录通常有以下的形式: 212 | 213 | ... 214 | 2012 年 9 月 11 日 添加评论功能 215 | 2012 年 8 月 26 日 添加用户上传功能 216 | 2012 年 8 月 15 日 添加 wiki 功能 217 | 2012 年 8 月 10 日 网站上线 218 | 219 | 还有一种被称为时间线的功能,它和上面的功能记录一样,只是时间线通常用在记录用户信息而不是网站信息,比如: 220 | 221 | ... 222 | 2012 年 8 月 12 日 21 时 你写了文章《Redis 源码分析(1)》 223 | 2012 年 8 月 11 日 20 时 你关注了 @peter 224 | 2012 年 8 月 11 日 18 时 @peter 关注了你 225 | 2012 年 8 月 10 日 13 时 你注册了网站 226 | 227 | 以上两种功能的写入和读取操作,都可以使用前面给出的时间日志实现来处理。除此之外,我们还要添加一个新的函数,用于读出最新的 ``n`` 条信息: 228 | 229 | load 'time_log.rb' 230 | 231 | def recent(category, n) 232 | return $redis.zrevrange(category, 0, n-1, :with_scores => true) 233 | end 234 | 235 | 以下是一个时间线实例: 236 | 237 | irb(main):001:0> load 'timeline.rb' 238 | => true 239 | irb(main):002:0> write('user-timeline', 'register') # 写入 240 | => true 241 | irb(main):003:0> write('user-timeline', '@peter following you') 242 | => true 243 | irb(main):004:0> write('user-timeline', 'you following @peter') 244 | => true 245 | irb(main):005:0> write('user-timeline', 'you write new post Redis code analysis (1)') 246 | => true 247 | irb(main):006:0> write('user-timeline', '@peter comment on post Redis code analysis (1)') 248 | => true 249 | irb(main):007:0> write('user-timeline', '@tom comment on post Redis code analysis (1)') 250 | => true 251 | irb(main):008:0> recent('user-timeline', 5).each {|msg| p msg} # 读取并打印最新 5 条时间线信息 252 | ["@tom comment on post Redis code analysis (1)", 1344850781.377236] 253 | ["@peter comment on post Redis code analysis (1)", 1344850767.113229] 254 | ["you write new post Redis code analysis (1)", 1344850732.5523758] 255 | ["you following @peter", 1344850695.8571677] 256 | ["@peter following you", 1344850683.2889433] 257 | => [["@tom comment on post Redis code analysis (1)", 1344850781.377236], ["@peter comment on post Redis code analysis (1)", 1344850767.113229], ["you write new post Redis code analysis (1)", 1344850732.5523758], ["you following @peter", 1344850695.8571677], ["@peter following you", 1344850683.2889433]] 258 | 259 | 260 | ## 多种日志实现之间的对比 261 | 262 | 在前面介绍的三种日志实现中,只有时间日志可以直接存储时间信息,其他两种日志需要通过编码/解码(parse、JSON等手段)来对时间信息进行支持。 263 | 264 | 另外一个需要注意的地方是,时间日志(也即是有序集合)里面不可以保存同样的值,同样内容的日记会互相覆盖, Redis 只会更新时间值: 265 | 266 | irb(main):001:0> load "time_log.rb" 267 | => true 268 | irb(main):002:0> write("log", "server fail") 269 | => true 270 | irb(main):003:0> read_all("log") 271 | => [["server fail", 1345568960.594956]] 272 | irb(main):004:0> write("log", "server fail") # 写入内容相同的日志 273 | => false 274 | irb(main):005:0> read_all("log") 275 | => [["server fail", 1345568974.2670465]] # 时间被更新 276 | 277 | 定长日志将所有内容都塞进一个字符串里面,所以定长日志最快,且最节省内存。不过 Redis 的字符串不提供截断(tirm)功能,因此对定长日志的部分删除操作没有其他两种日志来得方便。 278 | 279 | 定长日志和列表日志的功能基本相同,使用哪一个取决于日志内容的长度是否固定。 280 | -------------------------------------------------------------------------------- /usage/message_passing.md: -------------------------------------------------------------------------------- 1 | # 消息传递 2 | 3 | 4 | ## 消息传递的分类 5 | 6 | TODO: 介绍列表和订阅/发布两种实现方式。 7 | 8 | 9 | ### 持久化的和易失的 10 | 11 | 持久化消息保证除非被删除或者被取出,否则它们会一直保存在缓存区当中。 12 | 13 | 而易失消息在发送时,如果没有接收者等待这个消息,那么这个消息将被丢弃。 14 | 15 | 用 Redis 列表保存的信息总是持久化的,而使用 [PUBLISH](http://redis.readthedocs.org/en/latest/pub_sub/publish.html) 命令发送的信息则总是易失的。 16 | 17 | 18 | ### 阻塞与不阻塞 19 | 20 | 对于 Redis 的列表结构来说,接收信息是否阻塞取决于所使用的弹出原语: [LPOP](http://redis.readthedocs.org/en/latest/list/lpop.html) 和 [RPOP](http://redis.readthedocs.org/en/latest/list/rpop.html) 在取出消息时不阻塞,如果列表为空,它们就返回 ``nil`` 。 21 | 22 | 另一方面,如果使用 [BLPOP](http://redis.readthedocs.org/en/latest/list/blpop.html) 或者 [BRPOP](http://redis.readthedocs.org/en/latest/list/brpop.html) ,那么在列表为空时,阻塞直到有信息可弹出,或者等待超时为止。 23 | 24 | 对于发布/订阅机制来说, [SUBSCRIBE](http://redis.readthedocs.org/en/latest/pub_sub/subscribe.html) 命令和 [PSUBSCRIBE](http://redis.readthedocs.org/en/latest/pub_sub/psubscribe.html) 是否阻塞主要取决于所使用的驱动:比如 Ruby 的驱动 [redis-rb](https://github.com/redis/redis-rb) 在执行订阅时总是阻塞的,需要通过在 BLOCK 里设置 UNSUBSCRIBE 条件来退出。而 Python 的驱动 [redis-py](https://github.com/andymccurdy/redis-py) 则将所有消息保存到一个迭代器中,如果试图使用 ``next`` 从空迭代器中取出信息,进程就会被阻塞,但使用 ``redis.pubsub().listen()`` 进行订阅总是不阻塞的。 25 | 26 | 为了讨论的方便起见,我们假设订阅总是阻塞的。 27 | 28 | 29 | ### 一对一 30 | 31 | TODO 32 | 33 | 34 | ### 一对多 35 | 36 | TODO 37 | 38 | 39 | ### 多对多 40 | 41 | TODO 42 | 43 | 44 | ## 实例 45 | 46 | TODO 47 | 48 | 49 | ## 相关资料 50 | 51 | [http://en.wikipedia.org/wiki/Message_(computer_science)](http://en.wikipedia.org/wiki/Message_\(computer_science\)) 52 | -------------------------------------------------------------------------------- /usage/relation.md: -------------------------------------------------------------------------------- 1 | # 好友关系 2 | 3 | 好友关系是社交类网站最常见也是最重要的功能之一。 4 | 5 | 作为例子,以下是 twitter 网站上的好友关系截图,它显示了当前用户的关注数量、被关注数量,以及正在关注的用户: 6 | 7 | ![twitter好友关系实例图片](https://raw.github.com/redisbook/book/master/image/usage/twitter_relation.png) 8 | 9 | 文章接下来的部分会介绍如何实现这种的好友系统。 10 | 11 | 12 | ## API 13 | 14 | 一个基本的好友关系功能应该具有以下操作: 15 | 16 | ``follow(my_id, target_id)`` 关注给定用户。 17 | 18 | ``unfollow(my_id, target_id)`` 取消对给定用户的关注。 19 | 20 | ``following(my_id)`` 返回所有被我关注的人。 21 | 22 | ``count_following(my_id)`` 返回我关注的人数。 23 | 24 | ``follower(my_id)`` 返回所有关注我的人。 25 | 26 | ``count_follower(my_id)`` 返回关注我的人数。 27 | 28 | 另外还有两个谓词,用于检查两个用户之间的一对一关系: 29 | 30 | ``is_following?(my_id, target_id)`` 我是否正在关注给定用户? 31 | 32 | ``have_follower?(my_id, target_id)`` 给定用户是否正在关注我? 33 | 34 | 35 | ## 实现 36 | 37 | 好友关系可以通过对每个用户使用 ``following`` 和 ``follower`` 两个集合来构建: ``following`` 集合用于保存当前用户正在关注的人, ``follower`` 保存正在关注当前用户的人。 38 | 39 | 当一个用户对另一个用户进行 ``follow`` 动作的时候,程序将两个用户添加到彼此的集合中,而其他关系操作就通过对两个用户的集合进行处理来实现: 40 | 41 | require 'redis' 42 | 43 | $redis = Redis.new 44 | 45 | def follow(my_id, target_id) 46 | # 将目标添加到我的 following 集合里 47 | following_status = $redis.sadd(following_key(my_id), target_id) 48 | 49 | # 将我加到目标的 follower 集合里 50 | follower_status = $redis.sadd(follower_key(target_id), my_id) 51 | 52 | # 返回状态 53 | return following_status && follower_status 54 | end 55 | 56 | def unfollow(my_id, target_id) 57 | # 将目标从我的 following 集合中移除 58 | following_status = $redis.srem(following_key(my_id), target_id) 59 | 60 | # 将我从目标的 follower 集合中移除 61 | follower_status = $redis.srem(follower_key(target_id), my_id) 62 | 63 | # 返回状态 64 | return following_status && follower_status 65 | end 66 | 67 | 68 | # 关注 69 | 70 | def following(my_id) 71 | return $redis.smembers(following_key(my_id)) 72 | end 73 | 74 | def count_following(my_id) 75 | return $redis.scard(following_key(my_id)) 76 | end 77 | 78 | 79 | # 被关注 80 | 81 | def follower(my_id) 82 | return $redis.smembers(follower_key(my_id)) 83 | end 84 | 85 | def count_follower(my_id) 86 | return $redis.scard(follower_key(my_id)) 87 | end 88 | 89 | 90 | # 谓词 91 | 92 | def is_following?(my_id, target_id) 93 | return $redis.sismember(following_key(my_id), target_id) 94 | end 95 | 96 | def have_follower?(my_id, target_id) 97 | return is_following?(target_id, my_id) 98 | end 99 | 100 | 101 | # 辅助函数 102 | 103 | def following_key(id) 104 | return "#{id}::following" 105 | end 106 | 107 | def follower_key(id) 108 | return "#{id}::follower" 109 | end 110 | 111 | 测试: 112 | 113 | [huangz@mypad]$ irb 114 | irb(main):001:0> load 'relation.rb' 115 | => true 116 | irb(main):002:0> peter = 'user::10086' # 用户 id 117 | => "user::10086" 118 | irb(main):003:0> jack = 'user::123123' 119 | => "user::123123" 120 | irb(main):004:0> follow(peter, jack) # 关注和被关注 121 | => true 122 | irb(main):005:0> following(peter) 123 | => ["user::123123"] 124 | irb(main):006:0> follower(jack) 125 | => ["user::10086"] 126 | irb(main):007:0> is_following?(peter, jack) # 谓词 127 | => true 128 | irb(main):008:0> have_follower?(jack, peter) 129 | => true 130 | irb(main):009:0> count_following(peter) # 数量 131 | => 1 132 | irb(main):010:0> count_follower(jack) 133 | => 1 134 | 135 | 136 | ## 扩展:好友推荐 137 | 138 | 除了处理已有的好友关系外,关系系统通常还会为用户推荐一些他/她可能感兴趣的人。 139 | 140 | 举个例子,以下是 twitter 的好友推荐功能: 141 | 142 | ![twitter好友推荐示例图](https://raw.github.com/redisbook/book/master/image/usage/twitter_recommend.png) 143 | 144 | 使用 Redis 的集合操作,我们可以在好友关系实现的基础上,提供简单的好友推荐功能。 145 | 146 | 比如说,可以在用户 A 关注用户 B 之后,对用户 B 的 ``following`` 集合和用户 A 的 ``following`` 集合做一个差集操作,然后将结果推荐给用户 A ,鼓励他/她继续发现有趣的人。 147 | 148 | 以下是这一简单推荐系统的实现代码: 149 | 150 | def recommend(my_id, target_id) 151 | return $redis.sdiff(following_key(target_id), following_key(my_id)) 152 | end 153 | 154 | 测试: 155 | 156 | irb(main):001:0> load 'relation.rb' 157 | => true 158 | irb(main):002:0> peter = 'user::10086' 159 | => "user::10086" 160 | irb(main):003:0> jack = 'user::123123' 161 | => "user::123123" 162 | irb(main):004:0> mary = 'user::12590' 163 | => "user::12590" 164 | irb(main):005:0> tom = 'user::228229' 165 | => "user::228229" 166 | irb(main):006:0> follow(peter, jack) # peter 关注 jack 和 mary 167 | => true 168 | irb(main):007:0> follow(peter, mary) 169 | => true 170 | irb(main):008:0> follow(tom, peter) # tom 关注 peter 171 | => true 172 | irb(main):009:0> recommend(tom, peter) 173 | => ["user::123123", "user::12590"] # 将 peter 正在关注的 jack 和 mary 推荐给 tom 174 | 175 | 更进一步的好友推荐功能可以通过对用户关系的集合进行数据挖掘来实现。 176 | -------------------------------------------------------------------------------- /usage/rq_project/rq_project_analysis.md: -------------------------------------------------------------------------------- 1 | # RQ 项目分析 2 | 3 | RQ(http://python-rq.org/) 是使用 Python 编写, Redis 作为后端的一个消息队列库。 4 | 5 | 6 | ## 用例 7 | 8 | 来自官网: 9 | 10 | # job 11 | import requests 12 | 13 | def count_words_at_url(url): 14 | resp = requests.get(url) 15 | return len(resp.text.split()) 16 | 17 | # queue 18 | from rq import Queue, use_connection 19 | use_connection() 20 | q = Queue() 21 | 22 | # work 23 | from my_module import count_words_at_url 24 | result = q.enqueue(count_words_at_url, 'http://nvie.com') 25 | 26 | 27 | ## 数据结构 28 | 29 | 30 | ### 创建 Job 实例 31 | 32 | 每个传入队列的任务都用一个 ``Job`` 类实例来表示。 33 | 34 | ``Job`` 类保存了任务的 ``id`` 、所属队列、函数名称、函数的参数、执行结果、执行统计信息等等。 35 | 36 | ``Job`` 实例的所有信息都保存在一个 Redis 哈希表中,键名格式为 ``rq:job:`` , ``uid`` 一般使用 ``uuid.uuid4`` 函数来生成,也可以显式地指定。 37 | 38 | 以下是一个 ``Job.id`` 例子: ``rq:job:55528e58-9cac-4e05-b444-8eded32e76a1`` 。 39 | 40 | 41 | ### 执行 Job 任务 42 | 43 | 当需要执行任务时, ``Job.fetch`` 类方法会根据 ``id`` 值,将保存在 Redis 哈希表中的数据都取回来,并返回一个保存了这些数据的 ``Job`` 实例。 44 | 45 | ``Job`` 实例的 ``func_name`` 属性保存了执行任务所需的函数名,其实说『函数名』并不太正确,因为这个 ``func_name`` 属性既可以指向一个方法,也可以指向一个函数。 46 | 47 | 根据 ``func_name`` 属性, ``Job`` 实例可以通过 ``func`` 方法找到执行任务所需的函数(或方法)。 48 | 49 | ``Job`` 实例通过调用 ``perform`` 方法,将给定参数传给给定函数,从而执行任务。 50 | 51 | 52 | ### Queue 实例 53 | 54 | ``Queue`` 类负责保存和处理任务,它使用一个 Redis 列表保存队列中所有任务的 ``id`` 值。 55 | 56 | 每个 ``Queue`` 实例都使用 ``key`` 属性的值作为列表的键,键名格式为 ``rq:queue:`` , ``name`` 属性可以显式地指定,也可以使用默认值 ``'default'`` 。 57 | 58 | ``Queue`` 以先进先出([FIFO](http://en.wikipedia.org/wiki/FIFO))的方式处理任务: 59 | 它使用 ``rpush`` 命令将任务 ``id`` 放进 Redis 列表; 60 | 而 ``lpop`` 和 ``blpop`` 命令则负责将任务 ``id`` 从列表中取出。 61 | 62 | 63 | ### Worker 实例 64 | 65 | ``Worker`` 负责执行任务,它可以接受一个 ``Queue`` 类实例,或者一个包含 ``Queue`` 类实例的 Python 列表作为 ``queues`` 参数。 66 | 67 | ``Worker`` 每次从队列中弹出一个 ``Job`` 实例,并派生出一个子进程来执行任务,父进程会一直等待到任务结束,或者任务执行超时。 68 | 69 | 如果任务执行成功,并且执行任务的函数的返回值不为 ``None`` ,那么将这个返回值设置给 ``Job.result`` 属性。 70 | 71 | 如果任务执行失败,那么将任务添加到 ``FailedQueue`` 队列中,等待将来重试。 72 | 73 | 整个 ``Worker`` 执行过程可以用下图简单表示: 74 | 75 | ![Worker执行流程图](https://raw.github.com/redisbook/book/4a5f20061822f00f0801060a2df64b28b5ebebab/rq_project/rq_worker.png) 76 | 77 | 78 | ### FailedQueue 实例 79 | 80 | ``FailedQueue`` 继承自 ``Queue`` 类,它主要增加了 ``quarantine`` 和 ``requeue`` 两个方法。 81 | 82 | ``quarantine`` 方法将执行失败的 ``Job`` 实例加进执行失败的队列中(默认队列名为 ``'failed'``)。 83 | 84 | ``requeue`` 则将 ``Job`` 实例重新放回到原本执行它的那个队列中去,等待下一次重新执行。 85 | -------------------------------------------------------------------------------- /usage/rq_project/rq_worker.dot: -------------------------------------------------------------------------------- 1 | digraph worker { 2 | start_worker [label="Worker.work()\n启动 worker"]; 3 | get_job_from_queue [label="Queue.dequeue_any(...)\n取出任务"]; 4 | perform_job [label="派生出一个子进程来执行任务"]; 5 | 6 | job_perform_success [label="如果任务返回值不为 None\n将它赋值给 job.result 属性\n否则删除这个任务"]; 7 | job_perform_fail [label="将任务放进 FailedQueue"]; 8 | 9 | iter [label="循环"]; 10 | 11 | 12 | start_worker -> get_job_from_queue; 13 | get_job_from_queue -> perform_job; 14 | 15 | perform_job -> job_perform_success [label="执行成功"]; 16 | perform_job -> job_perform_fail [label="执行失败"]; 17 | 18 | job_perform_success -> iter; 19 | job_perform_fail -> iter; 20 | 21 | iter -> get_job_from_queue [label="处理下个任务"]; 22 | } 23 | -------------------------------------------------------------------------------- /usage/rq_project/rq_worker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redisbook/book/9c664d39c4abdf8b192628fbb2441e71cb37d69d/usage/rq_project/rq_worker.png -------------------------------------------------------------------------------- /usage/semaphore.md: -------------------------------------------------------------------------------- 1 | # 信号量 2 | 3 | 信号量用于限制同一资源可以被同时访问的数量。 4 | 5 | 它的基本操作有以下三个: 6 | 7 | ``init(name, size)`` 初始化信号量。 8 | 9 | 其中 ``name`` 参数为信号量的名字,而 ``size`` 则用于指定信号量的数量。 10 | 11 | ``acquire(name, timeout)`` 获取一个信号量。 12 | 13 | 如果获取成功,那么返回一个非空元素;如果获取失败,则返回一个 ``nil`` 。 14 | 15 | 其中 ``timeout`` 参数用于指定最长阻塞时间:如果调用 ``acquire`` 时没有信号量可用,那么就一直阻塞直到有信号量可用,或者 ``timeout`` 超时为止。默认 ``timeout`` 为 ``0`` ,也即是永远阻塞。 16 | 17 | ``release(name)`` 释放一个信号量。 18 | 19 | 客户端必须保证,只有获取了信号量的客户端能调用这个函数,否则会产生错误。 20 | 21 | 22 | ## 实现 23 | 24 | 信号量可以通过 Redis 的列表结构来实现: 25 | 26 | 当调用 ``init`` 函数初始化信号量时,我们将 ``size`` 数量的元素推入 key 为 ``name`` 的列表。 27 | 28 | 列表元素的内容不影响实现,只要不是 ``nil`` 就可以:这个实现将 ``size`` 个 ``name`` 字符串推入列表。如果担心字符串占用太多内存的话,也可以使用数字来代替,比如 ``1`` 。 29 | 30 | require "redis" 31 | 32 | $redis = Redis.new 33 | 34 | def init(name, size) 35 | item = [] << name 36 | all_item = item * size 37 | $redis.lpush(name, all_item) 38 | end 39 | 40 | ``acquire`` 函数可以使用 [BLPOP](http://redis.readthedocs.org/en/latest/list/blpop.html) 或者 [BRPOP](http://redis.readthedocs.org/en/latest/list/brpop.html) 实现。 41 | 42 | 通过这两个弹出命令的其中一个,函数对 key 为 ``name`` 的列表进行检查:如果列表不为空,表示有信号量可用;如果列表为空,那么说明暂时没有信号量可用。客户端阻塞直到 ``timeout`` 超时。 43 | 44 | def acquire(name, timeout=0) 45 | $redis.blpop(name, timeout) 46 | end 47 | 48 | ``release`` 函数将一个元素推入 key 为 ``name`` 的列表,从而释放一个信号量。 49 | 50 | 注意这个实现并没有对客户端进行任何检查,也即是,函数只管推入元素,不管这个客户端是否真的获取过信号量。这也是前面列出 API 时,要求调用者必须自己进行检查的原因。 51 | 52 | def release(name) 53 | $redis.lpush(name, name) 54 | end 55 | 56 | 57 | ## 参考资料 58 | 59 | [维基百科 Semaphore 词条](http://en.wikipedia.org/wiki/Semaphore_\(programming\)) 60 | -------------------------------------------------------------------------------- /usage/sorting.md: -------------------------------------------------------------------------------- 1 | # 排序 2 | 3 | 4 | ## 实现 5 | 6 | TODO 7 | 8 | ### 1) 有序集合 9 | 10 | ### 2) SORT key 命令 11 | 12 | ### 实现区别 13 | 14 | 有序集为各种排序数据分别保存一个 key ,有重复数据,每次排序复杂度为 O(log(N)+M), N 为有序集的基数,而 M 为结果集的基数。 15 | 16 | SORT 命令将数据保存在不同的 key 内,无重复数据,每次排序复杂度为 O(N+M*log(M)), N 为要排序的列表或集合内的元素数量, M 为要返回的元素数量。 17 | 18 | 19 | ## 实例:高性能分页 20 | 21 | TODO 22 | 23 | 24 | ## 实例:排行榜 25 | 26 | TODO 27 | -------------------------------------------------------------------------------- /usage/tag.md: -------------------------------------------------------------------------------- 1 | # 打标签 2 | 3 | 4 | ## 普通标签 5 | 6 | 思路:用 Set 结构实现 7 | 8 | 示例: 9 | 10 | 一篇名叫 "Redis Tutorial" 的文章可以用以下命令来打上标签: 11 | 12 | redis 127.0.0.1:6379> SADD "Redis Tutorial" "redis" "tutorial" "nosql" "database" 13 | (integer) 4 14 | 15 | 列出标签: 16 | 17 | redis 127.0.0.1:6379> SMEMBERS "Redis Tutorial" 18 | 1) "redis" 19 | 2) "tutorial" 20 | 3) "database" 21 | 4) "nosql" 22 | 23 | 24 | ## 带分值的聚合型标签 25 | 26 | 思路:用 SortedSet 保存各个单独的标签信息,每个标签信息以标签名作为 member 值,用 1 作为 score 值。然后使用 ZUNIONSTORE 命令对多个标签进行聚合计算。 27 | 28 | 示例: 29 | 30 | 还是和前面一样,假设有一篇名为 "Redis Tutorial" 的文章。 31 | 32 | Peter 将这篇文章标记为 "redis" 、"nosql" 和 "tutorial" ,执行以下操作: 33 | 34 | redis 127.0.0.1:6379> zadd peter-tag 1 "redis" 1 "nosql" 1 "tutorial" 35 | (integer) 3 36 | 37 | Tom 将这篇文章标记为 "redis" 和 "database" : 38 | 39 | redis 127.0.0.1:6379> ZADD tom-tag 1 "redis" 1 "database" 40 | (integer) 2 41 | 42 | 最后, Jack 将这篇文章标记为 "redis" 和 "tutorial" : 43 | 44 | redis 127.0.0.1:6379> ZADD jack-tag 1 "redis" 1 "tutorial" 45 | (integer) 2 46 | 47 | 现在,执行以下 ZUNIONSTORE 命令,可以得出 "Redis Tutorial" 这篇文章的聚合标签结果: 48 | 49 | redis 127.0.0.1:6379> ZUNIONSTORE redis-tutorial-tag 3 peter-tag tom-tag jack-tag 50 | (integer) 4 51 | 52 | 聚合计算完成之后,使用 ZREVRANGE 查看结果: 53 | 54 | redis 127.0.0.1:6379> ZREVRANGE redis-tutorial-tag 0 -1 WITHSCORES 55 | 1) "redis" 56 | 2) "3" 57 | 3) "tutorial" 58 | 4) "2" 59 | 5) "nosql" 60 | 6) "1" 61 | 7) "database" 62 | 8) "1" 63 | 64 | 当然,在实际的程序中,不可能只有 3 个用户给一篇文章打标签,所以编写程序时,聚合操作需要分两步执行: 65 | 66 | 1. 查找所有给文章打了标签的用户的 key 67 | 68 | 2. 执行 ZUNIONSTORE 69 | --------------------------------------------------------------------------------