聊一聊缓存和数据库不一致性问题的产生及主流解决方案以及扩展的思考1:https://developer.aliyun.com/article/1394667
2.3、Redis的序列化机制
Redis 的默认序列化机制是JDK序列化机制,这点可以在下图的源码中看出。
从源码中也可以看出,如果没有设置序列化机制,则defaulatSeralizer = new JdkSerializationRedisSerializer()
,可以明显看出,使用的就是JDK 的序列化机制。
JDK默认序列化机制并非不能使用,只是它具有一定的局限性
- 它只适用于Java项目,对其他语言编写的项目不兼容,如Go或者PHP
- 在Redis的可视化页面,无法进行较好的展示
我们需要将 Redis 的默认序列化机制改为JSON
格式,一方面兼容性较高,另一方面在可视化界面也较好查看。
在修改之前,先看一眼默认的JdkSerializationRedisSerializer
类吧。
public class JdkSerializationRedisSerializer implements RedisSerializer
我们可以看到它也是实现了
RedisSerializer
接口,点进接口去,在IDEA
中按下ctrl+H
可以查看整个类从上至下的树结构。
我们来可以看看它的实现类有哪些,有没有已经实现
JSON
相关的序列化机制的实现类。
从接口实现树上可以看到,有三个可以直接转换为JSON序列化的实现类
GenericJackson2JsonRedisSerializer
FastJsonRedisSerializer
Jackson2JsonRedisSerializer
这三者都是可以的,具体的区别,用法搜索或者看一下源码就会大概懂的使用了~,不是我的讨论重点。
我这里直接使用的是
Jackson2JsonRedisSerializer
,作用就是序列化object
对象为json
字符串。
2.4、更改Redis的默认序列化机制
那么如何更改勒?
在之前也已经看到在
RedisAutoConfiguration
中已经帮我们注入了RedisTemplete和StringTemplete
,我们现在要更改他们的设置,所以就改为手动注入。
在
RedisTemplete
中的afterPropertiesSet
方法中可以看到当RedisSerializer defaultSerializer;
当为 null
时,默认使用JdkSerializationRedisSerializer
的序列机制。
那么更改就很简单啦~
创建一个 MyRedisConfig 配置类,将
RedisTemplete和StringTemplete
改为手动注入,在注入RedisTemplete
时,手动set一个Jackson2JsonRedisSerializer
类即可。
/** * @description: * @author: Ning Zaichun * @date: 2022年09月06日 23:21 */ @Configuration public class MyRedisConfig { @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL); serializer.setObjectMapper(objectMapper); //设置value 值的序列化 redisTemplate.setValueSerializer(serializer); //key的序列化 redisTemplate.setKeySerializer(new StringRedisSerializer()); // set hash hashkey 值的序列化 redisTemplate.setHashKeySerializer(new StringRedisSerializer()); // set hash value 值的序列化 redisTemplate.setHashValueSerializer(serializer); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.afterPropertiesSet(); return redisTemplate; } @Bean public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { return new StringRedisTemplate(redisConnectionFactory); } }
测试: 还是之前一样的代码
/** * 虽然大家可以在Java 程序中看到取出来的值是正常的, * 但是在平时开发和测试的时候,我们还是需要借助 Redis 的可视化工具来查看的, * 你会发现,我们采用默认的序列化机制(JDK序列化机制),在Redis可视化软件中,会无法直接查看的, * 都是转码之后的数据: \xac\xed\x00\x05t\x00\x06user:2 * 图片见下面第二张图 */ @Test public void test8() { Map<String, Student> map = new HashMap<>(); Student s1 = new Student(); s1.setSchool("xxxx1"); s1.setUsername("ningzaichun1号"); s1.setAge(3); Student s2 = new Student(); s2.setSchool("xxxx2"); s2.setUsername("ningzaichun2号"); s2.setAge(5); map.put("user:1",s1); map.put("user:2",s2); redisTemplate.opsForHash().putAll("student:key",map); List<Object> values = redisTemplate.opsForHash().values("student:key"); values.forEach(System.out::println); //Student(username=ningzaichun1号, school=xxxx1, age=3) //Student(username=ningzaichun2号, school=xxxx2, age=5) }
效果图:
2.5、RedisConnectionFactory
想了想,都写到这里来了,还是说说
RedisConnectionFactory
(Redis连接工厂)吧。
SpringBoot 对于 Redis 连接工厂的实现有两个,一个是
SpringBoot 2.0
默认使用的`LettuceConnectionFactory
,另一个是早期就出现的JedisConnectionFactory
。
首先得说明,两者是有区别的。
Jedis
Jedis 是一个优秀的基于 Java 语言的 Redis 客户端
Jedis 在实现上是直接连接 Redis-Server,在多个线程间共享一个 Jedis 实例时是线程不安全的,如果想要在多线程场景下使用 Jedis,需要使用连接池,每个线程都使用自己的 Jedis 实例,当连接数量增多时,会消耗较多的物理资源。
Lettuce
SpringBoot 2.0 及之后版本 Redis 的默认连接工厂
Lettuce
则完全克服了其线程不安全的缺点;Lettuce
是一个可伸缩的线程安全的Redis
客户端,支持同步、异步和响应式模式。多个线程可以共享一个连接实例,而不必担心多线程并发问题。它基于优秀
Netty NIO
框架构建,支持 Redis 的高级功能,如 Sentinel,集群,流水线,自动重新连接和 Redis 数据模型。
一些关于
Lettuce
的使用,大家可以看看这篇文章
初探 Redis 客户端 Lettuce:真香! 作者: 博客园-vivo互联网技术
三、缓存的三大面试常客
3.1、缓存穿透
缓存穿透是指用户在不断访问一个缓存和数据库中都没有的数据。
缓存无法命中,从而导致一直请求数据库,流量过大就会导致数据库的崩溃.
如发起为id为 -1 的数据或id为特别大不存在的数据,这时的用户往往可能是恶意攻击者,这种恶意攻击会导致数据库压力非常大,扛不住的结果就是宕机。
解决方案:
1、缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也没有,如果我们不存储这个空数据,那么持续的访问就会导致我们的数据库压力倍增,此时我们就可以将空结果(null)存入到缓存中并且设置一个较短的过期时间。
2、接口层增加校验,如用户鉴权校验,编写一些特殊数据的校验,预防这样的事故的发生。如将id<=0的查询请求直接拒绝掉。
3.2、缓存雪崩
Redis挂掉了,请求全部走数据库。
如一个系统,每天的高峰期是每秒5000个请求,Redis缓存在的时候,可以差不多抗住,但是Redis的机器突然网络出问题了,完全访问不了。
那么此时每秒5000的请求都会直接打到数据库上,数据扛住了没啥事,扛不住了就是GG啦。
有哪些方案可以来预防和处理这样的故障呢?
从三个角度讨论:
1、部署角度:实现 Redis 的高可用,主从+哨兵,Redis集群。
2、应用程序角度:
本地缓存 + 限流&降级
允许的话,设置热点数据永远不过期
缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生
(但实际而言,这一点很多时候其实不做的,因为如果加上随机时间后,再碰撞又该如何呢?)
3、恢复角度:Redis 的 RDB+AOF组合持久化策略,方便redis宕机后及时恢复数据
浅谈一下限流和降级的好处:
限流会限制最高的访问人数,保证系统的正常运行,不会崩溃。
服务降级,会返回用户一些合适的提示信息,对于用户而言,刷新个四五次还是有可能访问成功的。
都可以保证系统的运行,不至于让系统崩溃。
3.3、缓存击穿
缓存击穿和缓存穿透其实非常类似,但是缓存击穿说的是数据在缓存中没有,但是在数据库中有的数据。
看到过的面试题中,最常举例的场景就是:
Redis 中大某一个热点key在突然失效,并且此时正处于高并发期间,导致流量全部打到数据库上,造成数据库极大的压力。我们通常将这样的事件称之为缓存击穿
其实读懂问题,解决方案也很好说明:
设置热点数据不过期;
第一时间去数据库获取数据填充到redis中,并且在请求数据库时需要加锁,避免所有的请求都直接访问数据库,一旦有一个请求正确查询到数据库时,将结果存入缓存中,其他的线程获取锁失败后,让其睡眠一会,之后重新尝试查询缓存,获取成功,直接返回结果;获取失败,则再次尝试获取锁,重复上述流程。
流程图:大致如下
大致流程就是这样的~
聊一聊缓存和数据库不一致性问题的产生及主流解决方案以及扩展的思考3:https://developer.aliyun.com/article/1394672