Redis学习笔记,并开发简易博客

263 查看

准备
  • 安装
wget http://download.redis.io/redis-stable.tar.gz
tar xzf redis-stable.tar.gz
cd redis-stable
make
# 将可执行程序复制到/usr/local/bin目录中以便以后执行程序时可以不用输入完整的路径
make install
# 测试Redis是否编译正确
make test

使用 make test 命令时提示 You need tcl 8.5 or newer in order to run the Redis test,解决方案如下:

wget http://downloads.sourceforge.net/tcl/tcl8.6.1-src.tar.gz  
tar xzvf tcl8.6.1-src.tar.gz  -C /usr/local/  
cd  /usr/local/tcl8.6.1/unix/  
./configure  
make  
make install  
  • 启动/停止

    • 直接启动

      redis-server
      # Redis服务器默认使用6379端口,通过--port参数可以自定义端口号
      redis-server --port 6380
    • 初始化脚本启动 Redis (推荐使用)

      1. 配置初始化脚本。将 Redis 源代码目录下的 utils 文件夹中的 redis_init_script 这个初始化脚本文件复制到 /etc/init.d 目录下,取名为 redis_6379,端口号可以改成自己想要的,只需修改脚本第6行 redisport 变量的值为同样的端口号即可
      2. 建立文件夹。/etc/redis/var/redis/6379,前者存放 Redis 的配置文件,后者存放 Redis 的持久化文件
      3. 修改配置文件。将 Redis 源代码目录下的 redis.conf 复制到 /etc/redis 目录中,以端口号命名(如 6379.conf )。然后将配置项如下表修改
      配置项 值 说明

      daemonize yes 使Redis以守护进程模式运行
      pidfile /var/run/redis_6379.pid 设置Redis的PID文件位置
      port 6379 设置Reids监听的端口号
      dir /var/redis/6379 设置持久化文件存放位置

现在就可以使用 /etc/init.d/redis_6379 start 来启动 Redis

  1. 停止 Redis
redis-cli SHUTDOWN
入门
  1. Redis 不区分命令大小写,但和 Mysql 一样,建议使用大写字母表示命令
  2. 对键的一些简单操作
    
    # 列出符合表达式的键名列表,pattern支持glob风格通配符格式
    KEYS pattern
设置键为 "bar" 值为 "1" 的键值对

SET bar 1

取出键为 "bar" 的值

GET bar

判断一个键是否存在。存在返回1,否则返回0

EXISTS key

删除键。可以一次删除多个键,返回删除的键个数

DEL key [key ...]

删除所有符合规则的键

redis-cli KEYS "sc_*" xargs redis-cli DEL

获得键值的数据类型

TYPE key


符号  含义
------
?  匹配一个字符
*  匹配任意个(包含0个)字符
[]  匹配括号间的任一符号,可以使用 "-" 符号表示一个范围,如[a-c]可以匹配"a","b","c"
\x  匹配字符x,用于转义符号。如要匹配 "?" ,就需要使用 \?

- 字符串类型:`Redis` 最基本的数据类型,可以存储二进制数据、`JSON` 化的对象(比如图片),一个字符串类型的键允许存储数据的最大容量为512M。

> 常用命令
增加指定的整数,key为键,increment为增加的步进值

INCRBY key increment

减少指定的整数,参数同INCRBY

DECRBY key increment

增加指定浮点数,参数同INCRBY

INCRBYFLOAT key incremnet

向尾部追加值,key为键,value为追加的值。如果键不存在则将该键的值设置为value。返回值是追加后字符串的总长度

APPEND key value

获取字符串长度,对于中文UTF-8编码,每一个中文长度是3

STRLEN key

同事获得/设置多个键值

MGET key [key ...]
MSET key value [key value ...]


- 散列类型
散列类型适合存储对象:使用对象类别和 `ID` 构成键名,使用字段表示对象的属性,而字段值则存储属性值,如下图所示。

graph LR
car:1-->color
car:1-->name
car:1-->price
color-->白色
name-->路虎
price-->90万


