本书将从 Redis 的历史讲起,结合基础与实践,带领读者一步步进入 Redis 的世界。

  • 前言
  • 正文
    • 简介
    • 入门
    • 进阶
    • 实践
    • 持久化
    • 集群
    • 管理
  • 总结

前言

本书是一本 Redis 的入门指导书籍,介绍了 Redis 基础与实践方面的知识,包括历史与特性等几乎所有方面。本书的目标读者不仅包括 Redis 新手,还包括那些已经掌握 Redis 使用方法的人。对新手而言,本书的内容由浅入深且紧贴实践;对于已经了解 Redis 的读者,通过本书大量实例以及细节介绍,也能发现很多新的技巧。

正文

  • (作者注:docker 快速启动 Redis 实例办法)
#https://hub.docker.com/_/redis
docker pull redis:6.2.6
docker run -p 6399:6379 --name redis -d redis:6.2.6

简介

  • Redis 是 Remote Dictionary Server (远程字典服务器)的缩写
支持的数据类型 字符串string、散列hash、列表list、集合set、有序集合zset
所有数据存储在内存 一台普通笔记本一秒读写超10万个键值
Redis提供持久化支持 异步写入到硬盘
  • 简单稳定 开源 C语言开发 代码量两三万行(支持集群后增到5万行)
读取散列类型键 HGET post:1 title
6379是手机键盘上MERZ的对应数字 MERZ是一名意大利歌女的名字
  • 常用命令
PING 状态回复PONG
INCR foo 整数回复 递增键值 不存在的键默认给0递增后是1 不是整数形式则报错
GET foo 字符串回复
KEYS * 多行字符串回复
CONFIG SET loglevel warning 动态修改Redis配置
CONFIG GET loglevel
SET bar 1 设置字符串键值
EXISTS 键存在返回整数类型1 否则0
DEL bar 返回删除的键的个数
redis-cli KEYS "user:*" | xargs redis-cli DEL 删除多个键
TYPE foo 获取数据类型(string hash list set zset)
LPUSH bar 1 向指定列表类型键中增加一个元素 如果键不存在就创建
DBSIZE 所有键的数量
INCRBY foo 2 区别INCR一次增加2而不是1
DECR DECRBY 键值递减并返回整数类型结果
APPEND 尾部追加值 如果没有就新建 返回字符串总长度
STRLEN 返回字符串总长度
MSET key1 v1 key2 v2 同时设置多个键值
MGET key1 key2 同时获取多个键值

入门

  • Redis默认支持16个字典
从0开始递增 可通过配置参数databases修改 默认选择0
SELECT 1 选择1号数据库
多个数据库不是完全隔离的 如FLUSHALL可以清空一个Redis所有库的数据
不宜用一个Redis存储不同应用程序的数据
Redis非常轻量级 一个空Redis实例占用内存只有1MB左右
Redis能存储任意形式的字符串包括二进制数据如图片 字符串最大容量512M
所有Redis命令都是原子操作atomic operation
  • 实践
键命名 没有强制要求 通常用“对象类型:对象id:对象属性”如 user:1:friends 多单词用.分隔
每篇文章生成自增id 对每个对象类型增加对象时调用 INCR users:count
新增文章 $postID = INCR posts:count
浏览文章 $count = INCR post:10:page.view
文章整体做字符串系列化 只读取标题或修改标题会资源浪费 可以用hash散列类型
hash的键值也是一种字典结构 存储filed和值的映射 值只支持字符串 最多2^32-1个字段
Redis的非字符串数据类型(list hash set zset)元素只能是字符串 不支持数据类型嵌套
  • 散列存储
散列看着更直观和更容易维护 HGETALL key可获取一个对象所有字段 删除也是一次
存储同样的数据散列类型往往比字符串类型更加节约空间
HSET key field value 散列赋值 不区分插入和更新 之前不存在返回1 存在返回0
HSETNX 同HSET 但如果字段存在则不执行任何操作(NX=if not exists)返回0
HGET key field 散列取值
HMSET key field value [field value ...] 散列多个赋值
HMGET key field [field ...] 散列多个取值
HGETALL key 获取所有字段和值的列表
HKEYS key 获取所有字段的列表
HVALS key 获取所有值的列表
HLEN key 获取字段数量
HEXISTS key field 如果字段存在返回1 否则0
HINCRBY key field 1 自增数字结果返回整数类型(没有HINCR命令)
HDELkey field [field ...] 删除一个或多个字段 返回值是被删除的字段个数
  • list列表类型
可以存储一个有序的字符串列表 常向两端添加元素或获得列表的某个片段
列表内部使用双向链表double linked list实现 两端添加或查询附近元素时间复杂度O(1)
适合做记录日志 可以保证加入新日志的速度不会受到已有日志数量的影响
适合做数据流 最新的数据读取非常快
一个列表类型最多能容纳2^32-1个元素 同hash散列类型
LPUSH key value [value ...] 向左边增加元素 返回结果列表长度
RPUSH key value [value ...] 向右边增加元素 返回结果列表长度
LPOP key 列表左边弹出一个元素 因此可做队或栈使用
RPOP key 列表右边弹出一个元素
LLEN 返回列表元素个数 不存在键返回0 时间复杂度O(1)
LRANGE key start stop 获取列表片段(可为负 包含头尾 LRANGE key 0 -1 查询所有)
LREM key count value 删除列表中指定的值 count>0从左删 <0从右删 =0删除全部
LINDEX key value 获得指定索引的元素值 最右边是-1
LSET key index value 设置指定索引的元素值 最右边是-1
LTRIM key start end 删除指定索引范围外所有元素
LINSERT key BEFORE|AFTER pivot value 从左到右查值为pivot的元素并插入元素
RPOPLPUSH source destination 将A列表最右元素移动到B列表最左边(原子操作)
RPOPLPUSH 可做网络监控系统 循环取出网址来测试 并容易多线程扩展和加入新网址
  • set集合类型
每个元素不同且无序 一个键最多存储2^32-1个字符串
SADD key member [member ...] 增加元素 如果已存在则忽略 返回成功加入的元素个数
SREM key member [member ...] 删除元素 如果不存在则忽略 返回成功删除的元素个数
SMEMBERS key 返回所有元素
SISMEMBER key member 存在返回1否则返回0 时间复杂度O(1)
SDIFF keyA [keyB ...] 多个集合执行差集运算 即A-B
SINTER key [key ...]
SUNION key [key ...]
SRANDMEMBER key [count] 返回随机多个元素
SPOP 因为set无序 随机选出一个元素弹出
  • zset和list区别
两者都有序 都可以获得某一范围元素
list通过链表实现靠近两端数据速度快 更适合"新鲜事"或"日志"这样很少访问中间
zset使用散列表和跳跃表(skip list) 读取中间速度也快 O(log(N))
列表不能简单调整某元素位置 zset可以(通过更改这个元素的分数)
有序集合比列表更耗费内存
  • zset有序集合类型(sorted set)
