redis

https://redis.io/

Redis采用的是基于内存的采用的是单进程单线程模型的KV数据库,由 C 语言编写。官方提供的数据是可以达到 10万+ 的 QPS。这个数据不比采用单进程多线程的同样基于内存的KV数据库Memcached 差。Redis/Memcached 这样的内存数据库,不同于 MySQL 这样的硬盘数据库,可以避免 I/O 开销,提升数据处理速度。

单进程单线程

单进程单线程好处:

  1. 代码更清晰,处理逻辑更简单;
  2. 不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
  3. 不存在多进程或者多线程导致的切换而消耗CPU;

单进程单线程弊端:

  1. 无法发挥多核CPU性能,不过可以通过在单机开多个Redis实例来完善;

Redis, in-memeory
MySQL, on-disk

GUI 软件:https://redisdesktop.com/

特性:

  1. 数据结构,string、list、set、hash、zset。
  2. 内存存储,这使得 Redis 的速度非常快。
  3. 远程,这使得 Redis 可以与多个客户端和服务器进行连接。
  4. 持久化,服务器可以在重启之后仍然保持重启之前的数据。
  5. 可扩展,通过主从复制和分片。

Redis 的 5 种数据结构

  • string:set、get、del
  • list:lpush、rpop、lrange、lindex
  • set:sadd、smemebers、sismember、srem
  • hash:hset、hget、hgetall、hdel
  • zset:zadd、zrange、zrangebyscore、zrem

Redis 的原子性

Redis的原子性有两点:

  • 单个操作是原子性的;
  • 多个操作也支持事务,即原子性,通过MULTIEXEC指令包起来;

Redis的操作之所以是原子性的,是因为Redis是单进程单线程的。 Redis 是单线程的事件循环,一个操作执行完了才执行下一个操作。

Redis本身提供的所有API都是原子操作Redis中的事务其实是要保证批量操作的原子性

Redis 事务

在多个客户端同时处理相同的数据时,不谨慎的操作很容易导致数据出错。使用 Redis 的事务来防止出错。

Redis 事务和 MySQL 的事务并不相同。MySQL 中用户先向数据库服务器发送 BGEIN,然后执行各个相互一致(consistent)的写操作和读操作,最后,用户可以选择发送 COMMIT 来确认之前所做的修改,或者发送 ROLLBACK来放弃那些修改。

Redis 的事务以特殊命令 MULTI为开始,之后跟着用户传入的多个命令,最后以 EXEC 命令为结束。

用户可以使用 MULTIEXECDISCARDWATCHUNWATCH 指令用来执行原子性的事务操作。

需要强调的是,Redis 中定义的事务,并不是关系数据库中严格意义上的事务。当 Redis 事务中的某个操作执行失败,或者用 DISCARD 取消事务时候,Redis 并不执行“事务回滚”,在使用时要注意这点。

Redis 为什么没有实现典型的加锁功能?

在访问以写入为目的数据的时候(SQL中的 SELECT FOR UPDATE)关系数据库会对被访问的数据行进行加锁,直到事务被提交(COMMIT)或者被回滚(ROLLBACK )为止。如果有其他客户端试图对被加锁的数据行进行写入,那么该客户端将被阻塞,直到第一不事务执行完毕为止。加锁在实际使用中非常有效,基本上所有关系数据库都实现了这种加锁功能,它的缺在于,持有锁的客户端运行越慢,等待解锁的客户端被阻塞的时间就越长。

因为加锁有可能会造成长时间的等待,所以 Redis 为了尽可能地减少客户端的等待时间,并不会在执行 WATCH命令时对数据进行加锁。相反地, Redis只会在数据已经被其他客户端抢先修改了的情况下,通知执行了 WATCH 命令的客户端,这种做法被称为乐观锁)(optimistic locking),而关系数据库实际执行的加锁操作则被称为悲观锁(pessimistic locking)。

乐观锁在实际使用中同样非常有效,因为客户端永远不必花时间去等待第一个取得锁的客户端——它们只需要在自己的事务执行失败时进行重试就可以了。

Redis 事务不支持回滚的应对办法

在事务运行期间,虽然Redis命令可能会执行失败,但是Redis仍然会执行事务中余下的其他命令,而不会执行回滚操作。

加强测试,确保操作 Redis 的代码不要范语法错误,确保 Redis 的命令都能正常完成。

应用场景

构建一个类似 Stack Overflow 的文章投票网站。

使用 hash 来保存文章信息的例子:

  • article:92617
    • title 文章标题
    • link 文章链接
    • poster 作者
    • time 发布时间
    • votes 支持票数

第一个 zset 为文章的时间: member 为文章ID,score 为文章的发布时间。
第二个 zset 为文章的评分: member 为文章ID,score 为文章的评分。
使用 set 存储某篇文章的投票用户

使用 Redis 构建 Web 应用

