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跟数据库的数据同步问题

《详谈redis跟数据库的数据同步问题》文章讨论了在Redis和数据库数据一致性问题上的解决方案,主要比较了先更新Redis缓存再更新数据库和先更新数据库再更新Redis缓存两种方案,文章指出,删除R... 目录一、Redis 数据库数据一致性的解决方案1.1、更新Redis缓存、删除Redis缓存的区别二

Redis与缓存解读

《Redis与缓存解读》文章介绍了Redis作为缓存层的优势和缺点,并分析了六种缓存更新策略,包括超时剔除、先删缓存再更新数据库、旁路缓存、先更新数据库再删缓存、先更新数据库再更新缓存、读写穿透和异步... 目录缓存缓存优缺点缓存更新策略超时剔除先删缓存再更新数据库旁路缓存(先更新数据库,再删缓存)先更新数

Redis事务与数据持久化方式

《Redis事务与数据持久化方式》该文档主要介绍了Redis事务和持久化机制,事务通过将多个命令打包执行,而持久化则通过快照(RDB)和追加式文件(AOF)两种方式将内存数据保存到磁盘,以防止数据丢失... 目录一、Redis 事务1.1 事务本质1.2 数据库事务与redis事务1.2.1 数据库事务1.

mac安装redis全过程

《mac安装redis全过程》文章内容主要介绍了如何从官网下载指定版本的Redis,以及如何在自定义目录下安装和启动Redis,还提到了如何修改Redis的密码和配置文件,以及使用RedisInsig... 目录MAC安装Redis安装启动redis 配置redis 常用命令总结mac安装redis官网下

Redis主从复制实现原理分析

《Redis主从复制实现原理分析》Redis主从复制通过Sync和CommandPropagate阶段实现数据同步,2.8版本后引入Psync指令,根据复制偏移量进行全量或部分同步,优化了数据传输效率... 目录Redis主DodMIK从复制实现原理实现原理Psync: 2.8版本后总结Redis主从复制实

SpringBoot使用注解集成Redis缓存的示例代码

《SpringBoot使用注解集成Redis缓存的示例代码》:本文主要介绍在SpringBoot中使用注解集成Redis缓存的步骤,包括添加依赖、创建相关配置类、需要缓存数据的类(Tes... 目录一、创建 Caching 配置类二、创建需要缓存数据的类三、测试方法Spring Boot 熟悉后,集成一个外

Redis分布式锁使用及说明

《Redis分布式锁使用及说明》本文总结了Redis和Zookeeper在高可用性和高一致性场景下的应用,并详细介绍了Redis的分布式锁实现方式,包括使用Lua脚本和续期机制,最后,提到了RedLo... 目录Redis分布式锁加锁方式怎么会解错锁?举个小案例吧解锁方式续期总结Redis分布式锁如果追求

Redis的Hash类型及相关命令小结

《Redis的Hash类型及相关命令小结》edisHash是一种数据结构,用于存储字段和值的映射关系,本文就来介绍一下Redis的Hash类型及相关命令小结,具有一定的参考价值,感兴趣的可以了解一下... 目录HSETHGETHEXISTSHDELHKEYSHVALSHGETALLHMGETHLENHSET

如何提高Redis服务器的最大打开文件数限制

《如何提高Redis服务器的最大打开文件数限制》文章讨论了如何提高Redis服务器的最大打开文件数限制,以支持高并发服务,本文给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧... 目录如何提高Redis服务器的最大打开文件数限制问题诊断解决步骤1. 修改系统级别的限制2. 为Redis进程特别设置限制

Java解析JSON的六种方案

《Java解析JSON的六种方案》这篇文章介绍了6种JSON解析方案,包括Jackson、Gson、FastJSON、JsonPath、、手动解析,分别阐述了它们的功能特点、代码示例、高级功能、优缺点... 目录前言1. 使用 Jackson:业界标配功能特点代码示例高级功能优缺点2. 使用 Gson:轻量