正常业务中数据库承受大量的读写请求,造成数据库压力过大从而导致服务器宕机等情况时有发生,即使通过数据库的横向拓展,通过搭建主从复制实现读写分离或者垂直拓展实现分库分表的策略,虽然都在一定程度上能够缓解单节点数据库服务器的压力,但是随着业务量的持续增大,通过持续投钱买服务器显然并不是合理的方案
为了缓和CPU与内存之间的速度差异,计算机的制造商为CPU增加了缓存,从而缓存的概念深入人心,而java程序员都不陌生的redis,在使用起来既满足我们大部分业务需求但同时也带了部分隐患
热点数据的缓存redis作为热点数据的缓存实现,客户端请求先去redis查询,redis没有再去数据库查询,再根据结果完成缓存重建,这是最常规的redis缓存使用方式,但是同时也引出了诸多问题
缓存穿透:客户端请求的id在redis缓存中没有,在数据库中也没有,短时间有大量的请求都会到达数据库,数据库会承受巨大的压力甚至可能导致服务器宕机而redis缓存也失去了意义
解决方案:
缓存击穿:某个高并发访问并且缓存重建逻辑复杂的key突然过期,在缓存重建期间大量的请求达到数据库导致服务器存在处理缓慢的情况甚至可能导致服务器宕机
解决方案:
通过对比互斥锁与逻辑过期的解决方案,可以发现其实是在数据的一致性跟服务的可用性之间做抉择,如选择互斥锁,则是牺牲了响应时间,保证了数据的一致性;若选择的事逻辑过期的方案,则是保证了服务的可用性,牺牲的数据的一致性,存在数据的短期不一致的问题
计数器在当个jvm进程中通常使用JUC(java.util.concurrent)包下的AtomicInteger类使用的cas,避免了使用synchronized带了的性能损耗,但是在分布式集群部署下,多进程的jvm则不能够满足要求。Redis6.0之前只对外提供了单线程服务,即redis的所有单条命令都是原子性的,并且通过redis的单线程,可以将并行的请求转换为串行执行,依托这个特性,可以通过redis incr命令进行一些计数的操作,并且在多进程jvm下可见
常见的业务使用:秒杀业务,比如说某件商品的秒杀,肯定要判断商品的库存,依托每次都去数据库中查询,如果使用悲观锁(synchonized,数据库中查询库存,如果大于0,则执行更新操作)效率一定很低,通过改进乐观锁的sql,在update时set stockNum=stockNum-1 where stockNum>0;这个策略效率肯定比悲观锁要好,但是如果某件商品的库存是1w,同时有100w用户在抢购,岂不是如此大的并发量同时落到数据库中了吗? 所以我们在换种策略,在秒杀活动开始前,提前把商品的库存放入redis缓存中,通过incr -1的命令完成库存的自减,要知道官方提供的数据,redis每秒读11w次,写8.1w次,在库存为0时,对其他请求可以立刻返回失败,其次结合lua脚本,可以同时可以判断用户是否已下单(满足一人一单的需求)
缓存过期时间使用redis命令时set nx px,可以设置缓存过期时间,通过这种缓存自动过期的策略,可以实现的业务场景有:优惠卷自动过期、订单超时未支付过期、手机验证码失效的场景
但是如果缓存的数据没有设置过期时间,当达到内存阈值瓶颈时,要依托内存淘汰策略去删除key
分布式锁单节点的redis,独立于多节点部署的jvm进程之外,面对synchronized和ReentrantLock只在同一进程内有效的锁而分布式集群下部署的jvm则失去了对共享资源争夺的互斥性,多节点都能同时获取锁成功,此时需要存在多进程jvm都可见的锁,也就是分布式锁,分布式锁的实现方案业内常见的redis和zookeeper;
redis分布式锁常用的方式,依托string结构,set key value,nx:not exist 不存在则创建,px:设置锁的过期时间避免死锁
存在的问题:
死锁:如果某一线程加锁后,没来得及删除锁,服务器就宕机了,那其他jvm进程中的线程则再也获取不到锁,也就是set nx key value失败,则要设置锁的过期时间 ,避免死锁
锁误删:
锁不可重入:jvm实现的synchronized锁和java api实现的ReentrantLock底层都维护了一个整型字段,用于锁重入的实现,而redis获取锁并没有锁重入的实现,导致同一线程多次获取锁的业务无需求法实现
获取锁失败不可重试:使用set nx px 单次获取锁失败立即返回失败,不会再有重试的机会,造成不能灵活的控制业务,充分利用cpu
主从复制引起的锁重复添加成功的情况:为了保存redis的高可用性,搭建了主从集群,实现主从复制,现有ab线程,假设a现在在master 1 加锁成功,在master1 跟slave1 数据同步前,master出现脑裂的情况,master突然网络异常,但是仍旧是正常运行的,sentinel集群判定该master为客观下线后,开始自动故障转移,选举slave1为新的主节点master2,此时b线程在新的主节点又加锁成功,此时master1和master2都存在同一个key
针对上面这些问题,可以用redisson客户端解决这些问题
redisson 利用hash结构记录线程id和重入次数
获取锁失败的线程,通过订阅释放锁的信号,灵活控制锁的重试等待,cpu利用率比较高,不会无限等待
超时续约:利用watchDog,每隔一段时间(releaseTime /3)重置超时时间,但是只有tryLock方法中参数leaseTime释放锁时间为-1时才能够启用超时续约
利用multiLock:通过搭建多个独立的Redis节点,必须在所有节点获取到锁才算真正的获取锁成功,避免主从复制带来的重复加锁成功的情况
如果部署redis服务器发生故障,如果只使用RDB的持久化策略,可能会丢失最后一次RDB后的数据,并且重启服务器的这段期间,服务都是不可用的状态,况且不清楚服务器宕机的具体原因,单节点下的redis并没有故障自动恢复的能力,导致长时间的服务不可用,此时就需要备份数据,通过冗余数据来保证服务的可用性;
2、单节点redis自身资源有限,无法承载更多资源分配redis的缓存是基于内存实现的,单节点内存是有限的,如果内存占满了会启用淘汰策略,而这个期间客户端的连接请求都会超时,造成服务的短暂不可用,删除一部分缓存,删除缓存的操作并不是我们通过业务主观的操作,可能导致部分查询缓存的业务失效,从而使大量请求达到数据库;随着业务的发展,数据量的增大,当存在单台服务器的存储上限和算力上限影响业务的正常使用,此时就需要通过一些策略去横向拓展存储和算力
3、并发访问,给服务器主机带来压力,性能瓶颈客户端的每一个tcp连接都会消耗redis服务器的资源,虽然redis官方号称每秒读11w次,写8.1w次,但是这仅仅是统计了理想状态下的读写请求,并没有其他外力因素,比如说此时主进程需要fork出子进程完成RDB,或者执行rebgwriteaof,对aof文件进行重写等影响redis服务器性能的操作,此时需要通过部署主从节点,对读写请求分开处理,从而提高redis服务器集群的响应能力,提高整体算力,在数据量非常大的情况下,还需要通过搭建分片集群,提高redis服务集群的存储上限
2、主从复制为了提高redis的并发量,通过搭建redis的主从集群,利用读写分离来提高并发量;通过redis来缓存数据,客户端对redis的操作肯定是读多写少的情况,读操作主从库都可以接收,写操作,首先到主库上执行,在通过主从复制同步给从库
主从复制的作用每台redis服务节点默认的角色都是master,可以登录redis服务端,通过info replication命令查看当前服务节点的主从关系,也可以通过slaveof ip port 主master 设置从节点绑定的主节点
主从复制是通过RBD实现的,分为全量复制和增量复制
全量复制:
增量复制:
从节点向master发送psync命令,请求的runId是第一次全量同步时接收master的runId,以及当前同步的offset,master接收到psync命令后,确认runId是自己,就从repl_backlog_buffer(环形缓冲区),该缓冲区中存放了master 的offset,以及诸多slave 的salve_repl_offset,用于保存master与slave之间的数据的差异,master就将这部分差异的数据repl_backlog发送给slave
什么时候进行全量同步1、搭建主从集群后,Slave节点宕机恢复后可以找master节点同步数据,那master节点宕机怎么办?
2、Master宕机期间,重启数据恢复期间,都不能接收客户端的写请求该怎么办?
3、脑裂以及redis的数据丢失
异步复制导致的数据丢失
因为主从复制是通过bgsave进行的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机了,此时这些部分数据就丢失了
脑裂导致的数据丢失
脑裂,主从集群中,master如果网络异常,被哨兵集群判定为客观下线,但是实际上master还运行着,开启选举,将最接近master的slave节点通过slave no one 将slave切换成主节点,其他slave执行slaveof ip port完成主从切换,此时整个集群就会有两个master;
此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续写向旧master的数据可能也丢失了,因此旧master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据会清空,重新从新的master复制数据
使用redis主从集群架构后,实现读写分离,但是不能够保证主节点宕机后依旧能够响应客户端请求,当然我们可以通过人工的方式手动执行 slaveof no one去完成slave切换为master,然后通过slaveof ip port命令去告知其他从节点更换了新的master,但是我们更希望的是提供故障的自动解决,如果由人工完成,则需要增加人力成本,且容易产生人工错误,还会造成一段时间的程序不可用;当master节点异常时自动从多个slave中选举出最接近master节点的新master,redis为我们提供了哨兵集群,保证Redis的高可用,使得系统更加健壮
哨兵的作用会从slave集群中选择与master数据最接近的slave作为新的master节点,一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据是这样的:
当选出一个新的master后,该如何实现切换呢?
流程如下:
主从模式或哨兵模式每个节点存储的数据都是全量的数据,数据量过大时,就需要对存储的数据进行分片后存储到多个redis实例上。此时就要用到Redis Sharding技术。
4、分片集群 为什么要引入分片集群 哨兵集群留下的问题Redis的master宕机后,在主从切换的过程中,Redis开启了保护机制,禁止一切的写操作,直到选举出新的Redis主节点
海量数据的问题为了提供主从同步的性能,我们通过不会将的redis的master内存设置得太高,如果内存设置得太高,在一定频率下进行RDB持久化或者多从节点进行全量同步时会有很多进程争夺的磁盘带宽,并且redis的master主节点内存过大还会导致fork出子进程时阻塞的时间过长,此时无法接受客户端的写请求。
但是如果降低master节点的内存上限,此时还有海量数据该如何存储?
高并发写的问题我们通过搭建主从集群、哨兵集群来保证服务的高可用,并且为了适应读多写少的情况,通过读写分离分担master服务器压力,来解决高并发读的问题,并且从节点故障恢复后可以通过主从复制中的全量同步或者增量同步来保证数据的一致性,主节点宕机后通过哨兵集群完成服务的自动故障转移,保证读的可靠性,但是高并发写的问题依旧没有解决
分片集群其他中间件也是通过主从复制来解决高并发读的问题,通过多主多从来解决高并发写的问题,在redis中提供了分片集群也是以多主多从的形式来解决高并发写的问题
使用分片集群与之前的主从集群、哨兵集群的区别:
主从集群:数据量不大,业务中只需要满足读写分离,并且对服务的可用性不高,允许短暂的服务不可用带来的风险,并且需要手动完成主从切换,则单使用主从集群完全够用
哨兵集群:数据量不大,并且对服务的可用性要求比较高,可以使用主从集群搭配哨兵集群,完成故障的自动转移,主节点不可用时自动完成主从切换
分片集群:主要针对海量数据、高并发、高可用的场景
以上便是Redis在多种业务场景下的使用方案,如有误解,请在评论区指出,谢谢
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