> 常用命令
散列类型的操作前缀带H
单个赋值

HSET key field value

单个取值

HGET key

多个赋值

HMSET key field value [field value ...]

多个取值

HMGET key field [field ...]

取全部值

HGETALL key

判断field字段是否存在

HEXISTS key field

当field字段不存在时赋值

HSETNX key field value

增加数字,如果key不存在则会自动建立并默认field字段为0,散列类型没有HINCR命令

HINCRBY key field incremnet

删除field字段

HDEL key field [field ...]

获取字段名/值

HKEYS key
HVALS key

获取字段数量

HLEN key

`HSET` 命令不区分插入和更新操作。当执行的是插入操作 `HSET` 返回 1,执行的是更新操作 `HSET` 返回 0

- 列表类型
    > 常用命令
# 向列表两端增加元素
# 补充说明:当添加多个值时
# eg. LPUSH news n1 n2 n3
# 先将n1增加到news列表中,然后是n2,n3,所以最后news:[n3 n2 n1 ...],因此当使用LRANGE输出全部值时就会变成n3在最开始
LPUSH key value [value ...]
RPUSH key value [value ...]

# 向列表两端弹出元素
LPOP key
RPOP key

# 获取列表中元素的个数
LLEN key

# 获得列表片段,返回索引从start到stop之间的所有元素(包括两端的元素),也支持负索引
# 补充说明:
# 1. 如果start的索引位置比stop的索引位置靠后,则会返回空列表 
# 2. 如果stop大于实际的索引范围,则会返回到列表最右边的元素
LRANGE key start stop
# eg.返回列表mylist中的所有元素
LRANGE mylist 0 -1

# 删除列表中指定的值,删除列表中前count个值为value的元素,返回值是实际删除的元素个数
# 补充说明:
# 1. 当 count > 0 时,LREM会从列表左边开始删除
# 2. 当 count < 0 时,LREM会从列表右边开始删除
# 3. 当 count = 0 时,LREM会删除所有值为value的元素
LREM key count value

# 获得/设置指定索引的元素值
LINDEX key index
LSET key index value

# 只保留列表指定片段
LTRIM key start end
# 补充说明:
# LTRIM常和LPUSH一起使用来限制列表中元素的数量,比如记录日志时只保留最近的100条日志
LPUSH logs newlog
LTRIM logs 0 99

# 向列表中插入元素,首先从左到右查找值为value1的元素,然后根据BEFORE或AFTER来决定将value2插入到该元素的前或后
LINSERT key BEFOREAFTER value1 value2

