如果你从来没使用过Redis 数据库,那你肯定会问,为什么我们要学 Redis数据库,我只使用 MySQL 或 Oracle 就够了。其实 Redis 虽叫数据库,可又不是传统意义上的关系型数据库,Redis 是一个高性能的 Key-value 数据库。
首先我们先来讲一下 Redis 的历史。Redis 其实是作者 Salvatore Sanfilippo 为了解决实际问题而创造出来的。当时作者Salvatore 有这么一个需求,就是多个网站不断向服务器发送页面,而服务器需要为每个网站保存一定数量的最新页面记录,同时通过网页将数据实时给用户看到。但是无论 Salvatore 如何优化,都很难在关系数据库里让小虚拟机处理大负荷的负载。最终他打算自己写一个内存数据库,能对列表的两端执行常数时间复杂度的弹出和推入操作,并加上子进程的持久化操作,于是 Redis 就诞生了。
到了今天,Redis 已经进入了成熟期。数以千计的开发者都在开发和使用这个数据库,Redis 拥有非常完善的文档。我记得第一次使用 Redis,是为了在保存有数十百万用户的关系数据库里对某个条件进行查询。大家知道,要想在几百万用户中找到某条数据,是很难通过关系数据库在十几秒查询到的。于是我选择了 Redis,在不断优化后每次操作可以控制在 1 秒钟甚至更短,带给我相当大的震撼。
在 Redis 之前,很多互联网公司会使用 MySql + Memcached 架构,这个架构虽然适合于海量数据存储,但随着业务的增加,会出现很多问题,例如,MySQL 数据库经常拆表,导致 Memcached 也不断扩容;同步问题;命中率低,导致直接穿透 Memcached 进入 DB 查询,DB资源池是有限的,进而宕机。这些问题都会导致Memcached其实并不好用。
Redis就在这种时代背景中产生,你会发现 Memcached 遇到的问题都被 Redis 给解决了。如果你用过 Memcached,你就会感受到 Redis 绝对不是简单的 Key-value 数据,还有 list、set、哈希等各种数据类型的存储,同时支持冷热备份和主从复制,不但解决了数据库的容错,还能轻易地将数据分布到多个 Redis 实例中。
那么 Redis 有哪些具体特性呢?大致可分为如下八大特性。
特性一,速度极快。官方给出的数据是 10 万次 ops 的读写,这主要归功于这些数据都存在于内存中。由于 Redis 是开源的,当你打开源代码,就会发现 Redis 都是用 C 语言写的,C 语言是最接近计算机语言的代码,而且只有区区 5 万行,保证了 Redis 的速度。同时一个 Redis 只是一个单线程,其真正的原因还是因为单线程在内存中是效率最高的。
特性二,持久化。Redis 的持久化可以保证将内存中的数据每隔一段时间就保存于磁盘中,重启的时候会再次加载到内存。持久化方式是 RDB 和 AOF。
特性三,支持多种数据结构。分别支持哈希、集合、BitMaps,还有位图(多用于活跃用户数等统计)、HyperLogLog(超小内存唯一值计数,由于只有 12K,是有一定误差范围的)、GEO(地理信息定位)。
特性四,支持多种编程语言。支持 Java、PHP、Python、Ruby、Lua、Node.js。
特性五,功能丰富。如发布订阅、Lua 脚本、事务、Pipeline(管道,即当指令到达一定数量后,客户端才会执行)。
特性六,简单。不依赖外部库、单线程、只有 23000 行 Code。
特性七,主从复制。主节点的数据做副本,这是做高可用的基石。
特性八,高可用和分布式。Redis-Sentinel(v2.8)支持高可用,Redis-Cluster(v3.0)支持分布式。
Redis最大的作用是增加你原来的访问性能问题,试想如果项目已经搭建好,这个项目一般是不太可能更换的。但是 Redis 独特的存在是只需要增加一层,把常用的数据存放在 Redis 即可。你在开发环境中使用 Redis 功能,但却不需要转到 Redis。
无论是什么架构,你都可以将 Redis 融入项目中来,这可以解决很多关系数据库无法解决的问题。比如,现有数据库处理缓慢的任务,或者在原有的基础上开发新的功能,都可以使用 Redis。接下来,我们一起看看 Redis 的典型使用场景。
1.缓存系统。这是 Redis 使用最多的场景。Redis 能够替代 Memcached,让你的缓存从只能存储数据变得能够更新数据,因此你不再需要每次都重新生成数据。毫无疑问,Redis 缓存使用的方式与 Memcached 相同。网络中总是能够看到这个技术更新换代,Redis 的原生命令,尽管简单却功能强大,把它们加以组合,能完成的功能是无法想象的。当然,你可以专门编写代码来完成所有这些操作,但 Redis 实现起来显然更为轻松。
2.计数器。如转发数、评论数,有了原子递增(Atomic Increment),你可以放心的加上各种计数,用GETSET重置,或者是让它们过期。目前新浪是号称史上最大的 Redis 集群。
比如,你想计算出最近用户在页面间停顿不超过 30 秒的页面浏览量,当计数达到比如 10 时,就可以显示提示。再比如,如果想知道什么时候封锁一个 IP 地址,INCRBY命令让这些变得很容易,通过原子递增保持计数;GETSET用来重置计数器;过期属性用来确认一个关键字什么时候应该删除。
3.消息队列系统。虽然 Kafka 更强,但是简单的可以使用 Redis。运行稳定并且快速,支持模式匹配,能够实时订阅与取消频道。
Redis还有阻塞队列的命令,能够让一个程序在执行时被另一个程序添加到队列。你也可以做些更有趣的事情,比如一个旋转更新的 RSS Feed 队列。
4.排行榜及相关问题。实际就是一种有序集合。对于 Redis 来说,如果你要在几百万个用户中找到排名,其他数据库查询是非常慢的,因为每过几分钟,就会有几百万个不同的数据产生变化,但是 Redis 却可以轻松解决。
排行榜(Leader Board)按照得分进行排序。ZADD 命令可以直接实现这个功能,而 ZREVRANGE 命令可以用来按照得分获取前 100 名的用户,ZRANK 可以用来获取用户排名,非常直接而且操作容易。
5.社交网络。Redis 可以非常好地与社交网络相结合,如新浪微博、Twiter等,比如QQ和用户交互的时候,用户和状态消息将会聚焦很多有用的信息,很多交互如实时聊天就是通过 Redis 来实现的。
6.按照用户投票和时间排序。Reddit 的排行榜,得分会随着时间变化。LPUSH 和 LTRIM 命令结合运用,把文章添加到一个列表中。一项后台任务用来获取列表,并重新计算列表的排序,ZADD 命令用来按照新的顺序填充生成列表。列表可以实现非常快速的检索,即使是负载很重的站点。
7.过期项目处理。通过 Unix 时间作为关键字,用来保持列表能够按时间排序。对 currenttime 和 timeto_live 进行检索,完成查找过期项目的艰巨任务。另一项后台任务使用 ZRANGE...WITHSCORES 进行查询,删除过期的条目。
8.实时系统。使用位图来做布隆过滤器,例如实现垃圾邮件过滤系统的开发变的非常容易。
综上所述, Redis 的应用是非常广泛的,而且在实际使用中是非常有价值的。你可以让网站向 100万用户推荐新闻、可以实时显示最新的项目列表、在游戏中实时获得排名、获得全球排名等等。Redis 的出现,解决了传统关系数据库的短板,让开发变的更加简单和高效,大大提高了开发效率,也在用户体验上获得更加实时的体验。随着 Redis 的使用越来越广泛,将会有更多的开发者加入 Redis 的使用和开发上来。
我们再来看下为什么 Redis 中单线程快。很多程序员应该深有体会,其实其他很多语言单线程是非常慢的,但是为什么 Redis 的单线程快呢?
我觉得最大的原因是纯内存存储。正因为这个是主要原因,所以后面两个原因显得有点不太重要,即非阻塞 IO 和避免线程切换和竞态消耗。
你要清楚,首先 Redis 一次只运行一条命令。其次我们应该减少长命令,哪些是长命令呢?如 KEYS、FLUSHALL、FLUSHDB、SlowLua Script、MULTI/EXEC、OperateBig Value(Collection)。最后说明一点,其实Redis 不只是单线程,如果你去读源码,你就会发现有些指令绝不是单线程能够做的。如 Fysnc FileDescriptor、Close File Descriptor等。
Redis的 key 没什么好说,值得一提的就是 value 的五种数据类型。分别是字符串类型、数字、二进制、和 JSON 类型的数据。
那我们在实际生产环境中有哪些场景使用呢?如缓存、计数器(每次加 1 的计数)、分布式锁等场景都能看到。
接着我将列出与该数据类型相关的命令及使用说明。
1.GET、SET 和 DEL
这是 Redis 最简单的命令,如果你要得到一个 value 的值,只需要 GET key,就 ok 了。如果你要设置某个 key 的值,那就 SET key value,搞定。
2.INCR、DECR、INCRBY、DECRBY
INCR key:就是 key 自增 1,不存在,自增后 get(key)=1;
DECR key:就是 key 自减 1,不存在,自减后返回 -1;
INCRBY key k:自增 k ,不存在,则返回 k;
DECRBY key k:自减 k 。
如果你想访问某个主页的访问量,那可以用 INCR id:pageview。
我们实际开发中,常常会使用下面的伪代码。
publicVideoInfo
get(
longid
)
{
String redisKey = redisPrefix + id;
VideoInfo videoInfo = redis.
get(redisKey);
if
(videoInfo ==
null){
videoInfo = mysql.
get(id);
if
(videoInfo !=
null){
// 序列化
redis.
set(redisKey, serialize(videoInfo));
}
}
return
videoInfo;
}
3.SET、SETNX、SET xx
SET key value:不管 key 是否存在,都设置;
SETNX key value:key 不存在,才设置(相当于 add);
SET key value xx:key 存在,才设置(相当于 update)。
实际操作,见如下代码。
exists php
--> 0
set php good
-->OK
setnx php bad
-->0
set php best xx
-->ok
exists lua
--> 0
set lua hehe xx
-->(nil)
4.MGET、MSET
MGET key1 key2 key3 ...:批量获取 key,原子操作;
MSET key1 val2 key2 val2 key3 val3:批量设置 key-value。
实际开发的过程中,我们通常使用 MGET,因为 MGET 只有 1 次网络时间和 n 次命令时间。但是如果你使用 GET 的话,就是 n 次网络时间和 n 次命令时间。
使用 MGET 效率更高。
5.GETSET、APPEND、STRLEN
GETSET key newvalue:set key newvalue 并返回旧的 value,旧的 value 会被删除;
APPEND key value:将 value 追加到旧的 value 上;
STRLEN key:返回字符串的长度(注意中文)。
6.INCRBYFLOAT、GETRANGE、SETRANGE
INCRBYFLOAT key 3.5:在 key 上追加对应的值 3.5;
GETRANGE key start end:获取字符串指定下标所有的值;
SETRANGE key index value:设置指定下标所有对应的值。
说到 Hash,就要说到为什么我们要使用 Hash。我们在使用字符串的数据类型的时候,如果我们存储的是个对象,比如某个图书馆的会员,里面存储着会员的姓名、年龄、身份证信息、地址、借阅书籍、借阅时间……一系列的属性。
如果我们用 String 来存储的话,那我们就需要每次都序列化这个字符串,每次只要一修改某个属性,我们就要把一串属性都覆盖一遍。这样是不是非常麻烦?
这个时候,哈希就应运而生了。Hash 相当于 value 是一个Map,里面的所有属性我们都可以单独新增、修改或者删除,而不需要像字符串那样全部覆盖操作。
常用的命令有HGET、HSET、HGETALL。
HGET key field:获取存储在哈希表中指定字段的值。
HSET key field value:将哈希表 key 中的字段 field 的值设为 value。
HGETALL key:获取在哈希表中指定 key 的所有字段和值,生产环境不常用。
List 是一种简单的字符串的集合,是有顺序的。在实际生产环境中,我们时常会使用它。比如当我们需要获取某个数据的列表(例如粉丝列表)。
由于 Redis 的 List 是链表结构,我们可以非常轻松的实现消息排行等功能,还能用于消息队列等功能。
常用的命令有LPUSH、RPUSH、LPOP、RPOP、LRANGE。
LPUSH key value1 [value2]:将一个或多个值插入到列表头部;
RPUSH key value1 [value2]:在列表中添加一个或多个值;
LPOP key:移出并获取列表的第一个元素;
RPOP key:移除并获取列表最后一个元素;
LRANGE key start stop:获取列表指定范围内的元素。
Set 和 List 最大的不同是无序,Set 是没有顺序的。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
常用命令有SADD、SCARD、SNENVERS、SPOP。
SADD key member1:向集合添加一个或多个成员;
SCARD key:获取集合的成员数;
SMEMBERS key:返回集合中的所有成员;
SPOP key:移除并返回集合中的一个随机元素。
SortedSet 和 Set 最大的不同是前者是自动排序的,而后者是无序的。如果你需要一个有序的,但是不重复的数据结构的,就可以使用sorted set。
常用的命令有 ZADD、ZRANGE、ZREM、ZCARD。
ZADD key score1 member1:向有序集合添加一个或多个成员,或者更新已存在成员的分数;
ZRANGE key start stop:通过索引区间返回有序集合成指定区间内的成员;
ZREM key member:移除有序集合中的一个或多个成员;
ZCARD key:获取有序集合的成员数。
即发布(Publish)与订阅(Subscribe)。在 Redis 中,你可以设定对某一个 key 值进行消息发布及消息订阅,当 key 的值进行了消息发布后,所有订阅它的客户端都会收到相应的消息,这类似于 QQ、微信。
常用的命令有PSUBSCRIBE、PUBSUB、PUBLISH、SUBSCRIBE。
PSUBSCRIBE pattern:订阅一个或多个符合给定模式的频道;
PUBSUB subcommand:查看订阅与发布系统状态;
PUBLISH channel message:将信息发送到指定的频道;
SUBSCRIBE channel:订阅给定的一个或多个频道的信息;
UNSUBSCRIBE [channel [channel ...]]:指退订给定的频道。
我们一般认为 NoSQL 数据库都没有事务,恐怕要让你失望了。Redis 就支持事务,但并不是我们一般意义上的事务,如果你执行 exec 命令,途中断电或者服务器挂掉了,我们还是会发现 Redis 里一部分插入了,一部分未插入。
不过 Redis 提供了 WATCH 命令,我们可以对某个 key 来 watch 一下,然后再执行 Transactions。如果这个被Watch 的值进行了修改,那么这个 Transactions 会发现并拒绝执行。
redis127
.0.0.1:6381>
MULTI
OK
redis127
.0.0.1:6381>
SETbook-name"
JAVAProgrammingMasteringSeries"
QUEUED
redis127
.0.0.1:6381>
GETbook-name
QUEUED
redis127
.0.0.1:6381>
SADDtag"
java" "
Programming" "
MasteringSeries"
QUEUED
redis127
.0.0.1:6381>
SMEMBERStag
QUEUED
redis127
.0.0.1:6381>
EXEC
1)
OK
2) "
JAVAProgrammingMasteringSeries"
3) (
integer) 3
4) 1) "
java"
2) "
Programming"
3) "
MasteringSeries"
常用命令有 MULTI、EXEC、DISCARD。
MULTI:标记一个事务块的开始;
EXEC:执行所有事务块内的命令;
DISCARD:取消事务,放弃执行事务块内的所有命令;
UNWATCH:取消 WATCH 命令对所有 key 的监视;
WATCH key:监视 key,如果在事务执行之前 key 被其他命令所改动,那么事务将被打断。
Redis作为一个数据库,很多开发者还可以单独使用它。事实上,更多时候 Redis 是在数据库和代码中间作为一个中间件使用,如果你发现你目前的数据库出现瓶颈,那么你就可以通过 Redis 来优化。
使用 Jedis 非常简单,只需要 new 一个 Jedis 对象,然后调用 SET 和GET 就 OK 了,简单吧。
Jedis jedis =
newJedis(
"127.0.0.1",
6379);
// Ip, 端口,连接超时,读写超时
jedis.
set(
"hello",
"world");
jedis.
get(
"hello");
jedis.close();
// 记得要close掉,这是一个良好的习惯。
连接完 Jedis,我们再介绍一下常用的三种数据类型。
1.String
jedis.
set(
"hello",
"world");
//string是最常用的,用set设置
jedis.
get(
"hello");
// get获取
jedis.incr(
"counter");
//自增1
2.Hash
jedis.hset(
"hello",
"v1",
"f1");
//hash设置用hset
jedis.hset(
"hello",
"v2",
"f2");
jedis.hgetAll(
"hello");
// redis返回{v1=f1,v2=f2}
3.List
jedis.rpush(
"vss",
"1");
//list设置用rpush
jedis.rpush(
"vss",
"2");
jedis.rpush(
"vss",
"3");
jedis.lrange(
"vss",
0,
-1);
//redis返回[1,2,3]
当然,我们不仅要使用 Redis,还会使用 Jedis 的连接池。为什么要使用连接池呢?因为 Jedis 对象不是线程安全的,在多线程下会发生并发问题。为了避免这些问题,Jedis提供了 JedisPool 连接池,示例代码如下。
GenericObjectPoolConfig poolConfig =
newGenericObjectPoolConfig();
//JedisPool是单例的,需要new一个GenericObjectPoolConfig对象
JedisPool jedisPool =
newJedisPool(poolConfig,
"127.0.0.1",
6379);
// 建立连接池的连接
Jedis jedis =
null;
// 类似于new一个jedis对象
try{
jedis = jedisPool.getResource();
// 从连接池获取Jedis对象
jedis.
set(
"hello",
"world");
// 设置
}
catch(Exception e){
e.printStackTrace();
}
finally{
if
(jedis!=
null){
jedis.close();
// 注意不是关闭,而是归还到连接池
}
}
其他语言都可以参照上面的Java 的客户端,内容方面都差不多,而且 Jedis 主要是掌握其原理和流程,操作其实是非常简单的。
最后我们回顾下本文所讲述的内容。
首先,介绍了 Redis主要是用于缓存系统的,不同于一般关系数据库。
其次,我们介绍了Redis 的八大特性。通过这八大特性,我们可以把经常变化的数据放在 Redis 数据库中,并设置过期时间,到达时间 Redis 就会自动删除;还可以缓解服务器压力,如我们日常发微博,先会保存在Redis 数据库中,然后等数据库压力比较小的时候保存进关系数据库中。
然后,我们介绍了Redis 用在哪些场景下,介绍了Redis的7种常见数据类型的使用。相信通过这些介绍,你应该对 Redis 有个比较详细的认识。
最后,我们介绍了下Redis的Java客户端的简单使用。接下去我们将实际使用 Redis。