简单来说Redis就是一个数据库,不过与传统的数据库不同的是Redis的数据是存在内存中的,所以存写速度非常快,因此Redis被广泛应用于缓存方向。
另外,Redis也经常用来做分布式锁。Redis提供了多种数据类型来支持不同的业务场景。
除此之外,Redis支持事务、持久化、LUA脚本、LRU驱动事件、多种集群方案。
1、为什么要用Redis,为什么要用缓存?
主要从“高性能”和“高并发”这两点来看待这个问题。
1.1、高性能
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在缓存中,这样下一次再次访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变之后,同步改变缓存中相应的数据即可!
1.2、高并发
直接操作缓存能够承受的请求远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
2、为什么要用Redis而不用map/guava做缓存?
缓存分为本地缓存和分布式缓存。以Java为例,使用自带的map或者guava实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着JVM的销毁而结束。并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
使用Redis或者Memcached之类的称为分布式缓存,在多实例的情况下,各实例公用一份缓存数据,缓存具有一致性。缺点是需要保持Redis或Memcached服务的高可用,整个程序架构上较为复杂。
3、Redis和Memcached的区别
现在公司一般都是用Redis来实现缓存,而且Redis自身也越来越强大了!
对于Redis和Memcached我总结了下面四点:
(a)、Redis支持丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的K/V类型的数据,同时还提供list、set、zset、hash等数据结构的存储。Memcached支持简单的数据类型String。
(b)、Redis支持数据的持久化:可以将内存中的数据保持在磁盘中,重启的时候可以再次加载使用,而Memcached把数据全部存在内存之中。
(c)、集群模式:Memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是Redis目前是原生支持Cluster模式的。
(d)、Memcached是多线程:非阻塞IO复用的网络模式;Redis使用单线程的多路IO复用模式。
对比参数 | Redis | Memcached |
类型 |
1、支持内存 2、非关系型数据库 |
1、支持内存 2、key-value键值对形式 3、缓存系统 |
数据存储类型 |
1、String 2、List 3、Set 4、Hash 5、ZSet(Sorted Set) |
1、文本型 2、二进制类型【新版增加】 |
查询【操作】类型 |
1、批量操作 2、事务支持【虽然是假的事务】 3、每个类型不同的CRUD |
1、CRUD 2、少量的其他命令 |
附加功能 |
1、发布/订阅模式 2、主从分区 3、序列化支持 4、脚本支持【Lua脚本】 |
1、多线程服务支持 2、多线程、非阻塞模式 |
网络IO模型 | 1、单进程模式 | |
事件库 | 自封装简易事件库AeEvent | 贵族血统LibEvent事件库 |
持久化支持 |
1、RDB 2、AOF |
不支持 |
4、Redis常见的数据结构以及使用场景分析
(a)、String
常用命令:set、get、decr,incr,mget等。
String数据结构是简单的Key-Value类型,Value其实不仅可以是String,也可以是数字。常规Key-Vaule缓存应用;常规计数:微博数、粉丝数等。
127.0.0.1:6379> set var1 aaa OK 127.0.0.1:6379> get var1 "aaa" 127.0.0.1:6379> set var2 1 OK 127.0.0.1:6379> get var2 "1" 127.0.0.1:6379> incr var2 (integer) 2 127.0.0.1:6379> get var2 "2" 127.0.0.1:6379> decr var2 (integer) 1 127.0.0.1:6379> get var2 "1" 127.0.0.1:6379> mget var1 var2 1) "aaa" 2) "1" 127.0.0.1:6379>
(b)、Hash
常用命令:hget、hset、hmset,hmget,hgetall等。
Hash是一个String类型的Field和Value的映射表,Hash特别适合用于存储对象。后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。比如我们可以Hash数据结构来存储用户信息,商品信息等。
127.0.0.1:6379> hset user1 id 1 (integer) 1 127.0.0.1:6379> hset user1 name zhouguowei (integer) 1 127.0.0.1:6379> hset user1 age 30 (integer) 1 127.0.0.1:6379> hset user1 location "Zhengzhou Henan" (integer) 1 127.0.0.1:6379> hget user1 id "1" 127.0.0.1:6379> hget user1 location "Zhengzhou Henan" 127.0.0.1:6379> hgetall user1 1) "id" 2) "1" 3) "name" 4) "zhouguowei" 5) "age" 6) "30" 7) "location" 8) "Zhengzhou Henan" 127.0.0.1:6379> hset user1 name "Function" (integer) 0 127.0.0.1:6379> hgetall user1 1) "id" 2) "1" 3) "name" 4) "Function" 5) "age" 6) "30" 7) "location" 8) "Zhengzhou Henan" 127.0.0.1:6379> hmset user2 id 2 name liuda age 28 location "Beijing" OK 127.0.0.1:6379> hgetall user2 1) "id" 2) "2" 3) "name" 4) "liuda" 5) "age" 6) "28" 7) "location" 8) "Beijing" 127.0.0.1:6379> hmget user2 id name age 1) "2" 2) "liuda" 3) "28" 127.0.0.1:6379>
(c)、List
常用命令:lpush、rpush、lpop、rpop、lrange等、
List就是链表,Redis List的应用场景非常多,也是Redis最重要的数据结构之一。
比如微博的关注列表,粉丝列表,消息列表等功能都可以用Redis的List结构来实现。
Redis List的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
另外还可以通过lrange命令,就是从某个元素开始读取多少个元素,可以基于List实现分页查询。这是很棒的一个功能,基于Redis实现简单的高性能分页;可以做类似微博那种下拉不断分页的东西。
127.0.0.1:6379> lpush list1 a (integer) 1 127.0.0.1:6379> lpush list1 b (integer) 2 127.0.0.1:6379> lrange list1 0 1 1) "b" 2) "a" 127.0.0.1:6379> rpush list c (integer) 1 127.0.0.1:6379> lrange list1 0 2 1) "b" 2) "a" 127.0.0.1:6379> rpush list1 c (integer) 3 127.0.0.1:6379> lrange list1 0 2 1) "b" 2) "a" 3) "c" 127.0.0.1:6379> lpop list1 "b" 127.0.0.1:6379> lrange list1 0 1 1) "a" 2) "c" 127.0.0.1:6379> lpop list1 "a" 127.0.0.1:6379> lrange list1 0 1 1) "c" 127.0.0.1:6379>
(d)、Set
常用命令:sadd、spop、srem、smembers、sdiff、sdiffstore、sinter,sinterstore、sunion等。
Set对外提供的功能与List类似是一个列表的功能,特殊之处在于Set是可以自动排重的。当你需要存储一个列表数据,又不希望出现重复数据时,Set是一个很好的选择。并且Set提供了判断某个成员是否在一个Set集合内的重要接口,这个也是List所不能提供的。你可以基于Set轻易实现交集、并集、差集的操作。
比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常方便的时间内如共同关注、共同粉丝、共同喜好等功能。
127.0.0.1:6379> sadd myset1 hello (integer) 1 127.0.0.1:6379> sadd myset1 world (integer) 1 127.0.0.1:6379> smembers myset1 1) "hello" 2) "world" 127.0.0.1:6379> sadd myset1 hello (integer) 0 127.0.0.1:6379> smembers myset1 1) "hello" 2) "world" 127.0.0.1:6379> sadd myset1 fine (integer) 1 127.0.0.1:6379> smembers myset1 1) "hello" 2) "fine" 3) "world" 127.0.0.1:6379> srem myset1 fine (integer) 1 127.0.0.1:6379> smembers myset1 1) "hello" 2) "world" 127.0.0.1:6379> spop myset1 "hello" 127.0.0.1:6379> smembers myset1 1) "world" 127.0.0.1:6379>
sadd:向名称为Key的set中添加元素,同一集合中不能出现相同的元素值。(用法:sadd set集合名称 元素值)。
srem:删除名称为key的set中的元素。(用法:srem set集合名称 要删除的元素值)。
spop:随机返回并删除名称为key的set中一个元素。(用法:srem set集合名称)。
127.0.0.1:6379> sadd myset2 two one (integer) 2 127.0.0.1:6379> smembers myset2 1) "two" 2) "one" 127.0.0.1:6379> sadd myset3 three two (integer) 2 127.0.0.1:6379> smembers myset3 1) "two" 2) "three" 127.0.0.1:6379> sdiff myset2 myset3 1) "one" 127.0.0.1:6379>
sdiff:返回所有给定key与第一个key的差集。(用法:sdiff set集合1 set集合2)。
127.0.0.1:6379> smembers myset2 1) "two" 2) "one" 127.0.0.1:6379> smembers myset3 1) "two" 2) "three" 127.0.0.1:6379> sdiffstore myset4 myset2 myset3 (integer) 1 127.0.0.1:6379> smembers myset4 1) "one" 127.0.0.1:6379>
sdiffstore:返回所有给定key与第一个key的差集,并将结果存为另一个key。(用法:sdiffstore 差集数据存入的集合 set集合1 set集合2)。
127.0.0.1:6379> smembers myset2 1) "two" 2) "one" 127.0.0.1:6379> smembers myset3 1) "two" 2) "three" 127.0.0.1:6379> sinter myset2 myset3 1) "two" 127.0.0.1:6379>
sinter:返回所有给定key的交集。(用法:sinter set集合1 set集合2)。
127.0.0.1:6379> smembers myset2 1) "two" 2) "one" 127.0.0.1:6379> smembers myset3 1) "two" 2) "three" 127.0.0.1:6379> sinterstore myset5 myset2 myset3 (integer) 1 127.0.0.1:6379> smembers myset5 1) "two" 127.0.0.1:6379>
sinterstore:返回所有给定Set集合的交集,并将结果存为另一个set集合。(用法:sinterstore 交集结果集合 set集合1 set集合2)。
127.0.0.1:6379> smembers myset2 1) "two" 2) "one" 127.0.0.1:6379> smembers myset3 1) "two" 2) "three" 127.0.0.1:6379> sunion myset2 myset3 1) "two" 2) "three" 3) "one" 127.0.0.1:6379>
sunion:返回所有给定key的并集。(用法:sunion set集合1 set集合2)。
127.0.0.1:6379> smembers myset2 1) "two" 2) "one" 127.0.0.1:6379> smembers myset3 1) "two" 2) "three" 127.0.0.1:6379> sunionstore myset6 myset2 myset3 (integer) 3 127.0.0.1:6379> smembers myset6 1) "two" 2) "three" 3) "one" 127.0.0.1:6379>
sunionstore:返回所有给定key的并集,并将结果存为另一个set集合。(用法:sunionstore 并集结果集合 set集合1 set集合2)。
(e)、Zset(Sorted Set)
常用命令:zadd、zrange、zrem、zcard等。
和Set相比,Sorted Set增加了一个权重参数Score,使得集合中的元素能够按照Score进行排序。
举例:在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息纬度的消息排行榜)等信息,适合使用Redis中的Sorted Set结构进行存储。
127.0.0.1:6379> zadd zset1 1 two (integer) 1 127.0.0.1:6379> zadd zset1 2 one (integer) 1 127.0.0.1:6379> zadd zset1 3 seven (integer) 1 127.0.0.1:6379> zrange zset1 0 -1 1) "two" 2) "one" 3) "seven" 127.0.0.1:6379> zrange zset1 0 -1 withscores 1) "two" 2) "1" 3) "one" 4) "2" 5) "seven" 6) "3" 127.0.0.1:6379>
zadd:向名称为key的zset中添加元素member,score用于排序。如果该元素存在,则更新其顺序。(用法:zadd 有序集合 顺序编号 元素值)。
127.0.0.1:6379> zrange zset1 0 -1 withscores 1) "two" 2) "1" 3) "one" 4) "2" 5) "seven" 6) "3" 127.0.0.1:6379> zrem zset1 one (integer) 1 127.0.0.1:6379> zrange zset1 0 -1 withscores 1) "two" 2) "1" 3) "seven" 4) "3" 127.0.0.1:6379>
zrem:删除名称为key的zset中的元素。(用法:zrem 有序集合 要删除的元素值)。
127.0.0.1:6379> zrange zset1 0 -1 withscores 1) "two" 2) "1" 3) "seven" 4) "3" 127.0.0.1:6379> zincrby zset1 5 seven "8" 127.0.0.1:6379> zrange zset1 0 -1 withscores 1) "two" 2) "1" 3) "seven" 4) "8" 127.0.0.1:6379>
zincrby:如果在名称为key的zset中已经存在元素member,则该元素的score增加increment,否则向该集合中添加该元素,其score的值为increment.即对元素的顺序号进行增加或减少操作。(用法:zincrby 有序集合 increment 指定的元素值)。
127.0.0.1:6379> zrange zset1 0 -1 withscores 1) "two" 2) "1" 3) "seven" 4) "8" 127.0.0.1:6379> zcard zset1 (integer) 2 127.0.0.1:6379>
zcard:返回集合中元素个数。(用法:zcard 有序集合)。
5、Redis设置过期时间
Redis中有个设置过期时间的功能,即对存储在Redis数据库中的值可以设置一个过期时间。作为一个缓存数据库,这个是非常实用的。
如我们一般项目中的Token或者一些登录信息,尤其是短信验证都会有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。
我们Set Key的时候,都可以给一个Expire Time,就是过期时间,通过过期时间我们可以指定这个Key可以存活的时间。
如果你设置了一批Key只能存活一个小时,那么接下来一小时后,Redis是怎么对这批Key进行删除的?
答案是:定期删除+惰性删除。通过名字大概就能猜出这两个删除方式的意思了:
(a)、定期删除:Redis默认是每个100ms就随机抽取一些设置了过期时间的Key,检查其是否过期,如果过期就删除。
注意:这里是随机抽取的。为什么要随机抽取呢?你想一想假如Redis存了几十万个Key,每个100ms就遍历所有的设置过期时间的Key的话,就会给CPU带来很大的负载!
(b)、惰性删除:定期删除可能会导致很多过期Key到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期Key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下哪个Key,才会被Redis给删除掉。这就是所谓的惰性删除,也是够懒的。
但是仅仅通过设置过期时间还是有问题的。我们想一下:如果定期删除漏掉了很多过期的Key,然后你也没及时去查,也就没有走惰性删除,此时会怎么样?如果大量过期Key堆积在内存里,导致Redis内存块耗尽了。怎么解决这个问题呢?
6、内存淘汰机制
MySQL里有2000W数据,Redis中只存20W数据,如何保证Redis中的数据都是热点数据?
Redis配置文件redis.conf中有关注释,我这里就不贴了,大家可以自行查阅或者通过这个网址查看:http://download.redis.io/redis-stable/redis.conf。
Redis提供了6中数据淘汰策略:
(a)、volatile-lru:从已设置过期时间的数据集(sever.db[i].expires)中挑选最近最少使用的数据淘汰。
(b)、volatile-ttl:从已设置过期时间的数据集(sever.db[i].expires)中挑选将要过期的数据淘汰。
(c)、volatile-random:从已设置过期时间的数据集(sever.db[i].expires)中任意选择数据淘汰。
(d)、allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)。
(e)、allkey-random:从数据集(server.db[i].dict)中任意选择数据淘汰。
(f)、no-enviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没有人使用吧!
7、持久化机制
怎么保证Redis挂掉之后重启数据可以进行恢复?很多时候我们需要持久化数据也就是内存中的数据写入到硬盘里面。
大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者为了防止系统故障而将数据备份到一个远程位置。
Redis不同于Memcached的重要一点就是,Redis支持持久化,而且支持两种不同的持久化操作。
Redis的一种持久化方式叫快照(anapshotting,RDB),另一种方式是只追加文件(append-only file,AOF)。
这两种方法各有千秋,下面我会详细讲这两种持久化方法是什么,怎么用,如何选择适合自己的持久化方法。
(a)、快照(snapshotting)持久化(RDB)
Redis可以通过创建快照来获取存储在内存里面的数据在某个时间点上的副本。
Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用。
快照持久化是Redis默认采用的持久化方式,在rredis.conf配置文件中默认有此配置:
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。 save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。 save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
(b)、AOF(append-only file)持久化
与快照持久化相比,AOF持久化的实时性更好,因此已成为主流的持久化方案。
默认情况下,Redis没有开启AOF(append-only file)方式的持久化,可以通过appendonly参数开启:
appendonly yes
开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件。
AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数配置的,默认的文件名是appendonly.aof。
在Redis的配置文件中存在三种不同的AOF持久化方式,它们分别是:
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度 appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘 appendfsync no #让操作系统决定何时进行同步
为了兼顾数据和写入性能,用户可以考虑appendfsync everysec选项,让Redis每秒同步一次AOF文件,Redis性能几乎没收到任何影响。
而这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis还会优雅的放慢自己的速度以便适应磁盘的最大写入速度。
8、Redis 4.0对持久化机制的优化
Redis 4.0 开始支持RDB和AOF的混合持久化(默认关闭,可以通过配置项aof-use-rdb-preamble开启)。
如果把混合持久化打开,AOF重写的时候就直接把RDB的内容写到AOF文件开头。
这样做的好处是可以结合RDB和AOF的优点,快速加载同时避免丢失过多的数据。
当然缺点也是有的,AOF里面的RDB部分是压缩格式,不再是AOF格式,可读性较差。
9、补充内容:AOF重写
AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,但体积更小。
AOF重写是一个歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有AOF文件进行任何读入、分析或者写入操作。
在执行BGREWRITEAOF命令时,Redis服务器会维护一个AOF重写缓存区,该缓存区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令。
当子进程完成创建新AOF文件的工作之后,服务器会重写缓存区中的所有内容追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致。
最后,服务器用新的AOF文件替换旧的AOF文件,以此来完成AOF文件重新操作。
10、Redis事务
Redis通过MULTI、EXEC、WATCH等命令来实现事务(transaction)功能。
事务提供了一种将多个命令请求打包,然后一次性,按顺序地执行多个命令的机制。
并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令执行完毕,然后才去处理其他客户端的命令请求。
在传统的关系数据库中,常常用ACID性质来验证事务功能的可靠性和安全性。
在Redis中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当Redis运行在某种特定的持久化模式下时,事务也具有持久性(Durability)。
127.0.0.1:6379> multi OK 127.0.0.1:6379> set a aaa QUEUED 127.0.0.1:6379> set b bbb QUEUED 127.0.0.1:6379> set c ccc QUEUED 127.0.0.1:6379> exec 1) OK 2) OK 3) OK 127.0.0.1:6379> keys * 1) "name" 2) "age" 3) "school" 4) "b" 5) "a" 6) "c" 127.0.0.1:6379> multi OK 127.0.0.1:6379> set aa aaa QUEUED 127.0.0.1:6379> set bb bbb QUEUED 127.0.0.1:6379> discard OK 127.0.0.1:6379>
11、缓存雪崩和缓存穿透问题解决方案
1、缓存雪崩
简介:缓存同一时间大面积失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决办法:
(a)、事前:尽量保证整个Redis集群的高可用,发现机器宕机尽快补上。选择合适的内存淘汰策略。
(b)、事中:本地Ehcache + Hystrix限流&降级,避免MySQL崩掉。
(c)、事后:利用Redis持久化机制保存的数据尽快恢复缓存。
2、缓存穿透
简介:一般是黑客故意去请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量的请求而崩掉。
解决办法:有很多方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中。
一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管数据不存在,还是系统故障),我们任然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
12、如何解决Redis的并发竞争Key问题
所谓Redis的并发竞争Key的问题也就是多个系统同时对一个Key进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同。
推荐一种方案:分布式锁(Zookeeper和Redis都可以实现分布式锁)。如果不存在Redis的并发竞争Key问题,不要使用分布式锁,这样会影响性能。
基于ZooKeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在ZooKeeper上的该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。
判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。放释放锁的时候,只需将这个瞬时节点删除即可。
同时,其可以避免服务器宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。
13、如何保证缓存与数据库双写时数据一致性
你只要要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性问题,那么你如何解决一致性问题?
一般来说,就是如果你的系统不是严格要求缓存 + 数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致性的情况。
最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。
串行话之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。