ZADD key socre member [score member ...] 键 [分数 元素] 分数支持int和双精度浮点数
ZADD 元素已存在则用新的分数更新 返回新添加个数(不包含已存在元素)
ZSCORE key member 获得元素的分数 两元素分数相同会按自按顺序排序0<9<A<Z<a<z
ZRANGE key start stop [WITHSCORES] 从小到大返回之间的元素(含两端) [+返回score]
ZREVRANGE key start stop [WITHSCORES] 从大到小返回之间的元素
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] 按元素分数从小到大顺序返回分数在min和max之间(包含两端)的元素 不想包含可在数字前加"("符号
ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
+inf 无穷大 -inf 负无穷大
ZINCRBY key increment member 增加某元素的分数(可用负减) 返回更改后的值
  • zset实践
ZINCRBY posts:page.view 1 postId 文章点击量作为分数 文章分数+1
ZSCORE posts:page.view postId 获取文章分数
ZREVRANGE posts:page.view 0 10 获取从大到小前十个
ZCARD key 获得集合中元素的数量
ZCOUNT key min max 获得指定分数范围内的元素个数
ZREM key member [member ...] 删除一个或多个元素 返回成功删除的元素个数
ZREMRANGEBYRANK key start stop 按照排名范围删除元素 返回删除个数
ZREMRANGEBYSCORE key min max 按照分数范围删除元素 返回删除个数
ZRANK key member 获得从小到大元素排名 返回排名 0是最小
ZREVRANK key member 获得从大到小元素排名 返回排名 0是最大
ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight ...] 计算zset的交集
ZUNIONSTORE 计算集合间的并集

进阶

  • Redis事务 transaction
MULTI # OK
SADD "user:1:following" 2 # QUEUED
SADD "user:2:followers" 1 # QUEUED
EXEC # (integer) 1 (integer) 1
可以保证提交的命令同时成功或失败(客户端未EXEC提交前掉线事务队列会被清空)
还可以保证事务内的命令依次执行而不被其他命令插入
  • 事务错误处理
语法错误 只要有一个命令有语法错误执行EXEC就会直接返回错误
运行错误
MULTI # OK
SET key 1 # QUEUED
SADD key 2 # QUEUED
SET key 3 # QUEUED
EXEC # (error) WRONGTYPE Operation against a key holding the wrong kind of value
GET key # "3"
虽然 SADD key 2 出现错误 但 SET key 3 依然执行
Redis事务不提供回滚(rollback) 开发者必须在出错后自己做复原处理
因为不需要对回滚进行支持,所以 Redis的内部可以保持简单且快速
Redis主要认为失败都是使用者造成的所以就没有回滚操作
在Redis中除了没有原子性外,Redis对事务也没有隔离级别的概念,所以就不会产生我们使用关系型数据库需要关注的脏读,幻读,重复读的问题
  • WATCH命令
事务中有时需要先获得一条命令的返回值 例如GET和SET命令自己实现INCR函数的递增功能会出现竞态条件
WATCH命令可以监控一个或多个键 被修改或删除时之后的事务不会执行
SET key 1 # OK
WATCH key # OK
SET key 2 # OK
MULTI # OK
SET key 3 # QUEUED
EXEC # (nil)
GET key # "2"
事务中 SET key 3 没有执行 EXEC返回空结果
用事务实现INCR函数
def incr($key)
WATCH $key
$value = GET $key
if not $value
$value = 0
$value = $value + 1
MULTI
SET $key, $value
result = EXEC
return result[0]
因为不能保证其他客户端不修改这一键值,所以EXEC执行失败后需要重新执行整个函数
  • 过期时间
开发中的时效性数据 如限时优惠活动、缓存或验证码等可以过期删除的数据,在关系数据库中一般要额外一个字段记录到期时间然后定期检测删除过期数据
Redis中使用EXPIRE命令设置一个键的过期时间 到时间后Redis会自动删除
EXPIRE key seconds 设置键过期时间 单位秒 返回1设置成功 返回0键不存在或设置失败
TTL key 返回键的剩余时间 单位秒 键未设置过期返回-1 键不存在返回-2
PERSIST key 取消键的过期时间设置 成功返回1 否则返回0
PEXPIRE 单位毫秒
PEXPIRE key 1000 == EXPIRE key 1
PTTL key * 1000 == TTL key
WATCH命令检测一个拥有过期时间的键 到期自动删除不会被WATCH认为键改变
EXPIREAT 和 PEXPIREAT 使用Unix时间作为第二个参数表示键的过期时刻
  • 实现访问频率限制之一
限制每个IP一段时间的最大访问量 减轻服务器压力
对每个IP使用一个名为 rate.limiting:IP 的字符串类型 每次访问使用INCR递增 如果第一次递增后的值是1则同时设置该键的过期时间为1分钟 IP每次访问时读取该键值是否超过100
isKeyExists = EXISTS rate.limiting:$IP
if isKeyExists is 1
times = INCR rate.limiting:$IP
if times > 100
exit
else
MULTI
INCR rate.limiting:$IP
EXPIRE $keyName 60
EXEC
  • 实现访问频率限制之二
方案一的力度较粗 不能保证任意间隔时段内都不超过限制 如一分钟最后一秒请求所有次
精确保证每分钟访问次数需记录IP每次访问的时间 用列表类型记录 判断列表元素数即可
listLength = LLEN rate.limiting:$IP
if listLength < 10
LPUSH rate.limiting:$IP now()
else
time = LINDEX rate.limiting:$IP -1
if now() - time < 60
exit
else
LPUSH rate.limiting:$IP now()
LTRIM rate.limiting:$IP 0 9
限制A时间最多访问B次 如果B较大时此方法会占用较多存储空间 需要开发者权衡
该方法会出现竞态条件 可通过脚本功能避免
  • 实现缓存
提高网站的负载能力 常常将访问频率高但CPU或IO消耗型操作的结果缓存起来 但大量使用缓存键且过期时间过长会导致Redis占满内存 过短会导致缓存命中率低并且大量内存白白的闲置 实际开发中很难为缓存设置合理的过期时间
限制Redis能使用的最大内存并让Redis按规则淘汰不需要的缓存键
修改配置文件的maxmemory参数 Redis最大可用内存(单位字节)
超出内存限制时Redis依据maxmemory-policy参数策略删除不需要的键直到小于指定内存
LRU(least recently used)最近最少使用 推定未来一段时间也最不会被用到 优先删除
实际实现LRU是每次随机取3个键并删除这3个键中最久未被使用的键 设置过期时间最接近的键也是这样 依据maxmemory-samples参数设置随机取键个数
volatile-lru   使用LRU算法删除一个键(只对设置了过期时间的键)
allkeys-lru   使用LRU算法删除一个键
volatile-random   随机删除一个键(只对设置了过期时间的键)
allkeys-random   随机删除一个键
volatile-ttl   删除过期时间最近的一个键
noeviction   不删除键 只返回错误
  • 排序
