分享一下个人网易架构师同事在spring boot下使用redis的心得~redis
首先总结了redis服务端单线程工做模型,redis四种部署方式及使用场景,而后从源码的角度上,分析springboot在jedis和lettuce客户端下使用redis的一些坑~尤为是在集群模式下的一些不兼容问题!spring
最近整理的Java架构学习视频和大厂项目底层知识点,须要的同窗欢迎私信我发给你~一块儿学习进步!安全
redis 内部使用文件事件处理(file event handler)处理客户端的请求,文件事件处理器是单线程的,因此redis才叫作单线程的模型。springboot
文件事件处理器的结构包含4个部分:多个socket、IO 多路复用程序、文件事件分派器、事件处理器(链接应答处理器、命令请求处理器、命令回复处理器)。服务器
文件事件处理器采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。markdown
Redis客户端经过socket链接reids服务端,多个 socket 可能会并发产生不一样的操做,每一个操做对应不一样的文件事件,可是 IO 多路复用程序会监听多个 socket,将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。网络
redis 单线程模型也能效率高的缘由:多线程
为何redis采用单线程模型呢?架构
若是采用多线程模型,cpu须要进行上下文切换,假设1MB的数据由多个线程读取了1000次,那么就有1000次时间上下文的切换,那么就有1500ns * 1000 = 1500us,而单线程的读完1MB数据才250us ,因此彻底不必使用多线程。并发
何时适合采用多线程的方案呢?
对于慢速设备:磁盘,网络,SSD 等等,将请求和处理的线程不绑定,请求的线程将请求放在一个buff里,而后等buff快满了,处理的线程再去处理这个buff。而后由这个buff 统一的去写入磁盘,或者读磁盘,这样效率最高。
Redis线程安全吗?
redis其实是采用了线程封闭的观念,把任务封闭在一个线程,天然避免了线程安全问题,不过对于须要依赖多个redis操做的复合操做来讲,依然须要锁,并且有多是分布式锁。
单节点模式只有一个节点,通常用来测试
主从模式包括一个主节点和多个从节点,通常来讲,主节点用来读写操做,从节点用户读操做,主节点的数据能够同步到从节点,因此从节点即使支持写操做也没有意义。
哨兵模式是基于主从模式的,哨兵模式为了实现主从模式的高可用,监控主从节点的状态,当sentinel发现master节点挂了之后,sentinel就会从slave中从新选举一个master。
通常来讲,经过sentinel集群能够管理多个主从redis,sentinel最好不要和Redis部署在同一台机器,否则redis的服务器挂了之后,sentinel也挂了。使用sentinel集群也是为了保证redis的高可用,避免哨兵节点挂了以后影响redis的使用。
当使用sentinel模式的时候,客户端就不要直接链接Redis,而是链接sentinel的ip和port,由sentinel来提供具体的可提供服务的Redis实现,这样当master节点挂掉之后,sentinel就会感知并将新的master节点提供给使用者。
sentinel模式基本能够知足通常生产的需求,具有高可用性。可是当数据量过大到一台服务器存放不下的状况时,主从模式或sentinel模式就不能知足需求了,这个时候须要对存储的数据进行分片,将数据存储到多个Redis实例中。
cluster的出现是为了解决单机Redis容量有限的问题,将Redis的数据根据必定的规则分配到多台机器。
cluster能够说是sentinel和主从模式的结合体,经过cluster能够实现主从和master重选功能,因此若是配置两个副本三个分片的话,就须要六个Redis实例。
如图所示,部署了三主三从的redis集群,redis cluster有固定的16384个hash slot,对每一个key计算CRC16值,而后对16384取模,能够获取key对应的hash slot,从而将数据存储至对应的slot上。
spring-boot-starter-data-redis支持两种redis客户端:jedis和lettuce
Springboot2.0默认使用的客户端是lettuce,下面跟踪源码来分析springboot如何加在lettuce客户端的,首先找到springboot自动加载的jar包下redis相关的加载配置类RedisAutoConfiguration
这里采用@Configuration @bean的方式向容器中注入RedisTemplate和StringRedisTemplate,注入二者的方法中须要传入RedisConnectionFactory,RedisConnectionFactory经过@Import导入的LettuceConnectionConfiguration和JedisConnectionConfiguration生成
能够看到在没有RedisConnectionFactory的状况下,会默认向Spring容器中注入LettuceConnectionFactory,若是要使用jedis客户端,只须要手动配置一个JedisConnectionFactory并注入容器便可。
从源码角度分析jedis客户端执行每一个命令的过程
首先借助于Client类的对应方法去执行命令
而后借助于Connection类的sendCommand方法执行
sendCommand方法每次执行都会调用connect方法
从connect方法中能够看到,socket是一个共享变量,假如两个线程公用一个jedis实例,当前尚未创建socket链接,两个线程同时进入创建socket链接
线程1创建socket链接后,开始获取输入输出流,于此同时,线程2从新初始化socket,而且没有执行到创建socket链接,此时线程1获取输入输出流将失败,由于此时的socket并无链接。
jedis自己不是多线程安全的,这并非jedis的bug,而是jedis的设计与redis自己就是单线程相关,jedis实例抽象的是发送命令相关,一个jedis实例使用一个线程与使用100个线程去发送命令没有本质上的区别,因此不必设置为线程安全的。可是若是须要用多线程方式访问redis服务器怎么作呢?那就使用多个jedis实例,每一个线程对应一个jedis实例,而不是一个jedis实例多个线程共享。一个jedis关联一个Client,至关于一个客户端,Client继承了Connection,Connection维护了Socket链接,对于Socket这种昂贵的链接,通常都会作池化,jedis提供了JedisPool。
集群中每一个节点只负责部分slot, slot可能从一个节点迁移到另外一节点,形成客户端有可能会向错误的节点发起请求。所以须要有一种机制来对其进行发现和修正,这就是请求重定向。
集群拓扑刷新是在ClusterTopologyRefreshScheduler中进行,下面进入类中一探究竟
ClusterTopologyRefreshScheduler类实现了ClusterEventListener接口,用来监听redis集群事件,集群事件包括ask重定向,move重定向,以及从新链接等。
在重定向方法中首先调用isEnabled方法判断是否开启刷新集群拓扑,而后调用indicateTopologyRefreshSignal方法刷新集群拓扑
判断集群是否开启刷新拓扑结构,依据ClusterTopologyRefreshOptions中自适应刷新的trigger中是否包含指定的重定向trigger,在默认配置下,这个trigger是什么样的呢?
能够看到默认状况下自适应刷新的trigger是空的,因此在集群模式下,使用默认的lettuce配置,若是主节点宕机,是不会刷新集群拓扑的,也就是会致使redis链接失败。
在enableAllAdaptiveRefreshTriggers方法中能够开启自适应刷新集群拓扑。开启自适应刷新集群拓扑后,又是如何刷新的呢?
在indicateTopologyRefreshSignal方法中提交一个刷新集群拓扑的clusterTopologyRefreshTask
在task中调用RedisClusterClient类的reloadPartitions方法从新加载集群拓扑信息,达到刷新的效果。
除了经过开始自适应刷新集群拓扑以外,还能够经过开启周期刷新的方式刷新集群拓扑
开启周期刷新集群拓扑后,在初始化集群拓扑的时,会调用activateTopologyRefreshIfNeeded开启周期刷新集群拓扑任务
这里会判断是否开启周期刷新,开启后才会提交一个定时任务。
周期刷新和自适应刷新比较:周期刷新和自适应刷新两种方法,最好仍是使用自适应刷新,由于周期刷新的周期须要设置,设置太长会致使服务可能一段时间不可用,设置过短对资源是一种浪费,而自适应刷新根据服务端的响应来刷新集群拓扑。
两种刷新方法不必都开启,都开启对资源也是一种浪费。
redis使用lua脚本的好处:
那Jedis客户端是如何支持lua脚本的呢?
Jedis执行lua脚本是经过ScriptExecutor类的execute方法执行的,在方法中进一步调用eval方法
进一步调用RedisScriptingCommands类的eval方法,由于实在集群模式下使用jedis客户端,因此调用JedisClusterScriptingCommands实现类的eval方法
再看JedisClusterScriptingCommands实现类的eval方法,竟然直接抛出异常,集群模式下不支持脚本。
解决方法是使用lettuce客户端,LettuceScriptingCommands类中的eval方法支持脚本
看到这里的小伙伴,若是你喜欢这篇文章的话,别忘了转发、收藏、留言互动!
若是对文章有任何问题,欢迎在留言区和我交流~
最近我新整理了一些Java资料,包含面经分享、模拟试题、和视频干货,若是你须要的话,欢迎私信我!