# 将一个列表右边的元素转到另一个列表左边
RPOPLPUSH source destination
```
  • 集合类型

    常用命令

    # 增加/删除元素
    # 补充说明:添加时若不存在该键则会自动创建。因为在集合中不能有相同的元素,所以如果元素已存在则会忽略。返回值时成功加入的元素数量
    SADD key member [member ...]
    SREM key member [member ...]
    
    # 获得所有元素
    SMEMBERS key
    
    # 判断元素是否在集合中
    SISMEMBER key member
    
    # 集合间运算:差集、交集、并集
    SDIFF key [key ...]
    SINTER key [key ...]
    SUNION key [key ...]
    
    # 获得元素个数
    SCARD key
    
    # 进行集合运算并将结果存储,将运算的结果存储在destination键中,常用于多步集合运算的场景中
    SDIFFSTORE destination key [key ...]
    SINTERSTORE destination key [key ...]
    SUNIONSTORE destination key [key ...]
    
    # 随机获得集合中的元素
    # 补充说明:
    # 1. 当 count > 0 时,随机从集合中获得count个不重复的元素,如果count的值大于集合中的元素个数,则返回全部元素
    # 2. 当 count  < 0 时,随机从集合里获得count个有可能重复的元素
    SRANDMEMBER key [count]
  • 有序集合类型

    常用命令

    # 增加元素,如果元素已经存在则会用新的分数替换原有的分数,返回值时新价格的元素个数,分数不仅可以是整数,还支持双精度浮点数
    # eg.
    # -inf 表示负无穷 +inf 表示正无穷
    # ZADD test -inf a
    # ZADD test +inf b
    ZADD key score member [score member ...]
    
    # 获得元素的分数
    ZSCORE key member
    
    # 获得排名在某个范围的元素列表,可选参数 WITHSCORES 表示连带分数也一起输出
    ZRANGE key start stop [WITHSCORES]
    ZREVRANGE key start stop [WITHSCORES]
    
    # 获得指定分数范围的元素
    ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
    # 补充说明:
    # 1. 如果希望分数范围不包含端点值,可以再分数前加上 “(” 符号。eg. ZRANGEBYSCORE test 60 (100
    # 2. min 和 max 支持无穷大(-inf +inf)。eg. ZRANGEBYSCORE test (60 +inf
    # 3. LIMIT offset count 与SQL中的用法基本相同
    
    # 增加/减少某个元素的分数,返回值是更改后的分数
    # eg. 给Jerry加4分:ZINCRBY test 4 Jerry
    # eg. 给Jerry减4分:ZINCRBY test -4 Jerry
    ZINCRBY key incremnet member
    
    # 获得集合中元素的数量
    ZCARD key
    
    # 获得指定分数范围内的元素个数,min max同样支持无穷大
    ZCOUNT key min max
    
    # 按照排名范围删除元素,返回删除的元素数量
    ZREMRANGEBYRANK key start stop
    
    # 按照分数范围删除元素,返回删除的元素数量
    ZREMRANGEBYSCORE key min max
    
    # 获得元素的排名
    ZRANK key member
    ZREVRANK key member
    
    # 计算有序集合的交集,destinade键中元素的分数是由AGGREGATE参数决定的
    # 1. 当AGGREGATE时SUM(也就是默认值),destination键中元素的分数是每个参与计算的集合中该元素分数的和
    # 2. 当AGGREGATE时MIN,destination键中元素的分数是每个参与计算的集合中最小的元素分数
    # 3. 当AGGREGATE时MAX,destination键中元素的分数是每个参与计算的集合中最大的元素分数
    # eg.
    # ZADD ss1 1 a 2 b
    # ZADD ss2 9 a 8 b
    # ZINTERSTORE res 2 ss1 ss2 AGGREGATE SUM
    # ZRANGE res 0 -1 WITHSCORES
    # 结果:a 10 b 10
    # 4. WEIGHTS参数可以设置每个集合的权重,在参与计算时元素的分数会被乘上该集合的权重
    # eg,
    # ZINTERSTORE res1 2 ss1 ss2 WEIGHTS 0.8 0.5
    # 结果:a 8 b 5
    ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUMMINMAX]
事务

Redis 中的事务是一组命令的集合,同命令一样都是 Redis 的最小执行单位,一个事务中的命令要么都执行,要么多不执行。

redis> MULTI
OK
redis> SADD "user:1:following" 2
QUEUED
redis> SADD "user:2:followers" 1
QUEUED
redis> EXEC
1) (integer) 1
2) (integer) 1

首先使用 MULTI 命令告诉 Redis 下列的命令是属于同一个事务的;接下来执行的所有命令返回 QUEUED 表示已经进入等待执行的事务队列中;使用 EXEC 告诉 Redis 将等待执行的事务队列中的所有命令按照发送顺序依次执行。如果在发送 EXEC 命令前客户端断线,则 Redis 会清空事务队列

  • 事务的错误处理
    • 语法错误:只要有一个命令有语法错误,执行 EXECRedis 就会直接返回错误,其他正确的命令也不会执行
    • 运行错误:比如使用散列类型的命令操作集合类型的键,这种情况下,其他的命令依然会继续执行
      由于 Redis 不支持回滚功能,也使得 Redis 在事务上可以保持简洁和快速。上述两种错误,语法错误完全可以在开发时找出并解决,运行错误可以通过良好的规划数据库来规避
# 监控一个或多个键,一旦其中有一个键被修改,之后的第一个事务就不会执行。监控一直持续到 EXEC 命令(因为事务中的命令是在 EXEC 之后才执行的,所以在 MULTI 命令之后 EXEC 命令之前是有可以修改监控的键的值),执行 EXEC 命令后会取消对所有键的监控
WATCH key [key ...]
# 取消监控,保证下一个事务的执行不会受到影响
UNWATCH key [key ...]

# 举例一:开始监听name后,在执行第一个事务前对name赋值,导致该事务不会执行,而第二个事务正常执行。也因此由于监控键的值在事务执行前被修改而导致失败后仍需再次执行,保证该事务的业务逻辑操作得到执行
redis> SET name koro
OK
redis> WATCH name
OK
redis> SET name sara
OK
redis> MULTI
OK
redis> SET name mike
QUEUED
redis> EXEC
(nil)
redis> GET name
"sara"
redis> MULTI
OK
redis> SET name lara
QUEUED
redis> EXEC
1) OK
redis> GET name
"lara"
  • 生存时间:在 Redis 中可以使用 EXPIRE 命令设置一个键的生存时间,到时间后 Redis 会自动删除它

    # 前者时间单位为秒,后者时间单位为毫秒。返回1表示设置成功,返回0表示键不存在或设置失败
    EXPIRE key seconds
    PEXPIRE key millisecond
    
    # 查看指定键剩余生存时间(单位秒),返回-1表示键不存在或永久存在(设置一个键时默认为永久存在)
    TTL key
    
    # 取消键的生存时间设置,即将键恢复成永久的。返回1表示成功清除,返回0表示键不存在或已经是永久的
    # 特别说明:使用 SET 或 GETSET 命令为键赋值也会同时清除键的生存时间;如果使用 WATCH 命令检测了一个拥有生存时间的键,该键时间到期自动删除并不会被 WATCH 命令认为该键被改变
    PERSIST key
  • 淘汰键策略:当服务器内存有限时,如果大量地使用缓存键且生存时间设置得过长就会导致 Redis 占满内存;另一方面如果为了防止 Redis 占用内存过大而将缓存键的生存时间设得太短,就可能导致缓存命中率过低并且大量内存白白地闲置。实际开发中会发现很难为缓存键设置合理的生存时间,为此可以闲置 Redis 能够使用的最大内存,并让 Redis 按照一定的规则淘汰不需要的缓存键,这种方式在只将 Redis 用作缓存系统时非常实用。
    设置方法:修改配置文件的 maxmemory 参数,限制 Redis 最大可用内存大小(单位是字节),当超出了这个限制时 Redis 会依据 maxmemory-policy 参数指定的策略来删除不需要的键,直到 Redis 占用的内存小于指定内存。
规则 说明

volatile-lru 使用LRU(最近最少)算法删除一个键(只对设置了生存时间的键)
allkeys-lru 使用LRU(最近最少)算法删除一个键
volatile-random 随机删除一个键(只对设置了生存时间的键)
allkeys-random 随机删除一个键
volatile-ttl 删除生存时间最近的一个键
noeviction 不删除键,只返回错误

  • 排序

    • SORT 命令:可以对列表类型、集合类型和有序集合类型键进行排序,并且可以完成与关系数据库中的连接查询相类似的任务
      • 对数字进行排序
      • 对非数字进行排序,需要加上 ALPHA 参数
      • 默认为从小到大排序,需要从大到小排序使用 DESC 参数
      • 需要分页显示可用 LIMIT 参数:LIMIT offset count(跳过前 offset 个元素并获取之后的 count 个元素)
      • 举例:SORT tag:php:articles DESC LIMIT 1 2
    • BY 参数
      • 语法为 BY 参考键 ,其中参考键可以使字符串类型键或者是散列类型键的某个字段。如果提供了 BY 参数,SORT 命令将依据参考键的值来排序
      • 举例(散列类型):SORT tag:php:articles BY article:*->time DESC
      • 举例(字符串类型):SORT articles BY articles:* DESC
    • GET 参数
      • 该参数不影响排序,是使 SORT 命令的返回结果不再是元素自身的值,而是 GET 参数中指定的键值。支持字符串类型和散列类型的键,并使用 * 作为占位符。可以有多个 GET 参数
      • 举例(实现在排序后直接返回 ID 对应的文章标题):SORT tag:php:articles BY article:*->time DESC GET article:*->title
      • GET # 可以获取元素本身的值
    • STORE 参数
      • 将结果保存到指定键中
    • 性能优化
      • 尽可能减少待排序键中元素的数量
      • 使用 LIMIT 参数值获取需要的数据
      • 如果要排序的数据量较大,尽可能使用 STORE 参数将结果缓存
  • 任务队列
    当页面需要进行如发送邮件、复杂数据运算等耗时较长的操作时会阻塞页面的渲染。为了避免用户等待太久,应该使用独立的线程来完成这类操作。不过一些编程语言或框架不易实现多线程,这时就可以通过其他进程来实现,那么只需要通知这个进程向指定的地址发送邮件就可以了。
    通知的过程可以借助任务队列来实现。与任务队列进行交互的实体有两类:生产者(producer)和消费者(consumer)。生产者会将需要处理的任务放入任务队列中,而消费者则不断从任务队列中读入任务信息并执行
    好处:

    • 松耦合:生产者和消费者无需知道彼此的实现细节,只需约定好任务的描述格式。这使得生产者和消费者可以由不同的团队使用不同的编程语言编写。
    • 可扩展性:消费者可以有多个,而且可以分布在不同的服务器中,可以轻易地降低单台服务器的负载。
      简单示例(伪代码):
      loop
      $task = BRPOP queue 0
      execute $task[1]

      命令:BRPOPBLPOP
      说明:当列表中没有元素时会一直阻塞住连接,直到有新元素加入,才会从队列中弹出一个元素。
      参数:key timeout,第一个是键名,第二个是超时时间,单位是秒。当超过了此时间仍然没有获得新元素的话就会返回 nil。若为 0,表示不限制等待的时间,即一直会等待新元素加入。
      详解:

    • 完整命令:BLPOP key [key ...] timeout
    • 说明:同时检测多个键,如果所有键都没有元素则阻塞,如果其中有一个键有元素则从该键中弹出元素。如果有多个键都有元素则按照 从左到右 的顺序取第一个键中的一个元素。
  • 优先级队列
    当需要对通知给同一个进程的不同任务进行优先级对待的时候,可以使用完整格式的 BLPOP 来实现

  • “发布/订阅”模式:用到时再做分析

  • 管道
    通过管道可以一次性发送多条命令并在执行完后一次性将结果返回。管道通过减少客户端与 Redis 的通信次数来实现降低往返时延累计值的目的

  • 节省空间
    • 精简键名和键值:最直观的减少内存占用的方式,比如将 very.important.person:20 改成 VIP:20,将存储用户性别的字符串类型键的值改成 0 和 1。
实践
  • PHP 与 Redis:Redis官方推荐的PHP客户端是 Predisphpredis。前者是完全使用PHP代码实现的原生客户端,后者是使用 C 语言编写的 PHP 扩展。在功能上区别并不大,就性能而言后者更胜一筹。 我选择的是后者 phpredis
    • 安装 Redis
      phpize
      ./configure [--enable-redis-igbinary]
      make && make install
    • 添加 php-redis 扩展。修改你的 php.ini 文件或者在 /etc/php5/conf.d 中添加一个 redis.ini 文件,内容为 extension=redis.so
    • 实践:若有兴趣可以参考我的开源项目blog-redis——基于PHP+Redis开发的简易博客系统
脚本
  • 脚本介绍:Redis 在2.6版推出了脚本功能,允许开发者使用 Lua 语言编写脚本传到 Redis 中执行。在 Lua 脚本中可以调用大部分人的 Redis 命令。
  • 好处:
    • 减少网络开销:通过使用脚本功能完成同样的操作只需发送一个请求即可,减少了网络往返时延
    • 原子操作:Redis 会将整个脚本作为一个整体执行,中间不会被其他命令插入。也就是说在编写脚本的过程中无需担心会出现竞态条件,也就无需使用事务。事务可以完成的所有功能都可以用脚本来实现。
    • 复用:客户端发送的脚本会永久存储在 Redis 中,这就意味着其他客户端(可以使其他语言开发的项目)可以复用这一脚本而不需要使用代码完成同样的逻辑。
  • 实例:访问频率限制

Lua 代码

local times = redis.call('incr', KEYS[1])

if times == 1 then
    -- 如果KEYS[1]刚创建,为其设置生存时间
    redis.call('expires', KEYS[1], ARGV[1])
end

if times > tonumber(ARGV[2]) then
    return 0
end

return 1

执行命令

redis-cli --eval ./ratelimiting.lua rate.limiting:127.0.0.1 , 10 3

参数说明: --eval 是告诉 redis-cli 读取并运行后面的 Lua 脚本,./ratelimiting.lua 是指脚本文件的位置,后面跟着脚本的参数,其中 , 前的参数是要操作的键,在脚本中 KEYS[1] 获取,, 后的参数是使用 ARGV[1]ARGV[2]获得。特别的,, 两边的空格不能省略,否则会出错。

  • RedisLua

    • 在脚本中调用 Redis 命令:redis.call()

      Redis返回值类型 Lua数据类型

      整数 数字
      字符串 字符串
      多行字符串 表(数组形式)
      状态 表(只有一个ok字段存储状态信息)
      错误 表(只有一个err字段存储状态信息)

    • EVAL 命令:Redis 提供了该命令使其可以像调用其他内置命令一样调用脚本。

      • 命令格式:EVAL script key_count [key ...] [arg ...]
      • 参数说明:script 表示脚本的内容,key_count 表示key参数的个数,这样才能确定后面的哪几个是key,哪几个是arg,特别的,当脚本不需要任何参数时也不能省略 key_count 这个参数
    • EVALSHA 命令:如果每次调用脚本都需要将整个脚本传给 Redis 会占用较多的带宽。所以提供该命令允许开发者通过脚本内容的 SHA1 摘要来执行脚本。与 EVAL 命令基本相同,只不过将脚本内容换成脚本内容的 SHA1 摘要。

      • 使用流程(许多编程语言的 Redis 客户端都会代替开发者完成这一流程):
        1. 先计算脚本的 SHA1 摘要,并使用 EVALSHA 命令执行脚本。
        2. 获得返回值,如果返回 NOSCRIPT 错误则使用 EVAL 命令重新执行脚本。
    • 实例:

      $script = 'return redis.call("set", "test", "aaa")';
      
      if (($sha = $this->redis->script('load', $script)) !== false) {
          $this->redis->evalSha($sha);
      }
      
      echo $this->redis->get("test");
  • 原子性和执行时间:脚本执行期间 Redis 不会执行其他命令。为了防止某个脚本执行时间过长导致 Redis 无法提供服务(比如陷入死循环),Redis 提供了 lua-time-limit 参数限制脚本的最长运行时间,默认为5秒。如果超过这一限制后,Redis 将开始接收其他命令但不会执行(以确保脚本的原子性,因为此时脚本并没有终止),但是会返回 BUSY 错误。特别地,如果执行的脚本对数据进行了修改,那么 SCRIPT KILL 命令不会终止脚本的运行(因为如果脚本执行了一部分就被终止,会违背脚本的原子性要求)。这时只能通过 SHUTDOWN NOSAVE 命令强行终止。而改命令不会进行持久化操作,意味着所有发生在上一次快照后的修改都会丢失。

  • 建议:由于 Redis 脚本非常高效,所以在大部分情况下都不用担心脚本的性能。但同时由于脚本的强大功能,很多原本在程序中执行的逻辑都可以放到脚本中执行,这时就需要开发者根据实际情况权衡到底哪些任务适合交给脚本。通常来讲不应该在脚本中进行大量耗时的计算。

最后,

  1. 简易博客源代码分享 传送门
  2. 基于 PHP+Redis 开发的简易博客系统链接 传送门