SORT list/set/zset [DESC] [ALPHA] [LIMIT offset count] 返回从小到大排序列表
BY参数 参考键可以是字符串类型键或散列类型键的某个字段(键名->字段名)
依据tag获取time倒序的文章id
SORT tag:ruby:posts BY post:*->time DESC
SORT会读取post:*几个散列键中的time字段并以此决定tag:ruby:posts键值返回顺序
GET参数 不影响排序 可使返回结果不再是自身值而是GET参数指定的键值
依据tag获取time倒序的文章的title
SORT tag:ruby:posts BY post:*->time DESC GET post:*->title [GET post:*->time][GET #]
一个SORT命令只能有一个BY参数 可以有多个GET参数 获取文章ID用GET #
STORE参数 如果希望保存排序结果可用STORE参数
SORT tag:ruby:posts BY post:*->time DESC GET # STORE sort.result 返回列表长度
保存后的键为列表类型 如果键存在会被覆盖
LRANGE sort.result 0 -1 返回列表
STORE参数常结合EXPIRE命令缓存排序结果
  • SORT性能优化
SORT是Redis中最强大最复杂的命令之一 时间复杂度O(n+mlog(m))其中n表示待排序list/set/zset的长度 m表示要返回的元素个数
n较大时SORT性能相对较低
排序前会建立一个长度为n的临时容器来存储待排序的元素 同时进行较多的大数据量排序会严重影响性能
尽可能减少待排序键中元素数量 n尽可能小
使用LIMIT参数只获取需要的数据 m尽可能小
如果要排序的数据量较大 尽可能使用STORE参数将结果缓存
  • 任务队列
与任务队列进行交互的实体有两类 生产者producer和消费者consumer
松耦合(隐藏实现) 易于扩展(分布式)
BRPOP key timeout 阻塞式从最右弹出元素 timeout为0时永久阻塞
使用Redis实现任务队列
loop
task = BRPOP queue 0
execute(task[1])
优先级队列
BRPOP key [key ...] timeout 队列从左到右的key顺序取完key的元素
发布订阅模式 publish/subscribe channel
PUBLISH channel message 发布消息(不被持久化) 返回这条消息的订阅者数量
SUBSCRIBE channel [channel ...] 订阅频道 客户端会进入订阅模式不能执行无关命令
  • 管道
客户端和Reids使用TCP协议连接 网络传输来回总耗时称为往返时延
Redis的底层通信协议对管道pipelining提供支持 可以一次获取多个命令的结果
  • 节省空间 减少内存消耗优化存储
精简键名和键值 如male和female用0和1代替
内部编码优化 Redis为每种数据类型提供两种内部编码方式 如散列类型是通过散列表实现O(1)时间复杂度的查找赋值操作 但键中元素很少时Reids会采用一种更紧凑但性能稍差O(n)的内部编码方式 对开发者来说是透明的 Redis会根据实际情况自动调整
OBJECT ENCODING key 查看内部编码方式
Redis的每个键值都是使用一个redisObject结构体保存的
typedef struct redisObject {
unsigned type:4;
unsigned notused:2;  /* Not used */
unsigned encoding:4;
unsigned lru:22;  /* lru time (relative to server.lruclock) */
int refcount;
void *ptr;
} robj;
type字段表示键值的数据类型
$define REDIS_STRING 0
$define REDIS_LIST 1
$define REDIS_SET 2
$define REDIS_ZSET 3
$define REDIS_HASH 4
encoding字段表示Redis键值对的内部编码方式
#define REDIS_ENCODING_RAW 0 /* Raw representation */
#define REDIS_ENCODING_INT 1 /* Encoded as integer */
#define REDIS_ENCODING_HT 2 /* Encoded as hash table */
#define REDIS_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define REDIS_ENCODING_INTEST 6 /* Encoded as intset */
#define REDIS_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
#define REDIS_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
字符串类型 raw int embstr
散列类型 hashtable ziplist
列表类型 linkedlist ziplist
集合类型 hashtable intset
有序集合类型 skiplist ziplist
  • 字符串类型
Redis使用一个sdshdr类型的变量存储字符串 ptr字段指向该变量的地址
struct sdshdr {
int len; /* string length */
int free; /* buf remaining space */
char buf[]; /* string */
}
SET key foobar 存储键值空间
sizeof(redisObject) + sizeof(sdshdr) + strlen("foobar") = 30字节
redisObject中的refcount字段存储的是该键值被引用数量 Redis启动后会预先建立10000个分别存储0到9999数字的redisObject类型变量作为共享对象 如果设置的字符串键值在这些数字内则直接引用共享对象而不是再建立一个redisObject 存储键值占用的空间是0字节 Redis只需存储键名和对共享对象的引用即可
当通过文件参数maxmemory设置Redis最大内存后 Redis不会使用共享对象 因为对于每个键值都需要使用redisObject来记录其LRU信息
Redis3.0新加入了REDIS_ENCODING_EMBSTR字符串编码方式 类似与REDIS_ENCODING_RAW都是基于sdshdr实现 只不过sdshdr结构体与对应的分配在同一块连续的内存空间 分配内存或释放内存的操作都从两次减少为一次 键值不超过39字节时Redis采用
  • 散列类型
hashtable或ziplist 在配置文件可以定义使用ziplist编码方式的时机
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
散列类型键的字段个数少于hash-max-ziplist-entries参数且每个字段名和字段值的长度都小于hash-max-ziplist-value参数(单位字节)时,Redis就会使用ziplist每当键值变更后Redis都会自动判断条件完成转换
Redis的键值对存储也是通过散列表实现的 与REDIS_ENCODING_HT编码方式类似
ziplist是一种紧凑的编码格式 牺牲部分读取性能换取极高的空间利用率 适合元素少
ziplist编码每个元素由4个部分组成
前一个元素大小、当前元素编码类型、当前元素大小、当前元素内容
  • 列表类型
linkedlist或ziplist 在配置文件定义使用ziplist编码方式的时机
list-max-ziplist-entries 512
list-max-ziplist-value 64
同散列类型
linkedlist编码方式即双向链表 每个元素用redisObject存储 优化办法同字符串
redis3.2版本新增 REDIS_ENCODING_QUICKLIST 编码方式是linkedlist和ziplist的结合,原理是将长列表分成若干个以链表形式组织的ziplist 从而达到减少空间占用同时提升ziplist编码性能的效果
  • 集合类型
hashtable或intset
所有元素都是整数且小于配置set-max-intset-entries(默认512)时Redis使用intset编码
typedef struct intset {
unit32_t encoding;
unit32_t length;
int8_t contents[];
} inset;
contents存储集合中的元素值 根据encoding不同每个元素占用的字节大小不同
默认encoding是INTSET_ENC_INT16(2字节)新增的整数元素无法用2字节表示时会升级为INT32(4字节)或INT64(8字节) intset编码以有序的方式存储元素 所以用SMEMBERS命令返回结果有序 但添加删除元素都要调整后面元素内存位置 集合内元素太多时性能较差
新增不是整数或元素数量超过set-max-inset-entries时自动结构转换hashtable
转换后删除元素也不可逆 因为删除元素遍历集合判断是O(n)操作
  • 有序集合类型
skiplist或ziplist 配置ziplist
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
同散列类型和列表类型
skiplist Redis使用散列表和跳跃表两种数据结构存储有序集合类型键值

实践

  • Python与Redis(书内还有PHP、Node.js的实践示例 本文未列出)
Redis官方推荐redis-py (pip install redis)
import redis
r = redis。StrictRedis(host='127.0.0.1', port=6379, db=0)
r.set('foo', 'bar') # True
r.get('foo') # 'bar'
开启事务
pipe = r.pipline()
pipe.set('foo', 'bar')
pipe.get('foo')
result = pipe.execute()
print result # [True, 'bar']
管道和事务相同且都支持链式调用 区别只不过在构造时加上transaction=False
result = r.pipeline().set('foo', 'bar').get('foo').execute()
在线好友=全站在线用户集合和某用户所有好友的集合取交集
在线用户可以用一定时间内有请求而定
另一种方法:有序集合
网站记录全站用户最后访问时间 利用此数据获得最后访问发生在10分钟内的用户列表为在线用户 使用zset记录用户最后访问时间 元素值为用户id 分数为最后一次访问的Unix时间
使用 ZRANGEBYSCORE 获得最近十分钟访问的用户列表
ten_minutes_ago = time.time() - 10 * 60
online_users = r.zrangebyscore('last.seen', ten_minutes_ago, '+inf')
想要获得在线好友列表需要和好友列表set取交集 所以需要把在线用户zset转存成set
我们希望将ZRANGEBYSCORE命令的结果直接存入一个新键
(1)赋值一个last.seen键的副本temp.last.seen方法为
ZUNIONSTORE temp.last.seen 1 last.seen 参加求并集的元素只有一个 结果自然是本身
(2)将超过10分钟的不在线用户删除 方法为
ZREMRANGEBYSCORE temp.last.seen 0 10分钟前的Unix时间
(3)现在temp.last.seen键中存的就是当前的在线用户 和好友列表set做交集
ZINTERSTORE online.friends 2 temp.last.seen user:2333:friends
(4)使用ZRANGE命令获取online.friends键的值
(5)收尾 删除temp.last.seen和online.friends键
temp.last.seen被所有用户共用 可以根据情况将其缓存一段时间 有值直接使用
以上5步需要使用事务或脚本实现 保证每个步骤的原子性
有时候我们使用zset来存储好友列表此时第3步依然奏效
  • 脚本
期望Redis直接提供一个"RATELIMITING"的命令来实现访问频率限制功能 只需要我们提供键名、时间限制和在时间限制内最多访问的次数三个参数直接返回访问频率是否超限
if RATELIMITING rate.limiting:$IP, 60, 100
exit
else
pass
这种方法不仅代码简单、没有竞态条件(Redis的命令都是原子的)而且减少了通过网络的传输开销 这时我们可以使用Redis脚本自己定义新的命令
Redis在2.6版退出脚本功能 允许开发者使用Lua语言编写脚本传到Redis中执行 在Lua脚本中可以调用大部分的Redis命令 有很多好处
(1) 减少网络开销 只要发送一个请求即可 减少了网络往返时延
(2) 原子操作 整个脚本执行中不会被其他命令插入 不会出现竞态条件也无需使用事务
(3) 复用 客户端发送的脚本会永久存储在Redis中 其他客户端可以复用这个脚本
local times = redis.call('incr', KEYS[1])
if times == 1 then
redis.call('expire', KEYS[1], ARGV[1])
end
if times > tonumber(ARGV[2]) then
return 0
end
return 1
测试脚本 先保存为ratelimiting.lua 然后在命令行中输入
redis-cli --eval /path/ratelimiting.lua rate.limiting:127.0.0.1 , 10 3
--eval参数告诉redis-cli读取并运行后面的Lua脚本 (ip在10秒内最多访问3次)
注意 KEYS和ARGV之间有逗号," , "逗号两边的空格不能省略 否则出错
Lua是一个高效的轻量级脚本语言 Lua葡萄牙语 月亮 寓意是卫星(胶水)语言
将执行脚本放到网上增强APP的灵活扩展性(投喂时间越短效用越低的例子)
function feed(timeSinceLastFeed)
local hungerValue = 0
if timeSinceLastFeed > 3600
hangerValue = ((timeSinceLastFeed - 3600) / timeSinceLastFeed) * 200
return hungerValue
end
然后在程序中嵌入一个Lua解释器,每次喂食就通过解释器调用这个Lua脚本 下次需要调整这个算法只需要从网上更新这个脚本连APP都不用重启 越多的逻辑放在脚本上程序的升级和扩展就越容易
实际上很多iOS游戏中都使用了Lua语言 如2011年很火的《愤怒的小鸟》使用Lua语言实现关卡 《魔兽世界》的插件也使用Lua开发
  • Lua语法(Redis常用的部分)
Lua是动态类型语言 变量可存储任何类型的值
数据类型
空(nil) 没有赋值的变量或表的字段都是nil
布尔(boolean) 布尔类型包含true和false两个值
数字(number) 整数和浮点数都是数字类型 1、0.2、3.5e20
字符串(string) 与Redis键值一样都是二进制安全的 可以用单引号或双引号
表(table) Lua语言中唯一的数据结构 既可以当数组也可以当字典
函数(function) 函数在Lua中是一等值(first-class value) 可做变量或传参和返回结果
变量
Lua变量分全局变量和局部变量 全局变量无需声明默认值nil
a = 1   -- 全局变量a
print(b)   -- 无需声明 默认值是nil
a = nil   -- 删除全局变量a
Redis脚本中不能使用全局变量 防止脚本之间相互影响 只能用局部变量
local c   -- 声明一个局部变量 默认值nil
local d = 1
local e, f
local say_hi = function ()
print 'hi'
end
变量名必须是非数字开头 只包含字母数字和下划线 区分大小写
local x = 10
if true then
local x = x + 1
print(x)
do
local x = x + 1
print(x)
end
print(x)
end
print(x)
打印 11 12 11 10
单行注释 -- 多行注释 --[[注释内容]]
赋值
local a, b = 1, 2   -- 赋值a=1 b=2
local a = {1, 2, 3}
操作符
Lua有以下5类操作符
(1) 数学操作符 +、-、*、/、%、^
(2) 比较操作符 ==相等、~=不相等、<、<=、>、>=
1 ~= '1' 类型不同不会自动类型转换
{'a'} ~= {'a'} 表类型值比较的是两者的引用
(3) 逻辑操作符 not、and、or 只有nil或false是假 0或空串是真 支持短路
(4) 连接操作符 .. 用来连接两个字符串 数字类型会自动转换成字符串
(5) 取长度操作符 #
print(#'hello')   -- 5
(6) if语句
if 条件表达式 then
语句块
elseif 条件表达式 then
语句块
else
语句块
end
可以省略每个语句结尾的分号; 不强制要求缩进
(7) 循环语句
while 条件表达式 do
语句块
end
repeat
语句块
until 条件表达式
for 变量 = 初值,终值,步长[可省略 默认1] do
for i = 1, 100 do
(8) 表类型
Lua中唯一的数据结构
a = {}
a['field'] = 'value'
print(a.field)
people = {
name = 'Bob',
age = 29
}
a = {}
a[1] = 'Bob'
a[2] = 'Jeff'
print(a[1])   -- 'Bob' Lua约定数组是从1开始而不是0
for index, value in ipairs(a) do   -- 迭代器 可打印非数组的表值
for i = 1, #a do   -- 循环
pairs与ipairs区别 pairs遍历所有值不为nil的索引 ipairs从1遍历到最后一个非nil
(9) 函数
function (参数列表)
函数体
end
local square = function (num)
return num * num
end
local function square (...)
local argv = {...}
for i = 1, #argv do
argv[i] = argv[i] * argv[i]
end
return unpack(argv)
end
a, b, c = suqare(1, 2, 3)   -- 1 4 9
  • Lua标准库
Base 提供一些基础函数
迭代器ipairs和pairs
类型转换tonumber和tostring
解包unpack
String 提供用于字符串操作的函数
string.len('hello') 字符串长度
string.lower('HELLO') 转换大小写
string.upper('hello') 转换大小写
string.sub(string, start [, end]) 获取子字符串 索引从1开始 end默认-1
Table 提供用于表操作的函数
table.concat(table [, sep [, i [, j]]]) 数组转字符串 分隔符sep 从i到j
table.insert(table, [pos,] value) 向数组中插入元素
table.remove(table, [, pos]) 从数组中弹出一个元素
Math 提供数学计算函数 (如果参数是字符串会尝试转换成数字)
math.abs/sin/cos/tan/ceil/floor/max/min/pow/sqrt
math.random([m, [, n]]) 随机数 默认[0,1)实数 [1,m]整数 [m,n]整数
math.randomseed(x)  伪随机数使用同一种子生成的随机数序列是相同的
Debug 提供用于调试的函数
其他库
除标准库外Redis还通过cjson库和cmsgpack库提供对JSON和MessagePack的支持 Redis自动加载了这两个库
local json = cjson.encode(str)
local str = cjson.decode(json)
  • Redis与Lua
脚本中调用Redis命令
redis.call('set', 'foo', 'bar')
local value = redis.call('get', 'foo')
整数回复对应Lua数字类型
字符串回复对应Lua字符串类型
多行字符串对应Lua表类型(数组形式)
状态回复和错误回复 表类型(只有一个ok或err字段和信息)
redis.pcall函数和redis.call相同 pcall在执行出错时会记录错误并继续执行 call会中断
Redis脚本返回值和get的转换规则相反(Lua的false比较特殊,会被转换成空结果)
脚本相关命令
EVAL命令 可以调用字符串代码直接解释执行
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 foo bar   -- OK
GET foo   -- bar
EVALSHA命令
EVAL会计算脚本的SHA1摘要并记录在脚本缓存中 使用EVALSHA可减少带宽
  • 深入脚本
KEYS与ARGV
EVAL "return redis.call('get' KEYS[1])" 1 user:Bob
EVAL "return redis.call('get', 'user:' .. ARGV[1])" 0 Bob
虽然未按照Redis的规则使用KEYS参数传递键名 但两者返回结果一样
虽然规定不是强制的 但3.0版后带有集群cluster功能,会将库中的键分散到不同的节点上 这意味着脚本执行前不知道会操作哪些键无法找到节点 所以无法兼容集群
有时键名是根据脚本某部分执行结果生成的就无法执行前明确键名无法兼容集群 但对避免网络往返很有效 这可能就要开发者自行权衡了
沙盒与随机数
Redis禁止Lua标准库中与文件和系统调用相关的函数且不允许全局变量 脚本隔离
使用沙盒不仅保证服务器安全性还确保相同的执行结果 不依赖外界条件
除了使用沙盒外确保结果可重现 Redis还对随机数特殊处理
对于会产生随机结果的命令如SMEMBERS(集合无序)或HKEYS(散列类型字段无序)等Redis会对结果按照字典顺序排序 内部通过调用Lua标准库table.sort函数实现的
其他脚本相关命令
除了EVAL和EVALSHA外 还有4个脚本相关命令 一般都被客户端封装起来
(1) 将脚本加入缓存 SCRIPT LOAD
(2) 判断脚本是否已经被缓存 SCRIPT EXISTS
(3) 清空脚本缓存 SCRIPT FLUSH
(4) 强制终止当前脚本的执行 SCRIPT KILL
原子性和执行时间
Redis脚本是原子性的 期间不会执行其他命令 为了防止某个脚本执行时间过长(如死循环) Redis提供lua-time-limit参数限制脚本最长运行时间 默认5秒 超过限制后其他命令会收到BUSY错误返回但不会执行 此时Redis实际只可执行两个命令 SCRIPT KILL和SHUTDOWN NOSAVE 但如果脚本中已对数据进行了修改为保证原子性不允许KILL 只能强制退出Redis并且丢失上一次快照后的数据库修改

持久化

  • 持久化
Redis重启后 内存中的数据会丢失 我们可能将Redis当数据库使用或做缓存服务器使用但要防止缓存同时失效 缓存被击穿导致缓存雪崩 服务器无法响应
Redis支持两种方式持久化
RDB方式 根据指定的规则"定时"将内存数据存储到硬盘
AOP方式 每次执行命令后将命令本身记录下来
两种持久化方式可单独使用 通常是将两者结合使用
  • RDB方式
RDB持久化通过快照snapshotting完成 当符合一定条件时Redis自动将内存的所有数据生成副本存储在硬盘 Redis会在以下情况进行快照
(1) 根据配置规则进行自动快照
(2) 用户执行SAVE或BGSAVE命令 (LASTSAVE查看)
(3) 执行FLUSHALL命令
(4) 执行复制replication
快照原理
默认将快快转文件存储在Redis当前进程的工作目录中的dump.rdb文件中 可以通过配置dir和dbfilename参数指定快照文件的存储路径和文件名
(1) Redis使用fork函数复制一份当前进程(父进程)的副本(子进程)
(2) 父进程继续接收并处理客户端的命令 子进程将内存中数据写入硬盘的临时文件
(3) 子进程写入完成后用该临时文件替换旧的RDB文件
在执行fork时(类Unix系统)会使用写时复制(copy-on-write)策略 即fork函数发生的一刻父子进程共享同一内存数据 父进程更改某片数据时 操作系统将该片数据复制一份以保证子进程的数据不受影响 索引新的RDB文件存储的是执行fork一刻的内存数据
写时复制策略也保证了在fork时刻看上去生成两份内存副本 实际内存占用量不会增加一倍
Redis在快照结束后才将旧文件替换 所以任何时候RDB文件都是完整的 我们可以定期备份
RDB文件是经过压缩的的二进制文件(配置rdbcompression参数可禁用压缩节省cpu占用)压缩后占用空间减小 更利于传输
Redis启动后会读取RDB快照文件载入内存 通常1000万个字符串键、大小为1GB的快照文件载入到内存要花费20-30秒
通过RDB方式实现持久化 如果Redis异常退出就会丢失最后一次快照以后的数据更改
例如使用Redis存储缓存数据时 丢失最近几秒的数据并不会有很大影响
  • AOF方式
使用Redis存储非临时数据时 一般需要打开AOF持久化来降低进程终止导致的数据丢失
AOF将Redis执行的每一条写入命令追加到硬盘文件中 这会降低Redis的性能但大部分情况下这个影响可以接受 使用高性能硬盘可以提高AOF性能
开启AOF
默认情况Redis没有开启AOF(append only file)可通过参数启用
appendonly yes
AOF文件的保存位置同RDB文件 默认名appendonly.aof
AOF文件以纯文本形式记录Redis执行的写命令
对键进行覆盖写时前面的写键操作就无用可以只保留最后一条 实际上Redis也是这样做的 每当达到配置的一定条件时Redis就会自动重写AOF文件
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
auto-aof-rewite-percentage参数的意义是当目前的AOF文件大小超过上一次重写时AOF文件大小的百分之多少时再次进行重写 如果之前没有重写过 则以启动时AOF文件大小为依据
auto-aof-rewrite-min-size参数限制了允许重写的最小AOF文件大小
重写过程只和内存中的数据有关 和之前的AOF文件无关 在启动时Redis会逐个执行AOF文件中的命令来将硬盘中的数据载入内存 相比RDB会慢一些
同步硬盘数据
虽然每次执行更改数据库内容操作时AOF都会将命令记录在AOF文件中 但事实上由于操作系统的缓存机制 数据并没有真正写入硬盘而是进入系统的硬盘缓存 在默认情况下系统每30秒会执行一次同步操作以便将硬盘缓存中的内容真正写入硬盘 如果系统异常退出则会导致硬盘缓存中的数据丢失 Redis写入AOF文件后主动要求系统将缓存内容同步到硬盘中 可通过修改appendfsync参数设置同步时机
#appendfsync always
appendfsync everysec
# appendfsync no
默认Redis使用everysec规则 每秒执行一次同步操作 always每次执行写入都会执行同步 最安全也最慢 no表示不主动同步操作完全由操作系统来做 一般everysec就足够了
Redis允许同时开启AOF和RDB 既保证数据安全又使得进行备份等操作十分容易 此时重启Redis会使用AOF文件恢复数据 因为AOF方式的持久化可能丢失的数据更少

集群

  • 集群
结构上 单个Redis服务器会发生单点故障 同时一台服务器承受所有请求负载
容量上 单个Redis服务器内存非常容易成为存储瓶颈 需要进行数据分片
Redis中有复制、哨兵(sentinel)和集群(cluster)
  • 复制
通过持久化功能 Redis保证了异常终止也不会出现大量损失 但是由于数据存储在一台机器如果出现硬盘故障等问题也会导致数据丢失 为避免单点故障可以使用复制功能
复制(replication)功能可以实现一台数据库中数据更新后自动将更新的数据同步其他库
在复制的概念中数据库分为两类 一类是主数据库(master)一类是从数据库(slave)主数据库可以进行读写操作 当写操作导致数据变化时会自动将数据同步给从库 从库一般只读 一个主库可以有多个从库 主库无需配置 从库在配置文件中加入"slaveof 主库地址 主库端口"即可
INFO replication 可以查看主从节点相关信息
SLAVEOF IP PORT 可在配置文件 也可以运行时执行 会转向同步新的主库
SLAVEOF NO ONE 可以使当前库取消从库身份
复制原理 (复制初始化阶段、复制同步阶段)
首先复制初始化 当一个从库启动后会向主库发送SYNC命令 同时主库接收到SYNC命令后会开始在后台保存快照(即RDB持久化的过程) 并将保存快照期间接收的命令缓存起来 当快照完成后 Redis会将快照文件和所有缓存的命令发给从库 从库会载入快找文件并执行收到的缓存的命令 初始化结束后主库收到写命令时就会将命令同步给从库
从具体协议角度看复制初始化过程 Redis使用TCP协议通信 我们使用telnet工具伪装成从库和主库通信 telnet 127.0.0.1 6379
PING #发送PING命令确认主库是否可连接 主库返回+PONG
REPLCONF listening-port 6381 #向主库说明自己端口号(随意) 返回+OK
SYNC #向主库发送SYNC命令开始同步 主库返回快照文件(二进制)和缓存的命令
从库将收到的内容写入硬盘的临时文件中 写入完成后从库会用该临时文件替换RDB快照文件 然后和RDB持久化启动恢复过程一样 需要注意的是在同步过程中从库并不会阻塞 而是继续处理客户端发来的命令 默认情况下从库会用同步前的数据对命令进行相应 可以配置
slave-server-stable-data参数为no使从库同步时回复错误 SYNC with master in progress
复制初始化阶段结束后主库执行的任何会导致数据变化的命令都会异步的传给从库
(1) 只要执行复制就会进行RDB快照 即使关闭了RDB方式持久化
(2) Redis2.8之后支持无硬盘复制
(3) Redis采用乐观复制策略 容忍一定时间内主从不一致(异步 最终同步)保证性能
(4) Redis2.8之后从库会向主库发送PSYNC来代替SYNC以实现增量复制
(5) 从库也可以有从库
(6) 主库将命令传给从库出现网络断开时此两者数据会不一致 主库无法得知某个命令最终同步给了多少个从库 Redis提供两个配置来限制当数据至少同步给指定数量从库时 主库才是可写的
min-slaves-to-write 3 #只有当3个或以上从库连接到主库时 主库才是可写的
min-salves-max-lag #允许从库最长失去连接的时间(最后REPLCONF ACK)
读写分离与一致性
通过复制实现读写分离提高服务器负载能力 当单机Redis无法应对大量读请求时(尤其是较耗资源的请求如SORT命令等)可通过复制功能让多个从库负责读操作
数据库持久化
持久化较消耗资源 为了提高性能可以主库禁用持久化而从库开启持久化 从库崩溃后主库仍会自动将数据同步过来 主库崩溃后运维手工操作将从库使用 SLAVEOF NO ONE命令将从库提升为主库 启动之前崩溃的主库并设置为现主库的从库即可
注意开启复制且主库关闭持久化功能时 一定不要使用Supervisor等进程管理工具令主库崩溃后自动重启 这样数据库所有数据都被清空而从库依然会从主库接收数据 从库也被清空 因为这种手工维护方式相对麻烦所以Redis提供了一种自动化方案哨兵来实现这一过程
无硬盘复制
2.8版开始Redis引入无硬盘复制选项 开启该选项时Redis在主从复制初始化时不会讲快照内容存到硬盘上而是直接通过网络发送给从库 避免硬盘的性能瓶颈
repl-diskless-sync yes
增量复制
2.8版开始Redis允许主从断线重连时增量复制而不是2.6版重新发送SYNC命令复制初始化而是发送"PSYNC 主库的运行ID 断开前最新的命令偏移量"(复制同步阶段 主库每将一个命令传给从库都会同时把该命令存放在一个积压队列并记录当前存放的命令的偏移量) 主库收到PSYNC命令后会执行判断是否可以增量同步 首先判断传来的主库ID是否与自己运行ID一致再判断从库最后同步成功的命令偏移量是否在积压队列中 不满足则执行全部同步同Redis2.6
积压队列本质上是个固定长度的循环队列 默认大小1MB 可通过配置repl-backlog-size调整
repl-backlog-ttl 当所有从库与主库断开后多久可以释放积压队列 默认1小时
  • 哨兵 (2.6开始提供 2.8开始稳定可用)
哨兵是一个独立进程 作用是监控Redis主库从库是否正常运行 出现故障时从库转换为主库
首先配置好Redis一主二从库 INFO replication查看完成 然后配置哨兵文件sentinel.conf
# sentinel monitor mymaster ip port quorum
sentinel monitor mymaster 127.0.0.1 6379 1
mymaster表示要监控的主库名字可以自定义 最后1表示最低通过票数
$ redis-sentinel /path/sentinel.conf
配置哨兵系统只需要配置监控主库即可 哨兵自动发现该主库的从库
+try-failover #主库崩溃后等待指定时间(默认30秒 可配置)哨兵进行故障转移
+failover-end #switch到新的主库 这时已经完成了领头哨兵的选举、备选从库等
重启原主库后哨兵会发现恢复 -sdown(与+sdown相反)
一个哨兵可监控多个Redis主从系统 多个哨兵也可同时监控一个Reids主从系统
哨兵启动后会与要监控的主库建立两条连接 连接方式同普通客户端 一条连接订阅该主库的__sentinel__:hello频道以获取其他同样监控该库的哨兵节点信息 另一条连接定期向主库发送INFO等命令获取主库信息(并获取到从库 再向从库发送两条连接)
(1) 每10秒哨兵向主库和从库发送INFO命令
(2) 每2秒哨兵向主库和从库的__sentinel__:hello频道发送自己的信息
(3) 每1秒哨兵向主库从库和其他哨兵节点发送PING命令
#每隔600毫秒发送一次PING命令 如果超过1000则为每个一秒
sentinel down-after-milliseconds mymaster 600
超过指定时间后PING未回复则哨兵认为其主观下线(subjectively down)哨兵发送SENTINEL is-master-down-by-addr命令询问其他哨兵节点 如果达到指定数量则认为其客观下线(objectively down)并选举领头的哨兵节点对主从系统发起故障恢复(这样可以保证同一时间只有一个哨兵节点执行故障恢复)选举领头哨兵使用Raft算法
(1) 发现主库客观下线的哨兵A向每个哨兵节点发送命令 要求对方选自己为领头哨兵
(2) 如果目标节点没有选过其他人则会同意A设置为领头哨兵
(3) 如果A发现有超过半数且超过quorum参数值的哨兵节点同意则A成为领头哨兵
(4) 当有多个哨兵同时参选领头哨兵则可能出现没有节点当选 此时每个参选节点将等待一个随机时间重新发起参选请求进行下一轮选举 直到选举成功
因为必须超过半数所以肯定只会选出一个领头哨兵执行恢复故障
(1) 所有在线从库中选择优先级最高的从库 优先级可通过slave-priority设置
(2) 如果优先级相同则复制命令偏移量越大(数据最完整)越优先
(3) 如果以上条件一样 选择运行ID较小的从库
选出从库后领头哨兵向从库发送SLAVEOF NO ONE升级其为主库 向其他从库发送 SLAVEOF命令使其成为新主库的从库 然后更新内部记录将已崩溃的旧主库更新为从库
最佳部署策略 让N个哨兵具有代表性即为每个节点(无论主库从库)部署一个哨兵 使得哨兵与其对应节点的网络环境相近 同时设置quorum的值为N/2+1 使大部分哨兵同意才进行恢复
  • 集群 cluster
哨兵中每个库仍有所有数据导致总数据存储量受限于最小库形成木桶效应
旧版对Redis进行水平扩容可以由客户端决定哪些键交给哪个库节点存储但需要扩容时需要对数据进行手工迁移还需要暂停集群服务 比较复杂
考虑到Redis非常轻量 可以采用预分片presharding技术 在部署初期建立足够多的实例(如128个节点)因为Redis非常轻量所以开销并不大只需要较少服务器 日后存储规模扩大 只需要将部分实例迁移到其他服务器而不需要对数据重新分片和数据迁移
但客户端分片还是有很多缺点 维护成本高 Redis3.0版支持集群Cluster 拥有和单机实例相同的性能同时在网络分区后能提供一定的可访问新和故障恢复
集群只能使用0号数据库 MGET等多键命令如果不在同一节点会提示错误
如果不需要分片或已经在客户端进行分片场景下哨兵已经足够使用
配置集群
在配置项cluster-enabled yes即可
INFO cluster # cluster_enabled:1
节点加入集群
Redis源代码中提供辅助工具redis-trib.rb(使用Ruby)
$ /path/redis-trib.rb create --replicas 1 [ip:port]x6
--replicas 1表示一个主库有一个从库 初始化为3主3从 自动分配的原则是尽量让主库运行在不同IP上 同时每个主库和从库不在同一ip上 保证系统容灾能力
分配完成后会为每个主库分配插槽(哪些键归哪些节点)
Redis连接任一节点执行 CLUSTER NODES 可获得集群所有节点信息
一个集群中所有键分配给16384个插槽slots 每个主库负责其中一部分
Redis将每个键名有效部分使用CRC16算法计算散列值对16384取余
如果键名有{xxx}部分则有效部分为xxx 这样设计有助于多键操作如MGET
获取与插槽对应的节点
如果连接的节点上没有要操作的键会收到 MOVED 重定向请求 一般需要客户端如Redis库处理 重定向需要依次请求两个节点所以对性能会有影响 客户端操作库应该缓存所有插槽的路由信息 之后每次请求只发向正确节点从而达到和单机实例相同性能
故障恢复
集群中每个节点都会定期向其他节点发送PING命令并通过是否回复判断节点是否下线 具体操作是每个节点每隔一秒随机选5个节点然后选择最久没有响应的节点发送PING
(1) 节点A认为节点B疑似下线(PFAIL)状态就在集群中传播该消息 所有其他节点记录该消息
(2) 某一节点C收到半数以上认为B疑似下线时 将B标记下线(Fall)并向集群记录该消息 B在整个集群中下线 如果至少有一个B的从库 则像哨兵选举过程一样基于Raft算法尝试故障恢复 如果没有可用从库则集群默认进入下线状态无法继续工作 修改配置可让集群仍能正常工作
cluster-require-full-coverage no (默认为yes)

管理

  • 管理
可信的环境 不应开放外网访问 可通过 bind参数绑定一个地址
配置requirepass参数为Redis设置密码 Redis不会主动延迟所以容易被穷举
配置Redis复制时如果主库设置了密码需在从库配置中设置masterauth主库密码
rename-command FLUSHALL oyfekmjvmwxq5a9c8usofuo #命令重命名
rename-command FLUSHALL "" #禁用命令
通信协议 Redis支持两种通信协议 一种是二进制安全的统一请求协议(unified request protocal)另一种是比较直观的适合在telnet程序中输入的简单协议 两种协议只有命令格式的区别 返回值的格式一样
耗时命令日志
SLOWLOG GET  # 返回列表 日志唯一id、Unix时间、耗时微秒、命令和参数
slowlog-log-slower-than 设置超过耗时的记录
命令监控
MONITOR  # 任何Redis执行的命令都在客户端中打印出来
非常影响Redis性能 一个客户端使用MONITOR命令会降低Redis近一半的负载能力

总结

很好的 Redis 入门使用手册,可以帮助你上手或了解 Redis 所能提供的功能全貌,比想象中的入门指导更加详实丰富。

读书笔记《Redis入门指南》相关推荐

  1. 读书笔记 | 墨菲定律

    1. 有些事,你现在不做,永远也不会去做. 2. 能轻易实现的梦想都不叫梦想. 3.所有的事都会比你预计的时间长.(做事要有耐心,要经得起前期的枯燥.) 4. 当我们的才华还撑不起梦想时,更要耐下心来 ...

  2. 读书笔记 | 墨菲定律(一)

    1. 有些事,你现在不做,永远也不会去做. 2. 能轻易实现的梦想都不叫梦想. 3.所有的事都会比你预计的时间长.(做事要有耐心,要经得起前期的枯燥.) 4. 当我们的才华还撑不起梦想时,更要耐下心来 ...

  3. 洛克菲勒的38封信pdf下载_《洛克菲勒写给孩子的38封信》读书笔记

    <洛克菲勒写给孩子的38封信>读书笔记 洛克菲勒写给孩子的38封信 第1封信:起点不决定终点 人人生而平等,但这种平等是权利与法律意义上的平等,与经济和文化优势无关 第2封信:运气靠策划 ...

  4. 股神大家了解多少?深度剖析股神巴菲特

    股神巴菲特是金融界里的传奇,大家是否都对股神巴菲特感兴趣呢?大家对股神了解多少?小编最近在QR社区发现了<阿尔法狗与巴菲特>,里面记载了许多股神巴菲特的人生经历,今天小编简单说一说关于股神 ...

  5. 2014巴菲特股东大会及巴菲特创业分享

     沃伦·巴菲特,这位传奇人物.在美国,巴菲特被称为"先知".在中国,他更多的被喻为"股神",巴菲特在11岁时第一次购买股票以来,白手起家缔造了一个千亿规模的 ...

  6. 《成为沃伦·巴菲特》笔记与感想

    本文首发于微信公众帐号: 一界码农(The_hard_the_luckier) 无需授权即可转载: 甚至无需保留以上版权声明-- 沃伦·巴菲特传记的纪录片 http://www.bilibili.co ...

  7. 读书笔记002:托尼.巴赞之快速阅读

    读书笔记002:托尼.巴赞之快速阅读 托尼.巴赞是放射性思维与思维导图的提倡者.读完他的<快速阅读>之后,我们就可以可以快速提高阅读速度,保持并改善理解嗯嗯管理,通过增进了解眼睛和大脑功能 ...

  8. 读书笔记001:托尼.巴赞之开动大脑

    读书笔记001:托尼.巴赞之开动大脑 托尼.巴赞是放射性思维与思维导图的提倡者.读完他的<开动大脑>之后,我们就可以对我们的大脑有更多的了解:大脑可以进行比我们预期多得多的工作:我们可以最 ...

  9. 读书笔记003:托尼.巴赞之思维导图

    读书笔记003:托尼.巴赞之思维导图 托尼.巴赞的<思维导图>一书,详细的介绍了思维发展的新概念--放射性思维:如何利用思维导图实施你的放射性思维,实现你的创造性思维,从而给出一种深刻的智 ...

  10. 产品读书《滚雪球:巴菲特和他的财富人生》

    作者简介 艾丽斯.施罗德,曾经担任世界知名投行摩根士丹利的董事总经理,因为撰写研究报告与巴菲特相识.业务上的往来使得施罗德有更多的机会与巴菲特亲密接触,她不仅是巴菲特别的忘年交,她也是第一个向巴菲特建 ...

最新文章

  1. mysql插入大量数据
  2. 吴恩达最新成果 CheXNet详解:肺炎诊断准确率超专业医师
  3. 简单粗暴tensorflow2.0
  4. 《C#本质论》读书笔记(18)多线程处理
  5. linux 心跳灯_Linux下点亮第一个LED灯
  6. 20100412 最近在看加密解密
  7. Java反梯形图案_梯形法求定积分(一)设计梯形法求积分的类模板,梯形法
  8. run在java_Java语言start和run方法的区别
  9. java后端工程师平时开发或多或少会用到eclipse,那么它有哪些快捷键呢
  10. matlab和C/C++混合编程--Mex (转载)
  11. 浅谈React Event实现原理
  12. 2020年IT技术的发展趋势!
  13. 微信小程序返回Qcloud\\Cos\\Exception\\NoSuchBucketException: Cos Error Code: NoSuchBucket, Status Code: 404
  14. Glide异步加载图片,设置默认图片和错误图片
  15. [blog] Speech Recognition Is Not Solved 语音识别领域尚待解决的子问题
  16. 酷狗音乐web端API接口数据
  17. IP-guard和Ping32屏幕监控对比
  18. pgr_createTopology正运行时卡死的解决
  19. 拦截来电(来电挂断)
  20. 【DevCloud · 敏捷智库】软件开发团队如何管理琐碎、突发性任务(内附下载材料)

热门文章

  1. 支付宝支付提示 4006 ISV权限不足
  2. pta 计算圆周率(C语言实现)
  3. 史上最全的美国留学资料,从此不用找中介
  4. php storm 安装,PhpStorm安装教程
  5. _beginThread如何传递多个参数
  6. CMakeLists.txt 语法介绍与实例演练
  7. 撤销excel工作表保护
  8. 六年码农生涯的 2019 总结:君子坐而论道,少年起而行之
  9. 蝉妈妈对话曹颖:5月带货1.1亿,踩过的坑都变成了卖货的筹码
  10. 机械专业向机器人工程专业转型的可行性与前景分析