Redis 由浅入深 (5) - Redis 旁路方案

2023-10-10 21:10

本文主要是介绍Redis 由浅入深 (5) - Redis 旁路方案,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Redis 旁路方案

    • Redis旁路设计说明
      • 为什么需要旁路
    • Redis旁路设计的几种方案
      • 应用缓存旁路
        • 1.try catch
        • 2.try catch + 熔断旁路
        • 2.try catch + 熔断旁路 + 本地缓存
      • session旁路

Redis旁路设计说明

Redis的旁路设计是指Redis在宕机之后或者网络不通的情况下,应用系统能够正常的访问,做出正确的缓存方案切换,一般指切换到应用缓存。不会造成系统不可访问的情况。但从redis集群的设计运维来说,redis肯定要保持高可用的,换句话说是在redis服务层面去做高可用,而不是应用层面去找替代方案。比如:redis挂了之后 80% ~ 90%是因为高并发或者是大对象缓存导致阻塞、连接满了,或者是主从复制内存耗尽。假如说这种情况出现,就算应用配置不错也是顶不住的。所以说一般情况下使用Redis集群是不需要做redis的旁路设计的,如果redis不能访问是要直接做熔断设计的。

为什么需要旁路

可是有些情况,比如说手动重启redis/磁盘满了(redis和其他服务在一起),(普通应用redis服务挂的概率低到可以忽略不计)这时候需要重启redis。但是服务不能停且服务没有多少人访问的情况下进行切换到JVM缓存。还有种情况下是redis挂了之后能快速恢复,系统需要稍微顶一下。
当然还有一种情况是系统硬性需要做旁路设计。

如果要做旁路session切换就必须保证集群负载已经做好了IP hash,就是说每次用户访问过来都会访问到同一台服务器。F5的session粘连以及Nginx的IP hash都可以做到。

Redis旁路设计的几种方案

应用缓存旁路

1.try catch

在访问redis的时候如果redis服务不可访问的情况下会报错,这个时候再catch中访问数据库。

