简单来讲Redis就是一个数据库,不过与传统的数据库不一样的是Redis的数据是存在内存中的,因此存写速度很是快,所以Redis被普遍应用于缓存方向。redis
另外,Redis也常常用来作分布式锁。Redis提供了多种数据类型来支持不一样的业务场景。数据库
除此以外,Redis支持事务、持久化、LUA脚本、LRU驱动事件、多种集群方案。缓存
主要从“高性能”和“高并发”这两点来看待这个问题。安全
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,由于是从硬盘上读取的。将该用户访问的数据存在缓存中,这样下一次再次访问这些数据的时候就能够直接从缓存中获取了。操做缓存就是直接操做内存,因此速度至关快。若是数据库中的对应数据改变以后,同步改变缓存中相应的数据便可!bash
直接操做缓存可以承受的请求远远大于直接访问数据库的,因此咱们能够考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用通过数据库。服务器
缓存分为本地缓存和分布式缓存。以Java为例,使用自带的map或者guava实现的是本地缓存,最主要的特色是轻量以及快速,生命周期随着JVM的销毁而结束。而且在多实例的状况下,每一个实例都须要各自保存一份缓存,缓存不具备一致性。网络
使用Redis或者Memcached之类的称为分布式缓存,在多实例的状况下,各实例公用一份缓存数据,缓存具备一致性。缺点是须要保持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 |
类型 | 一、支持内存 二、非关系型数据库 |
一、支持内存 二、key-value键值对形式 三、缓存系统 |
数据存储类型 | 一、String 二、List 三、Set 四、Hash 五、ZSet(Sorted Set) |
一、文本型 二、二进制类型【新版增长】 |
查询【操做】类型 | 一、批量操做 二、事务支持【虽然是假的事务】 三、每一个类型不一样的CRUD |
一、CRUD 二、少许的其余命令 |
附加功能 | 一、发布/订阅模式 二、主从分区 三、序列化支持 四、脚本支持【Lua脚本】 |
一、多线程服务支持 二、多线程、非阻塞模式 |
网络IO模型 | 一、单进程模式 | |
事件库 | 自封装简易事件库AeEvent | 贵族血统LibEvent事件库 |
持久化支持 | 一、RDB 二、AOF |
不支持 |
(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 有序集合)。
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内存块耗尽了。怎么解决这个问题呢?
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:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操做会报错。这个应该没有人使用吧!
怎么保证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还会优雅的放慢本身的速度以便适应磁盘的最大写入速度。
Redis 4.0 开始支持RDB和AOF的混合持久化(默认关闭,能够经过配置项aof-use-rdb-preamble开启)。
若是把混合持久化打开,AOF重写的时候就直接把RDB的内容写到AOF文件开头。
这样作的好处是能够结合RDB和AOF的优势,快速加载同时避免丢失过多的数据。
固然缺点也是有的,AOF里面的RDB部分是压缩格式,再也不是AOF格式,可读性较差。
AOF重写能够产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态同样,但体积更小。
AOF重写是一个歧义的名字,该功能是经过读取数据库中的键值对来实现的,程序无须对现有AOF文件进行任何读入、分析或者写入操做。
在执行BGREWRITEAOF命令时,Redis服务器会维护一个AOF重写缓存区,该缓存区会在子进程建立新AOF文件期间,记录服务器执行的全部写命令。
当子进程完成建立新AOF文件的工做以后,服务器会重写缓存区中的全部内容追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致。
最后,服务器用新的AOF文件替换旧的AOF文件,以此来完成AOF文件从新操做。
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>
一、缓存雪崩
简介:缓存同一时间大面积失效,因此,后面的请求都会落到数据库上,形成数据库短期内承受大量请求而崩掉。
解决办法:
(a)、事前:尽可能保证整个Redis集群的高可用,发现机器宕机尽快补上。选择合适的内存淘汰策略。
(b)、事中:本地Ehcache + Hystrix限流&降级,避免MySQL崩掉。
(c)、过后:利用Redis持久化机制保存的数据尽快恢复缓存。
二、缓存穿透
简介:通常是黑客故意去请求缓存中不存在的数据,致使全部的请求都落到数据库上,形成数据库短期内承受大量的请求而崩掉。
解决办法:有不少方法能够有效地解决缓存穿透问题,最多见的则是采用布隆过滤器,将全部可能存在的数据哈希到一个足够大的bitmap中。
一个必定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
另外也有一个更为简单粗暴的方法(咱们采用的就是这种),若是一个查询返回的数据为空(无论数据不存在,仍是系统故障),咱们任然把这个空结果进行缓存,但它的过时时间会很短,最长不超过五分钟。
所谓Redis的并发竞争Key的问题也就是多个系统同时对一个Key进行操做,可是最后执行的顺序和咱们指望的顺序不一样,这样也就致使告终果的不一样。
推荐一种方案:分布式锁(Zookeeper和Redis均可以实现分布式锁)。若是不存在Redis的并发竞争Key问题,不要使用分布式锁,这样会影响性能。
基于ZooKeeper临时有序节点能够实现的分布式锁。大体思想为:每一个客户端对某个方法加锁时,在ZooKeeper上的该方法对应的指定节点的目录下,生成一个惟一的瞬时有序节点。
判断是否获取锁的方式很简单,只须要判断有序节点中序号最小的一个。放释放锁的时候,只需将这个瞬时节点删除便可。
同时,其能够避免服务器宕机致使的锁没法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。
你只要要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就必定会有数据一致性问题,那么你如何解决一致性问题?
通常来讲,就是若是你的系统不是严格要求缓存 + 数据库必须一致性的话,缓存能够稍微的跟数据库偶尔有不一致性的状况。
最好不要作这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就能够保证必定不会出现不一致的状况。
串行话以后,就会致使系统的吞吐量会大幅度的下降,用比正常状况下多几倍的机器去支撑线上的一个请求。