Redis高级应用总结

Redis的线程模型


Redis是单进程单线程的,但是使用的是单线程非阻塞的多路IO复用的模型。多线程模型会导致线程的生成、回收、线程上下文的切换,这些都会导致性能的降低。多线程牵涉到线程同步,还需要加锁,但是锁的使用也会导致性能降低。那么什么是多路IO复用呢?

官方的解释:多路I/O复用模型是利用select、poll、epoll可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。

单进程单线程的好处:

1.代码清晰,处理逻辑简单。

2.不用考虑锁,没有因为使用锁导致的性能下降、死锁问题。

3.没有多线程的线程上下文切换、线程生成回收导致的性能消耗。

单进程单线程的缺点:

在到处是CPU多核的时代,无法充分发挥CPU的多核优势,只能在一台服务器上部署多个实例的方案来使用多核。

问题:既然Redis是单线程的,那么是不是Redis的服务上只有一个线程在运行?

单线程指的是Redis处理我们的网络请求是单线程的,一个Redis服务上运行不止这一个线程,还有其他的比如数据持久化的线程。

Memcached使用的是多线程,处理能力非常强,Redis正因为使用了多路IO复用的机制才使单线程的性能不比多线程的差。

Redis的过期策略及内存淘汰机制


Redis中写入的数据超过了内存的容量,Redis使用什么机制来删除多余的数据?

Redis使用的是定期删除+惰性删除的策略。为什么不用定时删除?定时删除需要一个定时器来不停的监控Redis中的数据,过期了则删除,虽然内存得到了释放,但是定时器不停的轮询是十分消耗CPU的,在高并发情况下能明显降低性能。

定期删除+惰性删除是如何工作的?定期删除,Redis默认是100ms检查一次,是否有过期的数据,有了则删除。Redis并不是每个100ms都检查所有的数据,如果数据庞大,100ms根本检查不完,它是随机的抽取一部分数据进行检查。采用定期删除的策略有一个问题,就是好多过期的数据没有及时删除。于是,惰性删除就出现了。惰性删除的意思是,当程序读取某个数据时,Redis会主动的查看下这条数据是否设置了过期时间并且是否过期,如果过期直接删除。

定期删除+惰性删除还存在一个问题,如果有些数据过期了,但是定时删除没有检查到它,并且程序也没有请求这些数据,那么它会一直在内存中,这该怎么处理呢?这时候就要内存淘汰机制了。

在redis.conf的配置文件中有一行:# maxmemory-policy volatile-lru

这个就是设置内存淘汰的。有以下几个选项:

1. noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。

2. allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。推荐使用。

3. allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。

4. volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。

5. volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。

6. volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

Redis的分布式锁怎么实现


首先说明下什么是分布式锁。分布式系统下有一个理论CAP理论,这个理论的意思是任何分布式系统不可能同时满足强一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足其中的两个。互联网系统都是牺牲最终一致性来换取高可用性。最终一致性要在合理的时间范围内。为了保证最终一致性,我们需要分布式锁。保证一个方法在同一时间内只能被集群中的一台服务器中的一个线程执行,否则如果被多个线程执行了结果可能不正确。分布式环境下的事务和锁比单体程序下的复杂很多。

实现分布式锁要满足的条件:

1. 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;

2.  高可用的获取锁与释放锁;

3.  高性能的获取锁与释放锁;

4.  具备可重入特性;

5.  具备锁失效机制,防止死锁;

6.  具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

实现分布式锁有三种方案:数据库、Redis缓存、zookeeper,这里只介绍Redis和zookeeper。

Redis分布式锁的实现主要要使用到三个命令:

1. SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。

2. expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。

3. delete key:删除key

实现思想:

1. 获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。

2. 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。

3. 释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

Redis实现分布式锁的问题:

锁的过期时间设置多久为好呢?如果时间过短,方法还没执行完毕,锁就释放了,会产生并发问题,如果时间设置的过长,其他获取锁的线程就要白等很长时间。

Zookeeper实现分布式锁:

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

使用Zookeeper分布式锁的优点:

1. 锁无法释放?使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。

2. 非阻塞锁?使用Zookeeper可以实现阻塞的锁,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。

3. 不可重入?使用Zookeeper也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。

4. 单点问题?使用Zookeeper可以有效的解决单点问题,ZK是集群部署的,只要集中有半数以上的机器存活,就可以对外提供服务。

Redis分布式锁和zookeeper分布式锁的对比:

可靠性:zookeeper>Redis

性能:Redis>zookeeper

