简单来说,Redis就是一个数据库,不过与传统数据库不同的是Redis的数据是存在内存中的,所以存写速度非常快,因此Redis被广泛应用于缓存方向。

另外,Redis也经常用来做分布式锁。Redis提供了多种数据类型来支持不同的业务场景。

除此之外,Redis支持事物、持久化、LUA脚本、LRU驱动事件、多种集群方案。

首先,为什么用Redis(缓存)?

主要从“高性能”和“高并发”这两点来看待这个问题。

高性能

redis与数据库 redis和数据库_缓存

假如用户第一次访问数据库中的某些数据,这个过程就会比较慢,因为是从硬盘中读取的数据。然后将该用户访问的数据存在缓存中,这样当用户再次访问这些数据的时候就可以直接从缓存中读取数据了。

操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变了之后,同步改变关村中相应的数据即可。

高并发

redis与数据库 redis和数据库_数据_02

直接操作缓存能够承受的请求时远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存中读取数据而不用去数据库中读取数据。

为什么要用Redis而不用map/guava做缓存

缓存分为本地缓存和分布式缓存。以Java为例,使用自带的map或者guava实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着JVM的销毁而结束。

并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。使用Redis或Memcached之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持Redis或Memcached服务的高可用性,整个程序架构上较为复杂。

Redis和Memcached的区别

现在公司一般都是用Redis来实现缓存,而且Redis自身也越来越强大了。

对于Redis和Memcached区别主要有以下四点:

(1)Redis支持更丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的K/V类型的数据,同时还提供list、set、zset、hash等数据结构的存储。Memcache支持简单的数据类型String.

(2)Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memcache把数据全部存在内存中。

(3)集群模式:Memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是Redis目前是原生支持Cluster模式的。

(4)Memcached是多线程:非阻塞IO复用的网络模型,Redis使用单线程的多路IO复用模型。

redis与数据库 redis和数据库_Redis_03

Redis常见数据结构以及使用场景分析

(1)String

常用命令:set、get、decr、incr、mget等。

String数据结构是简单的Key-Value类型,Value其实不仅可以是Stirng,也可以是数字,常规Key-Value缓存应用,常规计数,微博数,粉丝数等。

(2)Hash

常用命令:hget、hset、hgetall等。

Hash是一个String类型的Field和Value的映射表,Hash特别使用与存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。比如我们可以Hash数据结构来存储用户信息,商品信息等等。

例如用hash类型存放一些信息:

key=JavaUser293847
value={
"id":1,
"name":"teng",
"age":22
}

(3)List

常用命令:lpush、rpush、lpop、rpop、lrange等。

List就是链表,Redis List的应用场景非常多,也是Redis最重要的数据结构之一。比如微博的关注列表,粉丝列表,消息列表等功能都可以用Redis的List结构来实现。Redis List的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。另外可以通过lrang命令,就是从某个元素开始读取多少个元素,可以基于List实现分页查询。这是很棒的一个功能,基于Redis实现简单的高性能分页,可以做类似微博那种下拉不断分页的东东,性能比较高。

(4)Set

常用命令:sadd、spop、smembers、sunion等。

Set对外提供的功能与List类似是一个列表的功能,特殊之处在于Set是可以自动排重的。当你需要存储一个列表数据,又不希望出现重复数据时,Set是一个很好的选择。并且Set提供了判断某个成员是否在一个Set集合内的重要接口,这个也是List所不能提供的。你可以基于Set轻易实现交集、并集、差集的操作。

比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合,Redis可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。

这个过程也就是求交集的过程,具体命令如下:

sinterstore key1,key2,key3  将交集存在key1内

(5)Sorted Set

常用命令:zadd,zrange,zrem,zcard等

和Set相比,Sorted Set增加了一个权重参数Score,使得集合中的元素能够按Score进行有序的排列。例如,在直播系统中,实时排行信息包含直播间在线的用户列表,各种礼物排行榜,弹幕消息等信息,适合使用Redis中的Sorted Set结构进行存储。

(6)Redis设置过期时间

Redis中有个设置过期时间的功能,即对存储在Redis数据库中的值可以设置一个过期时间。作为一个缓存数据库,这是非常实用的。如我们一般项目中的Token或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。我们Set Key的时候,都可以给一个Expire Time,就是过期时间,通过过期时间我们可以指定这个Key可以存活的时间。如果你设置了一批Key只能存活1个小时,那么接下来1小时后,Redis是怎么对这批Key进行删除的?

