缓存
1、本地缓存
本地缓存,在java中,所谓的本地缓存,就是根据map对象来存储,在查询前先去查询map里面有没有这个东西,如果没有就去数据库查询,查询出来后并存储到map里面,这样下次查询map里面就有这个东西了,就没必要去数据库查询了,直接去map里面取出数据。
2、Redis缓存
1、为什么学习redis缓存
首先本地的缓存无法满足分布式的项目需求,因为本地缓存是存储在应用中的,假如第一次路由到了第一台服务器上,发现没有然后去数据库查询了,查询出来后存入了map里面,当下次再去路由的时候路由到另一台机器上那么就还需要去数据库查询,这样是极大地消耗资源和时间。
所以就出现了统一管理缓存的中间件,比如redis、Memcached等。
2、redis是什么
Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
redis的出现,很大程度补偿了memcached这类key/value存储的不足,在部分场合可以对关系数据库起到很好的补充作用。它提供了Java,C/C++,C#,PHP,JavaScript,Perl,Object-C,Python,Ruby,Erlang等客户端,使用很方便。
Redis支持主从同步。数据可以从主服务器向任意数量的从服务器上同步,从服务器可以是关联其他从服务器的主服务器。这使得Redis可执行单层树复制。存盘可以有意无意的对数据进行写操作。由于完全实现了发布/订阅机制,使得从数据库在任何地方同步树时,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助。
3、redis的优势
-
性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
-
丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
-
原子 – Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。
-
丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。
-
redis 性能测试工具可选参数如下所示:
序号 选项 描述 默认值 1 -h 指定服务器主机名 127.0.0.1 2 -p 指定服务器端口 6379 3 -s 指定服务器 socket 4 -c 指定并发连接数 50 5 -n 指定请求数 10000 6 -d 以字节的形式指定 SET/GET 值的数据大小 2 7 -k 1=keep alive 0=reconnect 1 8 -r SET/GET/INCR 使用随机 key, SADD 使用随机值 9 -P 通过管道传输 请求 1 10 -q 强制退出 redis。仅显示 query/sec 值 11 –csv 以 CSV 格式输出 12 -l 生成循环,永久执行测试 13 -t 仅运行以逗号分隔的测试命令列表。 14 -I Idle 模式。仅打开 N 个 idle 连接并等待。
4、redis与其他key-value的区别?
- Redis有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。
- Redis运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,应为数据量不能大于硬件内存。在内存数据库方面的另一个优点是, 相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样Redis可以做很多内部复杂性很强的事情。 同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。
5.安装redis以及配置
安装和配置可自行网上查询,或者使用如下地址进行配置。
https://www.runoob.com/redis/redis-install.html
6.springboot整合Redis
1.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.配置redis的链接信息
spring:
redis:
host: 127.0.0.1
port: 6379
password: 123456
3.使用springboot提供的redistemplate来使用对redis 的操作
在Controller注入redistemplate
@Autowired
private RedisTemplate<String,String>redisTemplate;
4.RedisTemplate的一些方法作用
// redisTemplate.opsForValue();//操作字符串
// redisTemplate.opsForHash();//操作hash
// redisTemplate.opsForList();//操作list
// redisTemplate.opsForSet();//操作set
// redisTemplate.opsForZSet();//操作有序set
5.redis常用于一些访问量高的数据,而且比经常改变的数据。
逻辑是:首先前端发送请求到controller,然后接收到请求,判断redis里面有没有这条数据,如果有就直接从redis取出来,没有的话就去数据库查询,查询出来后再存入redis一份。例子如下:
7.Redis在高并发下的问题
1.缓存穿透
概念
缓存穿透就是访问数据库中不存在的数据,高并发情况下或有人恶意的不停的访问该数据,导致请求打到数据库,直至数据库崩溃.
比如我们在写项目的时候,商品的主键id很少为负数.那么可能有人就会不停的访问id为-1的商品.而这个商品又不存在,导致我们的数据库崩溃.
解决方案
这里我通常的解决方案是在一个请求打到数据库返回null值时,在redis中给这个商品的key存入一个empty数据.使后边的请求直接访问redis而不是再去访问数据库.
jedis.setex(key,60,"empty");
2.缓存雪崩
概念
缓存雪崩就是我们在设置缓存数据时,设置的有效期相同,导致在同一时刻,大部分的缓存同时到期,这时候,所有的请求在redis中拿不到数据,都去访问数据库,数据库就受到了所有请求的压力,在高并发的情况下,成千上万个请求同时到达数据库,导致数据库崩溃.
解决方案
对于缓存雪崩,我们通过概念可以发现,之所以会雪崩,就是因为多个缓存同时到期,那我们让他不同时到期就可以了,实现方式就是利用setex命令和Random类生成随机数的方式为缓存设置随机有效时间.这样就基本上不会出现多个缓存同时到期的现象了,也就是基本上不会出现缓存雪崩的现象了.
int random=new Random().nextInt(60*10);
1
jedis.setex(key,random,"empty");
3.缓存击穿
概念
缓存击穿像
是缓存穿透和缓存雪崩的结合,意思是一个数据太热点,在高并发情况下,这个数据的缓存到期,成千上万的请求又绕过了redis直接打到了数据库,导致数据库的崩溃.
解决方案
加锁.
加锁分为俩种,一种为本地锁,一种为分布式锁,本地锁的速度快,分布式所得速度比较慢,但是本地锁有一个问题就是他的锁是锁的当前线程,如果遇到分布式的话就会出现一个在一台机器上加了本地锁,而下次启动可能改服务就在另一台服务器上了,然后又去锁了一次。分布式锁就是一把限制人流的锁,像一个保安一样,守护着进入数据库的入口.对于抢到锁的那一个请求才会放行,后面的请求只能排队.等待抢到锁的人进入数据库将数据从数据库取出来存到缓存中,才允许后面的人进来访问,此时访问的时候,前面的人已经将数据存放到缓存中,则后面的人就可以从Redis中取数据而不是直接访问数据库了.
1.本地锁实现
可以通过锁线程方式实现,this表示当前线程
synchronized(this){
//在此之前先去查询redis有没有当前对象,如果没有再去数据库查询
if(!StringUtils.isEmpty(key)){
//缓存不为空直接返回
Map<Stirng,List<类型>> result=JSON.parseObject("key",new TypeReference<Map<Stirng,List<类型>>>);
return result;
}
//对数据库的查询操作..........
//查询数据库再放入缓存,将对象转为json形式放入缓存中
}
本地锁的流程
2.分布式锁
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 加锁
* @param targetId targetId - 商品的唯一标志
* @param timeStamp 当前时间+超时时间 也就是时间戳
* @return
*/
public boolean lock(String targetId,String timeStamp){
if(stringRedisTemplate.opsForValue().setIfAbsent(targetId,timeStamp)){
// 对应setnx命令,可以成功设置,也就是key不存在
return true;
}
// 判断锁超时 - 防止原来的操作异常,没有运行解锁操作 防止死锁
String currentLock = stringRedisTemplate.opsForValue().get(targetId);
// 如果锁过期 currentLock不为空且小于当前时间
if(!Strings.isNullOrEmpty(currentLock) && Long.parseLong(currentLock) < System.currentTimeMillis()){
// 获取上一个锁的时间value 对应getset,如果lock存在
String preLock =stringRedisTemplate.opsForValue().getAndSet(targetId,timeStamp);
// 假设两个线程同时进来这里,因为key被占用了,而且锁过期了。获取的值currentLock=A(get取的旧的值肯定是一样的),两个线程的timeStamp都是B,key都是K.锁时间已经过期了。
// 而这里面的getAndSet一次只会一个执行,也就是一个执行之后,上一个的timeStamp已经变成了B。只有一个线程获取的上一个值会是A,另一个线程拿到的值是B。
if(!Strings.isNullOrEmpty(preLock) && preLock.equals(currentLock) ){
// preLock不为空且preLock等于currentLock,也就是校验是不是上个对应的商品时间戳,也是防止并发
return true;
}
}
return false;
}
/**
* 解锁
* @param target
* @param timeStamp
*/
public void unlock(String target,String timeStamp){
try {
String currentValue = stringRedisTemplate.opsForValue().get(target);
if(!Strings.isNullOrEmpty(currentValue) && currentValue.equals(timeStamp) ){
// 删除锁状态
stringRedisTemplate.opsForValue().getOperations().delete(target);
}
} catch (Exception e) {
log.error("警报!警报!警报!解锁异常{}",e);
}
}
这个是Redis加锁和解锁的工具类,里面使用的主要是两个命令,SETNX和GETSET。
SETNX命令 将key设置值为value,如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做
GETSET命令 先查询出原来的值,值不存在就返回nil。然后再设置值 对应的Java方法在代码中提示了。 注意一点的是,Redis是单线程的!所以在执行GETSET和SETNX不会存在并发的情况。
自此基础的redis 已经有一定了解了,深入可参考如下文档进行学习。
https://blog.csdn.net/weixin_38405253/article/details/81198201