不难发现,我们经常使用Redis作为系统的缓存服务,但是你注意到了吗?我们每次操作Redis,都需要发送一次网络请求。这样就无法避免网络的开销。但是如何解决这个问题呢?我们引入了本地缓存来解决这个问题。查询逻辑由之前的直接查询变为:先查询本地缓存,如果不存在,再去远程查找,然后设置到本地缓存——适用于分布式客户端缓存。是不是感觉像我们用的本地缓存Guava、Caffeine等?它有什么特别之处?这里Redis6引入了Tracking机制。它的特殊之处在于,当发现某个节点的key对应的值发生变化或无效时,服务器会发送消息通知其他节点。让其他节点同步修改本地缓存。说到这里,你可能又会想,这不就是Redis消息的发布订阅吗?我已经用过了。有点像,但又不一样。Redis已经为我们做好了一切,我们这里只需要直接使用即可。刚刚提到的Tracking有三种模式:普通模式、广播模式、转发模式。注:由于需要支持Redis服务端消息推送,Redis实现了RESP3协议。为了兼容之前的RESP2协议,引入了转发模式,内部通过Redis消息的发布和订阅实现。这里就不赘述了,有兴趣的朋友可以查阅相关资料。1.普通模式开启追踪模式,默认关闭。启用跟踪模式后,Redis服务器会记录每个客户端请求的密钥。当key对应的值发生变化时,会向客户端发送失效消息。2、广播模式与普通模式的区别在于Redis服务器不需要记录客户端请求的key,而是在key对应的值发生变化时向客户端发送消息。客户端只能收听具有特定前缀键的消息。那么,我们如何在业务中选择合适的模式呢?这里可以根据自己的需要,普通模式会将客户端请求的关键信息存储在服务端,会占用服务端的内存。广播模式会向所有客户端发送消息。当然,我们也可以只关注特定前缀的消息。需要我们的规范命名key。了解了两种模式之后,我们通过demo来加深印象。在maven项目中引入Redis依赖org.springframework.bootspring-boot-starter-data-redisLettuce实现1.创建RedisClientRedisURIredisURI=RedisURI.builder().withHost("127.0.0.1").withPort(6379).withPassword("123456".toCharArray())//这里注意如果没有password.build();RedisClientclient=RedisClient.create(redisURI);StatefulRedisConnectionconnect=client.connect();2构建CacheFrontend客户端缓存Mapmap=newConcurrentHashMap<>();CacheAccessormapCacheAccessor=CacheAccessor.forMap(地图);CacheFrontendcacheFrontend=ClientSideCaching.enable(mapCacheAccessor,connect,TrackingArgs.Builder.enabled().bcast().prefixes("user","order").noloop());解析:使用map作为我们本地的缓存存储。connect为上一步获取的redis客户端连接。TrackingArgs.Builder.enabled()启用Redis跟踪机制。bcast()方法是广播模式,prefixes()是针对特定键前缀的过滤器。noloop()不接收连接本身修改密钥的消息。移除bcast()等相关广播模式配置为普通模式。然后就可以直接使用cacheFrontend进行缓存操作了。SpringBoot实现方法由于SpringBoot与上述配置类似,所以直接上传代码,不做过多分析。/***构建CacheFrontend**@paramredisConnectionFactory*@return*/@BeanpublicCacheFrontendcacheFrontend(RedisConnectionFactoryredisConnectionFactory){StatefulRedisConnectionredisConnect=this.getRedisConnect(redisConnectionFactory);如果(redisConnect==null){返回null;}CacheAccessormapCacheAccessor=CacheAccessor.forMap(newConcurrentHashMap<>());CacheFrontendcacheFrontend=ClientSideCaching.enable(mapCacheAccessor,redisConnect,//使用广播的方式,不需要在服务端存储客户端的密钥信息TrackingArgs.Builder.enabled().bcast().noloop());returncacheFrontend;}/***构建redis客户端**@paramredisConnectionFactory*@return*/privateStatefulRedisConnectiongetRedisConnect(RedisConnectionFactoryredisConnectionFactory){if(redisConnectionFactoryinstanceofLettuceConnectionFactory){AbstractRedisClientnativeClient=((LettuceConnectionFactory)redisConnectionFactory).getNativeClient();if(nativeClientinstanceofRedisClient){return((RedisClient)nativeClient).connect();}}returnnull;}对其源码展开了解的朋友可能会发现Redis服务端推送消息到客户端直接删除客户端本地缓存,然后在下次查询时直接应用Redis查询最新值,然后设置到本地缓存,这样本地缓存就不会实时更新了。有没有办法达到实时更新的效果?答案是肯定的,通过反射删除所有默认的监听器,然后添加自定义监听器,在听到key失效消息时更新本地Map。privatestaticfinalStringINVALIDATE="invalidate";privateStatefulRedisConnectionredisConnect;privateCacheAccessormapCacheAccessor;privateRedisCommandsredisCommands;/***用法:RedisPushListenerConfig.Builder.autoConfiguration(redisConnect,mapCacheAccessor);*/publicstaticclassBuilder{publicstaticvoidautoConfiguration(StatefulRedisConnectionredisConnect,CacheAccessormapCacheAccessor){newRedisPushListenerConfig(redisConnect,mapCacheAccessor);}}privateRedisPushListenerConfig(StatefulRedisConnectionredisConnect,CacheAccessormapCacheAccessor){this.redisConnect=redisConnect;this.mapCacheAccessor=mapCacheAccessor;this.redisCommands=redisConnect.sync();this.removeDefaultListeners();this.addNewListener();}/***通过反射删除所有默认监听*{@linkio.lettuce.core.support.caching.ClientSideCaching#create(CacheAccessor,RedisCache)}e}*/privatevoidremoveDefaultListeners(){try{Field字段=StatefulRedisConnectionImpl.class.getDeclaredField("pushHandler");field.setAccessible(true);PushHandlerpushHandler=(PushHandler)field.get(this.redisConnect);ListpushListeners=(CopyOnWriteArrayList)pushHandler.getPushListeners();Iteratoriterator=pushListeners.iterator();while(iterator.hasNext()){pushListeners.remove(iterator.next());}}catch(NoSuchFieldException|IllegalAccessExceptione){log.error(e.getMessage(),e);}}/***添加一个自定义监听,按键失效时更新本地Map*/privatevoidaddNewListener(){this.redisConnect.addListener(message->{if(INVALIDATE.equals(message.getType())){List