答案是:定期删除+惰性删除。通过名字大概就鞥猜出这两个删除方式的意思了。

      ①定期删除:Redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的,为什么要随机呢?你想一想假如Redis存了十几万个Key,每隔100ms就遍历所有的设置过期时间key的话,就会给CPU带来很大的负载。

     ②惰性删除:定期删除可能会导致很多过期key到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个key,才会被Redis给删除掉。

但是仅仅通过设置过期时间还是有问题的,我们想一下:如果定期删除漏掉了很多过期的key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期的key堆积在内存里,导致Redis内存块耗尽了,怎么解决这个问题呢?

Redis内存淘汰机制

Mysql里有2000w数据,Redis只存20w数据,如何保证Redis中的数据都是热点数据?

Redis提供6种数据淘汰策略:

(1)volatile-lru:从已经设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。

(2)volatile-ttl:从已经设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰。

(3)volatile-random:从已经设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。

(4)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)

(5)allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰。

(6)no-enviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错,这个应该没人使用的。

Redis持久化机制

怎么保证Redis挂掉之后再重启数据可以进行恢复?很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面。

大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置。

Redis不同于Memcached的很重要的一点就是,Redis支持持久化,而且支持两种不同的持久化操作。

Redis的一种持久化方法叫快照,另一种方式是只追加文件。这两种方法各有千秋,下面详细介绍两种方法并如何选择适合自己的持久化方法。

(1)快照(Snapshotting)持久化(RDB)

Redis可以通过创建快照来获取存储在内存里面的数据在某个时间点上的副本。

Redis创建快照之后,可以对快照进行备份,可以将快照赋值到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用。

快照持久化是Redis默认采用的持久化方式,在redis.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命令创建快照。

(2)AOF(APPEND-ONLY FILE)持久化

与快照持久化相比,AOF持久化的实时性更好,因此已经成为主流的持久化方案。默认情况下Redis没有开启AOF方式的持久化,可以通过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还会有优雅的放慢自己的速度以便适应硬盘的最大写入速度。

Reids4.0对于持久化机制的优化

Redis 4.0开始支持RDB和AOF的混合持久化(默认关闭,可以通过配置项aof-use-rdb-preamble开启)。如果把混合持久化打开,AOF重写的时候就直接把RDB的内容写到AOF文件开头。这样做的好处是可以结合RDB和AOF的优点,快读加载同时避免丢失过多的数据。当然缺点是AOF里面的RDB部分是压缩格式不再是AOF格式,可读性比较差。

补充内容:AOF重写

aof重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库装填一样,但是体积更小。AOF重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无需对现有AOF文件进行任何读入、分析或者写入操作。

在执行BGREWRIEAOF命令时,Redis服务器会维护一个AOF重写缓冲区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令。当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致。最后,服务器用新的AOF文件替换旧的AOF文件,以此来完成AOF文件重写操作。

Redis事物

Redis通过MULTI、EXEC、WATCH等命令来实现事务的功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制。并且在事务执行期间,服务器不会中断事务而改去执行其他哭护短的命令请求,它会将事物中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。在传统的关系式数据库中,常常用ACID性质来检验事务功能的可靠性和安全性。在Redis中,事务总是具有原子性、一致性和隔离性。并且当Redis运行在某种特定的持久化模式下时,事务也是具有持久性。

缓存雪崩和缓存穿透问题解决方案

(1)缓存雪崩

缓存同意时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

redis与数据库 redis和数据库_数据_04

解决办法:

事前:尽量保证整个Redis集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。

事中:本地Ehcache缓存+Hystrix限流&降级,避免Mysql崩掉。

事后:利用Redis持久化机制保存的数据尽快恢复缓存。

(2)缓存穿透

一般是黑客故意去请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决办法,有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bit map中。

一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法,如果一个查询返回数据为空,我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过5分钟。

如何解决Redis的并发竞争key问题

所谓Redis的并发竞争key的问题也就是多个系统同时对一个key进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!

推荐一种方案,分布式锁(zookeeper和Redis都可以实现分布式锁)如果不存在Redis的并发竞争key问题,不要使用分布式锁,这样会影响性能。

基于zookeeper临时有序节点可以实现的分布式锁,大致思想为:每个客户端对某个方法加锁时,在Zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬间有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需要将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。在实践中,当然是以可靠性为主。所以首推ZooKeeper。

如何保证缓存与数据库双写时的数据一致性

你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?

一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况。最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。