2020-08-11 19:56:23 晓掌柜 版权声明:本文为站长原创文章,转载请写明出处
一直以来都准备写一篇关于缓存和队列的文章。但是因为某些原因鸽了很久。但是呢,文章的编写还是要一直进行下去的。
正好最近项目改造进行到一个阶段(nacos + dubbo敬请期待),所以就赶紧补充上这篇文章了。
本篇文章脱胎于个人项目改造,主要是在开发过程中对一些场景和问题点的构想及处理方案的实现。
整体来说,本文会对项目体系的搭建、业务场景分析、相关经典问题的分析解决等。希望能对大家有所帮助...
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- rabbitMQ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
spring:
# redis相关配置
redis:
host: 127.0.0.1
port: 6379
password: XXX # 密码(默认为空)
timeout: 6000ms # 连接超时时长(毫秒)
jedis:
pool:
max-active: 1000 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 10 # 连接池中的最大空闲连接
min-idle: 5 # 连接池中的最小空闲连接
# RabbitMQ基础配置
rabbitmq:
host: 127.0.0.1
port: 5672
username: xa
password: guangmuhua
publisher-confirms: true
connection-timeout:
virtual-host: xa
4.1.1、什么是缓存穿透
缓存穿透简单来说就是查询一个数据库中根本不存在的数据。比如有人攻击你的系统查询一个ID为-100的数据...
4.1.2、产生原因
缓存穿透的原因有很多,一版来说比较多的是:恶意攻击、代码逻辑漏洞
4.1.3、缓存穿透的危害
缓存穿透会使得这些原本不存在于数据库中的数据请求都打到数据库中,轻则增加系统数据库压力重则服务器宕机
4.1.4、缓存穿透解决方案
① 缓存空数据:当程序检测到一个数据在缓存层和DB层都不存在时,可以写入到缓存数据中并设置短期超时
(这个时间一般不超过5min)
② 布隆过滤器:布隆过滤器是缓存和DB之间的一道屏障。机制是把所有的key写到过滤器中(也可以设置特定的规则,
比如 > 0 )在数据请求时先判断是否在过滤器中有数据存在
③ 相较而言
缓存空数据适合:key的数量有限,但是请求重复率过高
布隆过滤器适合:key值比较多,请求重复率较低
4.2.1、什么是缓存雪崩
缓存可以当做是数据保护的一道屏障,但是如果因为缓存服务器宕机,导致有大量的数据请求一起打到数据库而导致数据库
瞬时压力暴增甚至是崩溃。
4.2.2、解决方案
Redis集群,Hystrix来进行熔断、降级、限流 (TODO:这里暂时还没有详细实践)
4.3.1、什么是缓存击穿
一版来说我们会给缓存数据设置过期时间(建议都这样操作,可以根据具体场景来规划时间的长短)。但是,当缓存数据
集体失效,使得大量的数据请求蜂拥到数据库
4.3.2、解决方案
① 在设置过期时间时增加一个随机的变量(建议在原有规则上做小范围浮动)已到达某种程度上的随机过期时间
② 互斥锁:只允许一个线程重建缓存数据
③ 永不过期: 不推荐
4.4.1、什么是双写一致性
在数据变更的时候,我们是要更新缓存和DB两个层面的数据的。如果说这个删除和重新写如的顺序规划不好就可能导致
缓存和数据库中的数据不一致。
4.4.2、如果有数据强一致性的要求,慎重使用缓存
4.4.3、应对方案
① 先更新数据库,再更新缓存
如果有A和B两个请求进行更新,则可能出现线面的情况:
(1)线程A更新了数据库
(2)线程B更新了数据库
(3)线程B更新了缓存
(4)线程A更新了缓存
这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了
脏数据,因此不考虑。
② 先删除缓存,再更新数据库
同时有A进行更新操作,B进行查询操作:
(1)请求A进行写操作,删除缓存
(2)请求B查询发现缓存不存在
(3)请求B去数据库查询得到旧值
(4)请求B将旧值写入缓存
(5)请求A将新值写入数据库
上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。
③ 先更新数据库,再删除缓存
这种是采用比较多的一个方案(但是可能会出现下面的情况):
(1)假设有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生
(2)缓存刚好失效
(3)请求A查询数据库,得一个旧值
(4)请求B将新值写入数据库
(5)请求B删除缓存
(6)请求A将查到的旧值写入缓存
这样,脏数据就产生了,然而上面的情况是假设在数据库写请求比读请求还要快。实际上,工程中数据库的读操作
的速度远快于写操作的。
4.4.4、附上一篇文章链接
import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author XA
* date 2020/5/23 17:12
* description redis配置类
* params
* return
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
/**
* 功能描述:retemplate相关配置
* Param: [factory]
* Return: org.springframework.data.redis.core.RedisTemplate<java.lang.String,java.lang.Object>
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
/* 配置连接工厂 */
template.setConnectionFactory(factory);
/* 使用StringRedisSerializer来序列化和反序列化redis的key值 */
template.setValueSerializer(new StringRedisSerializer());
template.setKeySerializer(new StringRedisSerializer());
/* 设置hash key 和value序列化模式 */
FastJsonRedisSerializer fastJsonRedisSerializer= new FastJsonRedisSerializer(Object.class);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(fastJsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
/**
* 功能描述:对hash类型的数据操作
* Param: [redisTemplate]
* Return: org.springframework.data.redis.core.HashOperations<java.lang.String,java.lang.String,java.lang.Object>
*/
@Bean
public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForHash();
}
/**
* 功能描述:对redis字符串类型数据操作
* Param: [redisTemplate]
* Return: org.springframework.data.redis.core.ValueOperations<java.lang.String,java.lang.Object>
*/
@Bean
public ValueOperations<String, Object> valueOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForValue();
}
/**
* 功能描述: 对链表类型的数据操作
* Param: [redisTemplate]
* Return: org.springframework.data.redis.core.ListOperations<java.lang.String,java.lang.Object>
*/
@Bean
public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForList();
}
/**
* 功能描述:对无序集合类型的数据操作
* Param: [redisTemplate]
* Return: org.springframework.data.redis.core.SetOperations<java.lang.String,java.lang.Object>
*/
@Bean
public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForSet();
}
/**
* 功能描述:对有序集合类型的数据操作
* Param: [redisTemplate]
* Return: org.springframework.data.redis.core.ZSetOperations<java.lang.String,java.lang.Object>
*/
@Bean
public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForZSet();
}
}
/**
* 功能描述: 执行保存业务操作
* Param: [articleMqDTO]
* Return: void
*/
private void doSave(ArticleMqDTO articleMqDTO){
long time = 1;
if("null".equalsIgnoreCase(articleMqDTO.getTitle())){
time = 60*20*(int)(Math.random() * 5 + 1);
}else{
time = 60*60*24*(int)(Math.random() * 30 + 1);
}
redisUtil.hset("article" + articleMqDTO.getId(), articleMqDTO.getId().toString(), articleMqDTO, time);
}
水平所限,欢迎交流。下一篇是MQ,敬请期待。