用来登录的 cookie:

  • 签名 cookie,存储用户名、用户ID、登录时间、一个签名(服务器可以使用这个签名来验证浏览器发送的信息是否被改动,例如将 cookie 中的用户名改成另一个用户)。
  • 令牌 cookie,一串随机字符作为令牌,服务器可以根据令牌在数据库中查找令牌的拥有者。

当我们网站使用了令牌cookie来引用关系数据库中负责存储用户信息的条目(entry)。现在我们需要保存用户的访问时长和已浏览商品的数量,这样便于将来通过分析这些信息来学习更好地像用户推销商品。

从长远看来,用户的这些浏览数据非常有用,但问题在于,即使经过优化,大多数关系型数据在每台服务器上面每秒也只能插入、更新或者删除 200~2000 个数据行。尽管批量插入、批量更新和批量删除等操作可以更快的速度执行,但因为客户端每次浏览网页都只更新少数几个行,所以高速的批量插入在这里并不适用。

使用 Redis 重新实现登录 cookie 功能,取代目前由关系型数据库实现的登录 cookie 功能。适用 hash 来存储 cookie 令牌 与已登录用户之间的映射。

redis.hset('login:', token, user)// 维持令牌与已登录用户之间的映射。
redis.zadd('recent:', token, timestamp)// 记录令牌最后一次出现的时间。
redis.hget('login:', token) // 尝试获取并返回令牌对应的用户。

使用 Redis 实现购物车

使用 cookie 实现购物车——也就是将整个购物车都存储到 cookie 里面的做法非常常见。这种做法的一大优点是无须对数据库进行写入就可以实现购物车功能,而缺点则是程序需要重新解析和验证 cookie,确保 cookie 格式正确,并且包含的商品都是真正可以购买的。cookie 购物车还有一个缺点:因为浏览器每次发送请求都会连 cookie 一起发送,所以,如果购物车中 cookie 的体积比较大,那么请求发送和处理的速度可能会有所降低。

redis.hset('cart:' + session, item, count) // 将 商品ID => 订购数量 添加到购物车
redis.hrem('cart:' + session, item) // 从购物车中移除指定商品

修改 PHP 配置文件:

session.save_handler = redis
session.save_path = "tcp://HOST:6379?auth=PASSWORD"

网页缓存

数据行缓存

数据持久化

  • 快照(snapshoting),将存在于某一时刻的所有数据都写入硬盘。
  • 只追加文件(append-only file),在执行写命令时,将被执行的命令复制都硬盘。

快照

save 900 1 如果距离上次成功生成快照已经超过了 900 秒(15分钟),并且在此期间至少执行了 1 次写入操作,那么 Redis 就会自动开始一次新的 BGSAVE 操作。

如果丢失一个小时之内产生的数据是可以接受的,那么可以使用配置值 save 3600 1

Redis 生成自增回环数字

序号:生成1到999序号,超过 1000 重置回 1

传入参数:

  1. 需要操作的键值(key)
  2. 最大值(argv)

当自增数字超过传入的最大值,则重置回 1。

local max = tonumber(ARGV[1])
local currentval = redis.call("GET", KEYS[1]);
local current = tonumber(currentval) + 1;
if current < max
then
    redis.call("INCRBY", KEYS[1], 1);
    return current;
else
    redis.call("SET",KEYS[1], 1);
    return 1;
end
[vagrant@localhost ~]$ redis-cli set accorder 1
OK
[vagrant@localhost ~]$ redis-cli get accorder
"1"
[vagrant@localhost ~]$ redis-cli --help
redis-cli 3.2.10
[vagrant@localhost ~]$ redis-cli --eval acc.lua accorder , 1000
(integer) 2

或者缓存脚本:

[vagrant@localhost ~]$ redis-cli EVALSHA 7e6a596f2158ffcd5cfd953f556c5298626c4c53 1 accorder 1000
(integer) 3

注意 key 和 arav 有两种传入方式:

  1. redis-cli --eval myscript.lua key1 key2 , arg1 arg2 arg3
  2. redis-cli EVALSHA dd5df942745a2fa78c32b0b554b8503fba68d4b2 1 accorder 1000 则是通过空格分隔,第一个数字表示 key 的个数然后用空格追加 key 和 argv。

Redis 秒杀应用

使用 Redis 搭建电商秒杀系统

消费者提交订单,一般做法是利用数据库的行级锁,只有抢到锁的请求可以进行库存查询和下单操作。但是在高并发的情况下,数据库无法承担如此大的请求,往往会使整个服务 blocked,在消费者看来就是服务器宕机。

第一级流量拦截:利用浏览器缓存和 CDN 抗压静态页面流量

第二级流量拦截:利用读写分离 Redis 缓存拦截流量

在这一阶段我们主要读取数据,读写分离 Redis 能支持高大60万以上 qps 的,完全可以支持需求。

首先通过数据控制模块,提前将秒杀商品缓存到读写分离 Redis,并设置秒杀开始标记如下:

