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

相关文章

无人叉车3d激光slam多房间建图定位异常处理方案-墙体画线地图切分方案

墙体画线地图切分方案 针对问题:墙体两侧特征混淆误匹配,导致建图和定位偏差,表现为过门跳变、外月台走歪等 ·解决思路:预期的根治方案IGICP需要较长时间完成上线,先使用切分地图的工程化方案,即墙体两侧切分为不同地图,在某一侧只使用该侧地图进行定位 方案思路 切分原理:切分地图基于关键帧位置,而非点云。 理论基础:光照是直线的,一帧点云必定只能照射到墙的一侧,无法同时照到两侧实践考虑:关

高效+灵活,万博智云全球发布AWS无代理跨云容灾方案!

摘要 近日,万博智云推出了基于AWS的无代理跨云容灾解决方案,并与拉丁美洲,中东,亚洲的合作伙伴面向全球开展了联合发布。这一方案以AWS应用环境为基础,将HyperBDR平台的高效、灵活和成本效益优势与无代理功能相结合,为全球企业带来实现了更便捷、经济的数据保护。 一、全球联合发布 9月2日,万博智云CEO Michael Wong在线上平台发布AWS无代理跨云容灾解决方案的阐述视频,介绍了

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

JavaFX应用更新检测功能(在线自动更新方案)

JavaFX开发的桌面应用属于C端,一般来说需要版本检测和自动更新功能,这里记录一下一种版本检测和自动更新的方法。 1. 整体方案 JavaFX.应用版本检测、自动更新主要涉及一下步骤: 读取本地应用版本拉取远程版本并比较两个版本如果需要升级,那么拉取更新历史弹出升级控制窗口用户选择升级时,拉取升级包解压,重启应用用户选择忽略时,本地版本标志为忽略版本用户选择取消时,隐藏升级控制窗口 2.

如何选择SDR无线图传方案

在开源软件定义无线电(SDR)领域,有几个项目提供了无线图传的解决方案。以下是一些开源SDR无线图传方案: 1. **OpenHD**:这是一个远程高清数字图像传输的开源解决方案,它使用SDR技术来实现高清视频的无线传输。OpenHD项目提供了一个完整的工具链,包括发射器和接收器的硬件设计以及相应的软件。 2. **USRP(Universal Software Radio Periphera

MyBatis 切换不同的类型数据库方案

下属案例例当前结合SpringBoot 配置进行讲解。 背景: 实现一个工程里面在部署阶段支持切换不同类型数据库支持。 方案一 数据源配置 关键代码(是什么数据库,该怎么配就怎么配) spring:datasource:name: test# 使用druid数据源type: com.alibaba.druid.pool.DruidDataSource# @需要修改 数据库连接及驱动u

Redis中使用布隆过滤器解决缓存穿透问题

一、缓存穿透(失效)问题 缓存穿透是指查询一个一定不存在的数据,由于缓存中没有命中,会去数据库中查询,而数据库中也没有该数据,并且每次查询都不会命中缓存,从而每次请求都直接打到了数据库上,这会给数据库带来巨大压力。 二、布隆过滤器原理 布隆过滤器(Bloom Filter)是一种空间效率很高的随机数据结构,它利用多个不同的哈希函数将一个元素映射到一个位数组中的多个位置,并将这些位置的值置

一种改进的red5集群方案的应用、基于Red5服务器集群负载均衡调度算法研究

转自: 一种改进的red5集群方案的应用: http://wenku.baidu.com/link?url=jYQ1wNwHVBqJ-5XCYq0PRligp6Y5q6BYXyISUsF56My8DP8dc9CZ4pZvpPz1abxJn8fojMrL0IyfmMHStpvkotqC1RWlRMGnzVL1X4IPOa_  基于Red5服务器集群负载均衡调度算法研究 http://ww

Lua 脚本在 Redis 中执行时的原子性以及与redis的事务的区别

在 Redis 中,Lua 脚本具有原子性是因为 Redis 保证在执行脚本时,脚本中的所有操作都会被当作一个不可分割的整体。具体来说,Redis 使用单线程的执行模型来处理命令,因此当 Lua 脚本在 Redis 中执行时,不会有其他命令打断脚本的执行过程。脚本中的所有操作都将连续执行,直到脚本执行完成后,Redis 才会继续处理其他客户端的请求。 Lua 脚本在 Redis 中原子性的原因