当并发量提高的时候数据库就支撑不了很高的并发,这时候我们就可以引入redis来做一个数据库的缓存,来减小数据库的压力,当数据库第一次被查询之后,就把数据库查出来的结果用来存到redis当中
redis简介->入门
这样下一个请求来的时候就去redis里面了,就减轻了数据库的压力
缓存使用的简单的策略
redis的整合步骤
1. 将redis整合到项目中(redis+spring)
a.首先肯定是引入依赖
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
b.写一个Redis的工具类->用来将redis的池初始化到spring容器中
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;
}
}
c.写一个Spring整合redis的配置类
将redis的连接池创建到spring的容器中
@Configuration
public class RedisConfig {
//读取配置文件中的redis的ip地址
@Value("${spring.redis.host:disabled}")
private String host = “192.168.222.20”;
@Value("${spring.redis.port:0}")
private int port = “6179”;
@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); //初始化连接池
//@Bean注解就会把这个 return的这个对象注入到容器当中
return redisUtil;
}
}
3. 设计一个数据存储策略
->企业中的存储策略 数据对象名:数据对象ID:对象属性
eg:User:123:Info ->这个代表的就是存储的123这个用户的信息(info)
当然也可以直接使用RedisTemplate或者是StringRedisTemplate来直接使用
缓存list
ShCatalog shCatalog = null;
List<ShCatalog> list = new ArrayList<>();
String key = "catalog:parendId-" + id + ":";
if (redisTemplate.hasKey(key)) {
List<T> resule = (List<T>) redisTemplate.opsForList().range(key, 0, -1);
System.out.println("查了redis");
list = (List<ShCatalog>) resule;
} else {
System.out.println("查了数据库");
list = shCatalogRepository.findByParentId(id);
redisTemplate.opsForList().leftPush(key, JSON.toJSONString(list));
}
QueryResult<ShCatalog> queryResult = new QueryResult<>();
queryResult.setList(list);
queryResult.setTotal(list.size());
return new QueryResponseResult(CommonCode.SUCCESS, queryResult);
缓存对象
ShCatalog shCatalog = null;
String key = "catalog:" + id + ":";
if (redisTemplate.hasKey(key)) {
String catalogInfoJson = redisTemplate.opsForValue().get(key);
shCatalog = JSON.parseObject(catalogInfoJson, ShCatalog.class);
} else {
shCatalog = shCatalogRepository.getOne(id);
redisTemplate.opsForValue().set(key, JSON.toJSONString(shCatalog));
}
QueryResult<ShCatalog> queryResult = new QueryResult<>();
List<ShCatalog> list = new ArrayList<>();
list.add(shCatalog);
queryResult.setList(null);
queryResult.setTotal(0);
return new QueryResponseResult(CommonCode.SUCCESS, queryResult);
缓存在高并发和安全压力下的一些问题
缓存穿透
1.缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但数据库也是无此记录,并且处于容错考虑,我们没有将这次查询的null写入缓存.
这将导致这个不存在的数据每次请求都要到数据库去查询,失去了缓存的意义,在流量大的时候,可能DB就挂掉了,要是有人利用不存在的key频繁的攻击我们的应用,这就是漏洞
2.解决:**空结果进行缓存,但他的过期时间会很短,最长不超过五分钟
list = shCatalogRepository.findByParentId(id);
if (list.size() == 0) {
// 表示数据库中也没有这个数据,但是为了防止缓存穿透 所以我们要把空存到redis中
redisTemplate.opsForList().leftPush(key, "");
// 设置过期时间为三分钟
redisTemplate.expire(key, 3, TimeUnit.MILLISECONDS);
} else {
// 有数据
redisTemplate.opsForList().leftPush(key, JSON.toJSONString(list));
}
缓存击穿
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被高并发的访问,是一种非常热点的数据,这个时候,需要考虑一个问题:如果这个key在大量请求同时进来的时候正好失效,那么所有对这个key的数据查询都落在DB,我们称为缓存击穿 简要来说就是某一个热点key在高并发访问的情况下突然失效,导致大量的并发打进数据库
------>和缓存雪崩之间的区别
1.击穿是一个热点key失效
2.雪崩是很多key集体失效
解决方法:
1.通过锁,限制请求数量,
使用redis分布式锁的机制解决DB的压力问题
第一种分布式锁:redis自带的一个分布式锁,set ex nx
setnx 就是 set key value 不同的是 只有当redis中没有 这个key的时候才能够set成功,如果有了的话就会set失败.这样的话再同一时间就只有一个才能够去set成功,然后就只让这个set成功的线程去访问数据库,访问完成之后再删除这个,其他线程才能再去set成功
//设置nx 分布式锁 -> 防止缓存击穿
Boolean isSuccess = redisTemplate.getConnectionFactory().getConnection().setNX(keyFbs.getBytes(), "1".getBytes());
// 设置分布式锁的过期时间 10秒过期
redisTemplate.expire(keyFbs, 10, TimeUnit.SECONDS);
// 如果设置成功 表示拿到了lock 可以进行数据库访问 在10s的过期时间内可以访问数据库
if (isSuccess) {
list = shCatalogRepository.findByParentId(id);
// 在访问完成DB之后应该释放分布式锁
redisTemplate.delete(keyFbs);
} else {
// 如果设置失败 代表没有权利访问数据库 自旋->该线程在睡眠几秒后再重新访问本方法
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 这个方法findCatalogByIdList 就是现在运行的方法的名字 就是重新调用一次这个方法
return findCatalogByIdList(id);
}
第二种分布式锁:redisson框架,一个redis的带有juc的lock的功能实现(既有jedis的功能,又有juc的功能)
问题一:
如果在redis中的锁已经过期,然后锁过期的那个请求又执行完毕,回来删锁(删的是其他请求的锁)怎么办?
问题描述:就是一个请求加了锁去访问数据库了,然后锁都过期了还没访问完,这个时候另一个线程加了锁去访问数据库了.前面那个线程又访问完了回来删锁,删除了第二个线程的锁,这个时候怎么办
解决办法: 设置锁的时候设置了一个key和一个value ,虽然她们key是一样的但是我们设置锁的时候value设置为当前线程的token(token不一样)->直接获取一个UUID就行了<-将来删除锁的时候,根据value来判断是不是自己的加的那把锁
byte[] suoVal = redisTemplate.getConnectionFactory().getConnection().get(keyFbs.getBytes());
String tempsuo = suoVal.toString();
//如果是自己的锁才有权利删除
if (StringUtils.isNotEmpty(tempsuo) && tempsuo.equals(suoValue)) {
// 在访问完成DB之后应该释放分布式锁
redisTemplate.delete(keyFbs);
}
问题二:
如果碰巧在查询redis锁还没删除的时候,正在网络传输时,锁过期了怎么办?
问题解释:
就在你上面代码判断是自己的锁的时候锁过期了怎么办? ->临界点
那就查询到的一瞬间删除 ,如果没有查询到就不删
用Lua
缓存雪崩
缓存雪崩是值我们设置缓存时采用了相同的过期时间,导致缓存某一时刻同时失效,请求全部转发到DB,DB瞬时压力过大过重雪崩,
解决办法 原有的失效时间基础上增加一个随机值,如果1-5分钟的随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件.
上文中代码片段的完整代码
public QueryResponseResult findCatalogByIdList(String id) {
ShCatalog shCatalog = null;
List<ShCatalog> list = new ArrayList<>();
String key = "catalog:parendId-" + id + ":";
String keyFbs = "catalog" + id;
String suoValue = UUID.randomUUID() + "";
if (redisTemplate.hasKey(key)) {
List<T> resule = (List<T>) redisTemplate.opsForList().range(key, 0, -1);
System.out.println("查了redis");
list = (List<ShCatalog>) resule;
} else {
// 查询数据库的这个分支是应该受到保护的 尽量少去查询数据库
System.out.println("查了数据库");
//设置nx 分布式锁 -> 防止缓存击穿
Boolean isSuccess = redisTemplate.getConnectionFactory().getConnection().setNX(keyFbs.getBytes(), suoValue.getBytes());
// 设置分布式锁的过期时间 10秒过期
redisTemplate.expire(keyFbs, 10, TimeUnit.SECONDS);
// 如果设置成功 表示拿到了lock 可以进行数据库访问 在10s的过期时间内可以访问数据库
if (isSuccess) {
list = shCatalogRepository.findByParentId(id);
byte[] suoVal = redisTemplate.getConnectionFactory().getConnection().get(keyFbs.getBytes());
String tempsuo = suoVal.toString();
//如果是自己的锁才有权利删除
if (StringUtils.isNotEmpty(tempsuo) && tempsuo.equals(suoValue)) {
// 在访问完成DB之后应该释放分布式锁
redisTemplate.delete(keyFbs);
}
} else {
// 如果设置失败 代表没有权利访问数据库 自旋->该线程在睡眠几秒后再重新访问本方法
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 这个方法findCatalogByIdList 就是现在运行的方法的名字 就是重新调用一次这个方法
return findCatalogByIdList(id);
}
// 防止缓存穿透
if (list.size() == 0) {
// 表示数据库中也没有这个数据,但是为了防止缓存穿透 所以我们要把空存到redis中
redisTemplate.opsForList().leftPush(key, "");
// 设置过期时间为三分钟
redisTemplate.expire(key, 3, TimeUnit.MILLISECONDS);
} else {
// 有数据
redisTemplate.opsForList().leftPush(key, JSON.toJSONString(list));
}
}
QueryResult<ShCatalog> queryResult = new QueryResult<>();
queryResult.setList(list);
queryResult.setTotal(list.size());
return new QueryResponseResult(CommonCode.SUCCESS, queryResult);
}
setnx这个在redistemplate当中也有
实现分布式锁 使用redisson
首先引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.1</version>
</dependency>
在启动类中注入redisson
@Bean
public Redisson redisson(){
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
return (Redisson)Redisson.create();
}
使用
public void lockDemo() {
String id = "lockTest";
RLock redissonLock = redisson.getLock(id);//得到一个锁对象
redissonLock.lock(10, TimeUnit.SECONDS);//加锁
int value = Integer.valueOf(redisTemplate.opsForValue().get("have"));
if (value > 0) {
value--;
redisTemplate.opsForValue().set("have", value + "");
System.out.println("买到了货物---现在还剩" + value);
} else {
System.out.println("卖完了 没有买到");
}
redissonLock.unlock();//解锁
}