Redis中的虚拟内存


和操作系统的虚拟内存不是一回事,但思路是一样的。Redis会把经常不用的数据转存到文件中,从而腾出内存空间。通过VM操作来实现。

vm-enabled yes                          #开启vm功能

vm-swap-file /tmp/redis.swap  #交换出来的value保存的文件路径

vm-max-memory 1000000          #redis使用的最大内存上限,超过上限后redis开始交换value到磁盘文件中。

vm-page-size 32                    #每个页面的大小32个字节

vm-pages 134217728                 #最多使用在文件中使用多少页面,交换文件的大小 = vm-page-size * vm-pages

vm-max-threads  4             #用于执行value对象换入换出的工作线程数量。0表示不使用工作线程

vm-max-threads设置为0,不使用工作线程时

换出:当检测到有换出操作时,主线程会以阻塞的方式来操作。直到内存释放到设置的最大上限以下时,才继续处理客户端的请求。

换入:当客户端请求的数据被换出到文件时,主线程会以阻塞的形式从文件中加载对应的数据,此时主线程阻塞所有的客户端请求,直到加载完毕。

vm-max-threads设置为>0,使用工作线程时

换出:当主线程检测到内存超出最大上限后,会将要换出的数据放到一个队列中,由工作线程后台处理,主线程此时继续能处理客户端的请求。

换入:当客户端请求的数据被换出时,主线程会阻塞当前的请求,把需要换入的数据放到队列中,由工作线程去加载,加载完毕后工作线程通知主线程,主线程再响应此客户端请求。这种方式下,主线程只阻塞请求的数据被换出的客户端请求,其他请求不阻塞。

重要说明:在Redis的内存优化中是要关闭虚拟内存的,因为虚拟内存的换入换出操作的内存管理成本是很高的。如果把虚拟内存作为数据的一种持久化方式的话,这种方式并不成熟,也很鸡肋。虚拟内存已经被作者放弃。

Redis的高可用和集群


什么是高可用?高可用就是指一旦Redis的服务器宕机了,缓存服务可以切换到其他机器上继续对外提供服务,不影响使用。要解决高可用,肯定得使用集群了。也就是一台master服务器对外提供服务,然后设置几台slave服务器作为备用。一旦master挂了,可以切换到slave中的任意一台服务器继续提供服务。

使用集群了,就得考虑主从服务器之间的数据同步。得保证这几台服务器上的缓存数据必须是一致的。主从同步分为全量同步和增量同步。全量同步一般是在服务器启动时进行的,然后后续可以使用增量同步。所有的同步都可以使用Redis的api命令进行操作。

Redis有一个哨兵模式(Redis Sentinel),它主要负责主从状态的监控和切换。

Redis协议


Redis即 REmote Dictionary Server (远程字典服务);

而Redis的协议规范是 Redis Serialization Protocol (Redis序列化协议)

该协议是用于与Redis服务器通信的,用的较多的是Redis-cli通过pipe与Redis服务器联系。

Redis的通讯协议使用消息头标识,消息行,还有行里可能还有个数据块大小描述。

Redis是以行来划分,每行以\r\n行结束。每一行都有一个消息头,消息头共分为5种

分别如下:

(+)表示一个正确的状态信息,具体信息是当前行+后面的字符。

(-)表示一个错误信息,具体信息是当前行-后面的字符。

(*)表示消息体总共有多少行,不包括当前行,*后面是具体的行数。

($)表示下一行数据长度,不包括换行符长度\r\n,$后面则是对应的长度的数据。

(:)表示返回一个数值,:后面是相应的数字节符。

使用Redis的协议可以自己编写Redis的client处理程序。

Redis的内存优化


1. 禁止开启虚拟内存

2. redis.conf中的 maxmemory 选项,意思是当内存达到一个值后,Redis拒绝写入操作,避免影响性能甚至不停的写入导致系统崩溃。

Redis针对不同的数据类型提供了不同的参数来进行优化。Redis Hash 是 value 内部为一个 HashMap,如果该 Map 的成员数比较少,则会采用类似一维线性的紧凑格式来存储该 Map,即省去了大量指针的内存开销,这个参数控制对应在 redis.conf 配置文件中下面2项:


含义是当value这个 Map 内部不超过多少个成员时会采用线性紧凑格式存储,默认是64,即 value 内部有64个以下的成员就是使用线性紧凑存储,超过该值自动转成真正的 HashMap。

hash-max-zipmap-value含义是当 value 这个 Map 内部的每个成员值长度不超过多少字节就会采用线性紧凑存储来节省空间。

