目录
1 引入pom依赖信息(本工程所有redis统一放入service-util)
2 写一个reids的工具类(用来将redis的池初始化到spring容器中)
4 注意:每个以用工程引入service-util后,单独配置自己的redis的配置文件
1 cont get a connection from the pool:一般是redis的配置有问题
一、redis整合过程
考虑到该页面是被用户高频访问的,所以性能必须进行尽可能的优化。
一般一个系统最大的性能瓶颈,就是数据库的io操作。从数据库入手也是调优性价比最高的切入点。
一般分为两个层面,一是提高数据库sql本身的性能,二是尽量避免直接查询数据库。
提高数据库本身的性能首先是优化sql,包括:使用索引,减少不必要的大表关联次数,控制查询字段的行数和列数。另外当数据量巨大是可以考虑分库分表,以减轻单点压力。
重点要讲的是另外一个层面:尽量避免直接查询数据库。
解决办法就是:缓存
缓存可以理解是数据库的一道保护伞,任何请求只要能在缓存中命中,都不会直接访问数据库。而缓存的处理性能是数据库10-100倍。
结构图:
1 引入pom依赖信息(本工程所有redis统一放入service-util)
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
分别按照之前的方式放到parent模块和service-util的pom文件中。
2 写一个reids的工具类(用来将redis的池初始化到spring容器中)
由于redis作为缓存数据库,要被多个项目使用,所以要制作一个通用的工具类,方便工程中的各个模块使用。而主要使用redis的模块,都是后台服务的模块,xxx-service工程。所以咱们把redis的工具类放到service-util模块中,这样所有的后台服务模块都可以使用redis。
RedisUtil就是被注入的工具类以供其他模块调用。
public class RedisUtil {
private JedisPool jedisPool;
public void initPool(String host,int port ,int database){
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(200);
poolConfig.setMaxIdle(30);
poolConfig.setBlockWhenExhausted(true);
poolConfig.setMaxWaitMillis(10*1000);
poolConfig.setTestOnBorrow(true);
jedisPool=new JedisPool(poolConfig,host,port,20*1000);
}
public Jedis getJedis(){
Jedis jedis = jedisPool.getResource();
return jedis;
}
}
3 写一个spring整合redis的配置类
将redis的链接池创建到spring的容器中。RedisConfig负责在spring容器启动时自动注入
@Configuration
public class RedisConfig {
//读取配置文件中的redis的ip地址
@Value("${spring.redis.host:disabled}")
private String host;
@Value("${spring.redis.port:0}")
private int port;
@Value("${spring.redis.database:0}")
private int database;
@Bean
public RedisUtil getRedisUtil(){
if(host.equals("disabled")){
return null;
}
RedisUtil redisUtil=new RedisUtil();
redisUtil.initPool(host,port,database);
return redisUtil;
}
}
4 注意:每个以用工程引入service-util后,单独配置自己的redis的配置文件
Service-util的配置文件没有作用
同时,任何模块想要调用redis都必须在application.properties配置,否则不会进行注入。
spring.redis.host=192.168.xxx.xxx |
注意:在SpringBoot项目中启动类所在的位置至少要和RedisUtil、RedisConfig 所在的包平级;否则无法扫描到!!
5、使用:通过缓存查询的代码测试
结果:
6、在高并发环境下还有如下三个问题。
- 如果redis宕机了,或者链接不上,怎么办?
- 如果redis缓存在高峰期到期失效,在这个时刻请求会向雪崩一样,直接访问数据库如何处理?
- 如果用户不停地查询一条不存在的数据,缓存没有,数据库也没有,那么会出现什么情况,如何处理?
二、redis常见问题
1 cont get a connection from the pool:一般是redis的配置有问题
2 缓存在高并发和安全压力下的一些问题
(1)缓存穿透
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,并且处于容错考虑,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
解决: 为了防止缓存穿透将,null或者空字符串值设置给redis进行缓存,但它的过期时间会很短,最长不超过五分钟
(2)缓存雪崩
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决:设置不同的缓存失效时间。原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
(3)缓存击穿
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。
缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决:分布式锁
缓存击穿和缓存雪崩的区别:
A 击穿是一个热点key失效
B 雪崩是缓存中的很多key失效,导致数据库负载过重宕机
C 穿透是利用不存在的key去攻击mysql数据库
(4)分布式锁
击穿:在正常的访问情况下,如果缓存失效,如何保护mysql,重启缓存的过程
使用redis数据库的分布式锁,解决mysql的访问压力问题
第一种分布式锁:redis自带一个分布式锁,set px nx
核心代码
加锁 String token = UUID.randomUUID().toString(); String lock = jedis.set(key, token, "NX", "EX",20); |
第二种分布式锁:redisson框架,一个redis的带有juc的lock功能的客户端的实现(既有jedis的功能,又有juc的锁功能)
https://github.com/redisson/redisson/wiki/1.-Overview
面试题:
问题1 如果在redis中的锁已经过期了,然后锁过期的那个请求又执行完毕,回来删锁,删除了其他线程的锁怎么办?
答:在设置分布式锁的时候利用token来唯一标识,再删除之前先判断一下,判断token值是不是自己的token
问题2 如果碰巧在查询redis锁还没删除的时候,正在网络传输,锁过期了,怎么办?(意思就是,在get的时候锁还没过期,在进入判断准备删除的时候,锁过期了,怎么办)
答:可与用lua脚本,在查询到key的同时删除该key,防止高并发下的意外的发生
String script ="if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, Collections.singletonList("lock"),Collections.singletonList(token));
--------------------------------------------------------------------------------------------------------------------------------------------------
附Service层完整代码:
@Override
public PmsSkuInfo getSkuById(String skuId ) {
PmsSkuInfo pmsSkuInfo = new PmsSkuInfo();
//链接缓存
Jedis jedis = redisUtil.getJedis();
//查询缓存
String skuKey = "sku:"+skuId+":info";
String skuJson = jedis.get(skuKey);
if (StringUtils.isNotBlank(skuJson)){
pmsSkuInfo = JSON.parseObject(skuJson,PmsSkuInfo.class);
}else {
//如果缓存中没有
//设置分布式锁
String token = UUID.randomUUID().toString();
String OK = jedis.set("sku:"+skuId+":lock", token, "NX", "PX", 10*1000);//拿到锁的线程有10s过期时间
if(StringUtils.isNoneBlank(OK) && OK.equals("OK")){
//设置成功,有权在10s的过期时间内访问数据库
pmsSkuInfo = getSkuByIdFromDb(skuId);//原查询数据库的方法
//将结果存入redis
if (pmsSkuInfo !=null) {
jedis.set(skuKey, JSON.toJSONString(pmsSkuInfo));
}else {
//数据库中不存在该sku
//为了防止缓存穿透,将null值设置给redis或者空字符串并设置过期时间
jedis.setex(skuKey, 60*3,JSON.toJSONString(""));
}
//在访问mysql后,将mysql的分布式锁释放
String lockToken = jedis.get("sku:" + skuId + ":lock");
if (StringUtils.isNoneBlank(lockToken) && lockToken.equals(token)) {
//可与用lua脚本,在查询到key的同时删除该key,防止高并发下的意外的发生
jedis.del("sku:" + skuId + ":lock");//用token确认删除的是自己的锁
}
}else{
//设置失败,自旋(该线程在睡眠几秒后,重新尝试访问本方法)
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getSkuByIdFromDb(skuId);//不加return表示多了一条线程
}
}
//关闭jedis
jedis.close();
return pmsSkuInfo;
}