@Service
public class Side1 {@Autowiredprivate RedisTemplate<Object, Object> redisTemplate;public void execute () {try {redisTemplate.opsForValue().set("AAA", "BBB");throw new RuntimeException("Redis请求发生异常");} catch (Exception e) {// 查询数据库System.out.println("查询了数据库");}}
}
2.try catch + 熔断旁路

在上面得方案基础之上进行设计,加入说redis报异常了之后,使用全局volitile变量缓存下redis的状态,当下次请求的时候直接走数据库,起定时任务2s后恢复redis访问,如果redis仍不能访问继续设置volitile变量为false。直到redis能够正常访问。

@Service
public class Side2 {private static volatile boolean isAccessible = true;@Autowiredprivate RedisTemplate<Object, Object> redisTemplate;TimerTask timerTask = new TimerTask() {@Overridepublic void run() {isAccessible = true;}};public void execute () {try {if (isAccessible) {redisTemplate.opsForValue().set("AAA", "BBB");throw new RuntimeException("Redis请求发生异常");}} catch (Exception e) {// 查询数据库System.out.println("查询了数据库");isAccessible = false;new Timer().schedule(timerTask, 2000L);}}
}

这种方式Redis的操作代码可以封装成通用操作类,统一对异常进行处理,而不用每个请求处理一次。

2.try catch + 熔断旁路 + 本地缓存
@Service
public class Side3 {private static volatile boolean isAccessible = true;private Cache<String, Object> LOCAL_CACHE = CacheBuilder.newBuilder().expireAfterAccess(30, TimeUnit.MINUTES).maximumSize(10000).build();@Autowiredprivate RedisTemplate<Object, Object> redisTemplate;TimerTask timerTask = new TimerTask() {@Overridepublic void run() {isAccessible = true;}};public void execute () {try {if (isAccessible) {redisTemplate.opsForValue().set("AAA", "BBB");throw new RuntimeException("Redis请求发生异常");} else {Object aaa = LOCAL_CACHE.get("AAA", () -> {// 这里查询数据库返回默认值return "BBB";});}} catch (Exception e) {// 查询数据库System.out.println("查询了数据库");isAccessible = false;new Timer().schedule(timerTask, 2000L);}}
}
  • 这里的本地缓存可以使用Ehcache(2/3),也可以使用Guava Cache。最好不要自己写,一、性能可能不高,二、可能回出bug,三、写的可能不好还浪费时间。不要重复造轮子,但要知道轮子是怎么造的。
  • 这里只是很简单的写了下定时器,正常要用线程池去做,防止申请过多线程。

session旁路

1.冷切换
设置JVM参数或者在配置中心定义一个开启redis session的参数来确定是否使用redis,在系统代码中埋点来切换是否使用redis做session。

2.热切换
系统自动感知redis是否集群出现故障,如果出现故障立马切断redis使用JVM session。

Redis session和应用JVM session的热切换依赖于SessionRepoistoryFilter这个类,这个类会拦截Http Request将默认是使用的HttpSession替换为Redis Sesssion。但这个类的成员变量sessionRepository是不可更改的,我们如果需要修改起SessionRepository有两种方案:

① 重新SessionRepositoryFilter (推荐使用)
② 反射替换sessionRepository属性,这种方法很简单,就是检测到Redis集群不健康立马将sessionRepository属性替换为JVMSessionRepository。但这里有一个疑点,private final SessionRepository<S> sessionRepository;可以看到是由final修饰的属性,如果Java编译或者JIT实时编译的时候做了优化,将final属性做内联优化,这个时候其值是不可修改的,但我们这里引用的是变量,应该不会有这样的问题。我测试的时候用的就是这种方法来替换的,但我不清楚不同的JDK版本是否会有所差异还是有其他的问题。故建议还是重写SessionRepositoryFilter来替换。

Java 编译器不会对所有的 final 属型进行内联优化,只有八种基本类型属性和 LiteralString(直接用引号包裹而不是new关键字实例化的字符串) 会进行内联优化,对于引用类型不会则进行内联优化。此外,在运行期间才确定值的final属性也不会进行内联优化。

SessionRepositoryFilter

@Order(-2147483598)
public class SessionRepositoryFilter<S extends ExpiringSession> extends OncePerRequestFilter {private static final String SESSION_LOGGER_NAME = SessionRepositoryFilter.class.getName().concat(".SESSION_LOGGER");private static final Log SESSION_LOGGER;public static final String SESSION_REPOSITORY_ATTR;public static final String INVALID_SESSION_ID_ATTR;private static final String CURRENT_SESSION_ATTR;public static final int DEFAULT_ORDER = -2147483598;// session仓库private final SessionRepository<S> sessionRepository;private ServletContext servletContext;private MultiHttpSessionStrategy httpSessionStrategy = new CookieHttpSessionStrategy();public SessionRepositoryFilter(SessionRepository<S> sessionRepository) {if (sessionRepository == null) {throw new IllegalArgumentException("sessionRepository cannot be null");} else {this.sessionRepository = sessionRepository;}}public void setHttpSessionStrategy(HttpSessionStrategy httpSessionStrategy) {if (httpSessionStrategy == null) {throw new IllegalArgumentException("httpSessionStrategy cannot be null");} else {this.httpSessionStrategy = new SessionRepositoryFilter.MultiHttpSessionStrategyAdapter(httpSessionStrategy);}}public void setHttpSessionStrategy(MultiHttpSessionStrategy httpSessionStrategy) {if (httpSessionStrategy == null) {throw new IllegalArgumentException("httpSessionStrategy cannot be null");} else {this.httpSessionStrategy = httpSessionStrategy;}}protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response, this.servletContext);SessionRepositoryFilter<S>.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);HttpServletRequest strategyRequest = this.httpSessionStrategy.wrapRequest(wrappedRequest, wrappedResponse);HttpServletResponse strategyResponse = this.httpSessionStrategy.wrapResponse(wrappedRequest, wrappedResponse);try {filterChain.doFilter(strategyRequest, strategyResponse);} finally {wrappedRequest.commitSession();}}
}

整个代码的运行过程如下:
在这里插入图片描述
下面贴上代码:
CacheOperator

package com.allens.lettuce.cache;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisClusterNode;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;@Service
public class CacheOperator {@AutowiredRedisTemplate<Object, Object> redisTemplate;public void operate () {redisTemplate.opsForValue().set("aaa", "bbb");}@AutowiredLettuceConnectionFactory lettuceConnectionFactory;public boolean isRedisOk () {Iterable<RedisClusterNode> redisClusterNodes = lettuceConnectionFactory.getClusterConnection().clusterGetNodes();boolean flag = true;for (RedisClusterNode e : redisClusterNodes) {FutureTask<String> futureTask = new FutureTask<>(() -> redisTemplate.opsForCluster().ping(e));try {new Thread(futureTask::run).start();final String ping = futureTask.get(100, TimeUnit.MILLISECONDS);if (!ping.equals("PONG")) {flag = false;break;}} catch (InterruptedException | ExecutionException | TimeoutException interruptedException) {interruptedException.printStackTrace();flag = false;}}return flag;}
}

RedisConfig

package com.allens.lettuce.cache;import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;import java.time.Duration;@Configuration
public class RedisConfig {@BeanRedisTemplate<Object, Object> redisTemplate (RedisConnectionFactory redisConnectionFactory) {RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);redisTemplate.setDefaultSerializer(jackson2JsonRedisSerializer);redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);redisTemplate.setConnectionFactory(redisConnectionFactory);return redisTemplate;}@BeanCacheManager cacheManager (RedisConnectionFactory redisConnectionFactory) {RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string())).prefixKeysWith("allens").entryTtl(Duration.ofMinutes(5));return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory).withCacheConfiguration("test", redisCacheConfiguration).build();}
}

RedisState

package com.allens.lettuce.config;public class RedisState {public static volatile boolean REDIS_IS_OK = true;
}

RedisStateListener

package com.allens.lettuce.config;import com.allens.lettuce.cache.CacheOperator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.stereotype.Component;import java.lang.reflect.Field;@Component
public class RedisStateListener implements ApplicationListener<ApplicationStartedEvent> {@AutowiredCacheOperator cacheOperator;@AutowiredSessionRepositoryFilter sessionRepositoryFilter;private static RedisIndexedSessionRepository redisSessionRepository = null;@Overridepublic void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent) {new Thread(() -> {while (true) {try {boolean isOk = cacheOperator.isRedisOk();RedisState.REDIS_IS_OK = isOk;System.out.println("redis 健康检查..." + isOk);try {if (!isOk) {Field sessionRepository = SessionRepositoryFilter.class.getDeclaredField("sessionRepository");sessionRepository.setAccessible(true);if (this.redisSessionRepository == null) {this.redisSessionRepository = (RedisIndexedSessionRepository) sessionRepository.get(sessionRepositoryFilter);sessionRepository.set(sessionRepositoryFilter, new SwitchSessionRepository());}} else {if (this.redisSessionRepository != null) {Field sessionRepository = SessionRepositoryFilter.class.getDeclaredField("sessionRepository");sessionRepository.setAccessible(true);sessionRepository.set(sessionRepositoryFilter, this.redisSessionRepository);this.redisSessionRepository = null;}}} catch (NoSuchFieldException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}Thread.sleep(10000);} catch (InterruptedException e) {e.printStackTrace();}}}).start();}
}

SwitchSessionRepository

package com.allens.lettuce.config;import org.springframework.session.MapSession;
import org.springframework.session.SessionRepository;
import java.util.concurrent.ConcurrentHashMap;public class SwitchSessionRepository implements SessionRepository<MapSession> {/*** 存储所有的session*/private ConcurrentHashMap<String, MapSession> cache = new ConcurrentHashMap<>();@Overridepublic MapSession createSession() {return new MapSession();}@Overridepublic void save(MapSession mapSession) {cache.put(mapSession.getId(), mapSession);}@Overridepublic MapSession findById(String s) {return cache.get(s);}@Overridepublic void deleteById(String s) {cache.remove(s);}
}

学习更多干货类容(JAVA、前端、Kafka、redis等等)请关注我的公众号
互联网技术专栏

这篇关于Redis 由浅入深 (5) - Redis 旁路方案的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/183111

相关文章

Redis中Stream详解及应用小结

《Redis中Stream详解及应用小结》RedisStreams是Redis5.0引入的新功能,提供了一种类似于传统消息队列的机制,但具有更高的灵活性和可扩展性,本文给大家介绍Redis中Strea... 目录1. Redis Stream 概述2. Redis Stream 的基本操作2.1. XADD

MySQL 迁移至 Doris 最佳实践方案(最新整理)

《MySQL迁移至Doris最佳实践方案(最新整理)》本文将深入剖析三种经过实践验证的MySQL迁移至Doris的最佳方案,涵盖全量迁移、增量同步、混合迁移以及基于CDC(ChangeData... 目录一、China编程JDBC Catalog 联邦查询方案(适合跨库实时查询)1. 方案概述2. 环境要求3.

SpringBoot3.X 整合 MinIO 存储原生方案

《SpringBoot3.X整合MinIO存储原生方案》本文详细介绍了SpringBoot3.X整合MinIO的原生方案,从环境搭建到核心功能实现,涵盖了文件上传、下载、删除等常用操作,并补充了... 目录SpringBoot3.X整合MinIO存储原生方案:从环境搭建到实战开发一、前言:为什么选择MinI

Knife4j+Axios+Redis前后端分离架构下的 API 管理与会话方案(最新推荐)

《Knife4j+Axios+Redis前后端分离架构下的API管理与会话方案(最新推荐)》本文主要介绍了Swagger与Knife4j的配置要点、前后端对接方法以及分布式Session实现原理,... 目录一、Swagger 与 Knife4j 的深度理解及配置要点Knife4j 配置关键要点1.Spri

Redis出现中文乱码的问题及解决

《Redis出现中文乱码的问题及解决》:本文主要介绍Redis出现中文乱码的问题及解决,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1. 问题的产生2China编程. 问题的解决redihttp://www.chinasem.cns数据进制问题的解决中文乱码问题解决总结

SQLite3 在嵌入式C环境中存储音频/视频文件的最优方案

《SQLite3在嵌入式C环境中存储音频/视频文件的最优方案》本文探讨了SQLite3在嵌入式C环境中存储音视频文件的优化方案,推荐采用文件路径存储结合元数据管理,兼顾效率与资源限制,小文件可使用B... 目录SQLite3 在嵌入式C环境中存储音频/视频文件的专业方案一、存储策略选择1. 直接存储 vs

Redis的持久化之RDB和AOF机制详解

《Redis的持久化之RDB和AOF机制详解》:本文主要介绍Redis的持久化之RDB和AOF机制,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录概述RDB(Redis Database)核心原理触发方式手动触发自动触发AOF(Append-Only File)核

Redis分片集群、数据读写规则问题小结

《Redis分片集群、数据读写规则问题小结》本文介绍了Redis分片集群的原理,通过数据分片和哈希槽机制解决单机内存限制与写瓶颈问题,实现分布式存储和高并发处理,但存在通信开销大、维护复杂及对事务支持... 目录一、分片集群解android决的问题二、分片集群图解 分片集群特征如何解决的上述问题?(与哨兵模

SpringBoot连接Redis集群教程

《SpringBoot连接Redis集群教程》:本文主要介绍SpringBoot连接Redis集群教程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1. 依赖2. 修改配置文件3. 创建RedisClusterConfig4. 测试总结1. 依赖 <de

SpringBoot+Redis防止接口重复提交问题

《SpringBoot+Redis防止接口重复提交问题》:本文主要介绍SpringBoot+Redis防止接口重复提交问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不... 目录前言实现思路代码示例测试总结前言在项目的使用使用过程中,经常会出现某些操作在短时间内频繁提交。例