以上2个条件任意一个条件超过设置值都会转换成真正的 HashMap,也就不会再节省内存了,那么这个值是不是设置的越大越好呢,答案当然是否定的,HashMap 的优势就是查找和操作的时间复杂度都是 O(1) 的,而放弃 Hash 采用一维存储则是 O(n) 的时间复杂度,如果成员数量很少,则影响不大,否则会严重影响性能,所以要权衡好这个值的设置,总体上还是最根本的时间成本和空间成本上的权衡。

同样类似的参数还有:


说明:list数据类型多少节点以下会采用去指针的紧凑存储格式。


说明:list数据类型节点值大小小于多少字节会采用紧凑存储格式。


说明:set数据类型内部数据如果全部是数值型,且包含多少节点以下会采用紧凑格式存储。

如何解决高并发下Redis客户端频繁time out


先明白超时的原因是什么?首先前提是高并发,那么肯定有许多个客户端请求。客户端请求到达Redis服务器后,因为Redis是单线程的,客户端的请求命令服务器端是一条一条执行的。只有前面的执行完毕了,才能执行后面的。如果前面的某条数据执行时间比较长,慢查询,会导致后面的请求一直没法到达Redis服务器里,导致连接超时,请求阻塞。还有可能是客户端连接数太多,超出了服务器端的承受能力。解决方案如下:

1. 客户端连接池化,和传统的关系型数据库客户端池化是类似的。

2. 对于有慢查询、大数据量的查询尽量安排到去查询数据库,单独隔离出来,不让它影响其他的客户端请求。

3. 对于多核CPU的电脑,可以设置多个Redis实例,数据分摊到多个Redis实例下,客户端请求不同的服务器实例,分摊连接数量。

4. 合理设置连接的超时时间。

5. 使用Redis集群,分担客户端压力。

Redis的持久化


主要分为两种方式(快照和AOF),也有说是四种方式的(另外两种是虚拟内存和diskstore),这里只介绍快照和AOF两种。

快照:Redis内部有一个定时器,定时去检查数据发生的改变次数和时间是否满足持久化的条件,如果满足,通过操作系统的fork来调用一个子进程,通过子进程来遍历整个内存进行存储操作。

Redis的缓存穿透和缓存雪崩


缓存穿透:黑客故意去请求缓存中不存在的数据,导致所有的请求都到数据库,导致数据库连接异常。

解决办法

1. 互斥锁。缓存失效的时候,先获得锁,然后去数据库请求数据,没得到锁,就休眠一会重试,

2. 异步更新。客户端请求Redis时,无论缓存中是否存在请求的数据都直接返回。如果缓存过期了,异步开一个线程,去数据库中读取,然后更新缓存。这需要做缓存预热功能,即程序启动前,先加载缓存。

缓存雪崩:Redis缓存大面积失效,导致所有的请求都到数据库,导致数据库服务器压力巨大。

解决办法

1. 缓存的失效时间不要设置为一致,最好是随机数,避免同一时刻缓存大面积过期。

2. 使用互斥锁。但是锁的使用降低了性能。一种技术有优点,也有缺点。合理使用。

3. 双缓存。A缓存设置过期时间,B缓存不设置过期时间。先从A中查询,查询不到了再从B中查询。同时异步起一个线程,同时更新两个缓存。在很多微服务的架构中就使用了二级缓存。

Redis的数据类型及使用场景


Redis有5种数据类型:

1. String最简单最基本的数据类型。可以使用计数器。

2. Hash哈希的特点是value值是一个结构化的数据。使用的Redis共享session实现的单点登录就是使用的hash。程序前端代码的每个请求,用户不用再手动写sessionID的参数,程序自动处理。

3. List可以做简单的队列,还可以实现Redis的分页功能。

4. Set特点是不重复的集合。可以用来做去重。

5. Sorted set有序集合,可以做排行榜,Top N的操作。还可以做延时和范围查找。

如何解决Redis并发key


什么是并发key:就是同时有多个子系统或者多个线程同时去更新同一个key的值。特别是在集群,高并发环境下。

如果多个子系统同时更新key的值没有先后顺序,可以使用分布式锁(集群环境下,得使用分布式锁),谁先获得锁,谁先更新数据。

如果多个子系统更新的key值有先后顺序,那么必要要求这多个子系统按照先后顺序执行。可以实现RabbitMQ队列来处理(队列的两大特性:异步和先进先出)。



你可能感兴趣的:(Redis高级应用总结)