"goodsId_count": 100 //总数
"goodsId_start": 0   //开始标记
"goodsId_access": 0  //接受下单数
  1. 秒杀开始前,服务集群读取 goodsId_Start 为 0,直接返回未开始。
  2. 数据控制模块将 goodsId_start 改为1,标志秒杀开始。
  3. 服务集群缓存开始标记位并开始接受请求,并记录到 redis 中 goodsId_access,商品剩余数量为(goodsId_count - goodsId_access)。
  4. 当接受下单数达到 goodsId_count 后,继续拦截所有请求,商品剩余数量为 0。

可以看出,最后成功参与下单的请求只有少部分可以被接受。在高并发的情况下,允许稍微多的流量进入。因此可以控制接受下单数的比例。

利用主从版 Redis 缓存加速库存扣量

成功参与下单后,进入下层服务,开始进行订单信息校验,库存扣量。为了避免直接访问数据库,我们使用主从版 Redis 来进行库存扣量,主从版 Redis 提供10万级别的 QPS。使用 Redis 来优化库存查询,提前拦截秒杀失败的请求,将大大提高系统的整体吞吐量。

通过数据控制模块提前将库存存入 Redis,将每个秒杀商品在 Redis 中用一个 hash 结构表示:

"goodsId" : {
    "Total": 100
    "Booked": 100
}

扣量时,服务器通过请求 Redis 获取下单资格,通过以下 lua 脚本实现,由于 Redis 是单线程模型,lua 可以保证多个命令的原子性。

local n = tonumber(ARGV[1])
if not n  or n == 0 then
    return 0       
end                
local vals = redis.call("HMGET", KEYS[1], "Total", "Booked");
local total = tonumber(vals[1])
local blocked = tonumber(vals[2])
if not total or not blocked then
    return 0       
end                
if blocked + n <= total then
    redis.call("HINCRBY", KEYS[1], "Booked", n)                                   
    return n;   
end                
return 0

先使用SCRIPT LOAD将 lua 脚本提前缓存在 Redis,然后调用EVALSHA调用脚本,比直接调用EVAL节省网络带宽:

redis 127.0.0.1:6379>SCRIPT LOAD "lua code"
"438dd755f3fe0d32771753eb57f075b18fed7716"
redis 127.0.0.1:6379>EVAL 438dd755f3fe0d32771753eb57f075b18fed7716 1 goodsId 1

秒杀服务通过判断 Redis 是否返回抢购个数 n,即可知道此次请求是否扣量成功。

使用主从版 Redis 实现简单的消息队列异步下单入库

扣量完成后,需要进行订单入库。如果商品数量较少的时候,直接操作数据库即可。如果秒杀的商品是1万,甚至10万级别,那数据库锁冲突将带来很大的性能瓶颈。因此,利用消息队列组件,当秒杀服务将订单信息写入消息队列后,即可认为下单完成,避免直接操作数据库。

  1. 消息队列组件依然可以使用 Redis 实现,在 R2 中用 list 数据结构表示。
orderList {
     [0] = {订单内容} 
     [1] = {订单内容}
     [2] = {订单内容}
     ...
 }
  1. 将订单内容写入 Redis:
LPUSH orderList {订单内容}
  1. 异步下单模块从 Redis 中顺序获取订单信息,并将订单写入数据库。
BRPOP orderList 0

通过使用 Redis 作为消息队列,异步处理订单入库,有效的提高了用户的下单完成速度。

CentOS7 安装配置 Redis

# yum install epel-release 

yum install redis
systemctl start redis

systemctl status redis

systemctl enable redis

redis 默认地址:127.0.0.1:6379

进入 redis 服务:

➜  ~ redis-cli --version
redis-cli 3.2.10

➜  ~ redis-cli
127.0.0.1:6379> 

设置 Redis 最大可用内存

配置文件 /etc/redis.conf,修改 maxmemory

# Note on units: when memory size is needed, it is possible to specify
# it in the usual form of 1k 5GB 4M and so forth:
#
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
# 单位不区分大小写,所以 1GB 1Gb 1gB 都是一样的。
# units are case insensitive so 1GB 1Gb 1gB are all the same.

修改为:

# maxmemory <bytes>
# 最大内存设置为 256mb
maxmemory 256mb

重启 redis 服务后,查看内存使用情况:

➜  ~ redis-cli
127.0.0.1:6379> info memory

# Memory
used_memory:814520
used_memory_human:795.43K
used_memory_rss:5976064
used_memory_rss_human:5.70M
used_memory_peak:814520
used_memory_peak_human:795.43K
total_system_memory:1928687616
total_system_memory_human:1.80G
used_memory_lua:37888
used_memory_lua_human:37.00K
maxmemory:268435456
maxmemory_human:256.00M
maxmemory_policy:noeviction
mem_fragmentation_ratio:7.34
mem_allocator:jemalloc-3.6.0
  • used_memory_human 数据占用内存;
  • used_memory_rss_human redis占用内存;
  • used_memory_peak_human 占用内存的峰值;
  • total_system_memory_human 系统内存大小;
  • used_memory_lua_human lua引擎所占用的内存大小;


Photo by Micha Sager / Unsplash