目录
速度快,好用,内存的读写性能远高于磁盘,缓存可以大大降低用户访问并发量带来的服务器读写压力
缓存模型和思路
标准的操作方式就是查询数据库之前先查询缓存,如果缓存数据存在,则直接从缓存中返回,如果缓存数据不存在,再查询数据库,然后将数据存入redis
代码思路:如果缓存有,则直接返回,如果缓存不存在,则查询数据库,然后存入redis。
缓存更新策略
目的:为了节约内存
内存淘汰:redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)
超时剔除:当我们给redis设置了过期时间ttl之后,redis会将超时的数据进行删除,方便咱们继续使用缓存
主动更新:我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题
数据库和缓存不一致
数据库的数据发送变化,缓存没有同步,此时会有数据一致性问题存在
解决方案:
应当是先操作数据库,再删除缓存,原因在于,如果你选择第一种方案,在两个线程并发来访问时,假设线程1先来,他先把缓存删了,此时线程2过来,他查询缓存数据并不存在,此时他写入缓存,当他写入缓存后,线程1再执行更新动作时,实际上写入的就是旧的数据,新的数据被旧数据覆盖了。
1.先删除缓存,再操作数据库时:更新数据库的时间较长,介入其他线程的概率很打
2.先操作数据库,再删除缓存时:写缓存的速度很快,介入其他线程的概率很小
缓存与数据库双写一致
如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间;采用删除策略,来解决双写问题,修改数据时,先修改数据库,再删除缓存(例如:根据id修改数据时)
当我们修改了数据之后,然后把缓存中的数据进行删除,查询时发现缓存中没有数据,则会从mysql中加载最新的数据,从而避免数据库和缓存不一致的问题
缓存穿透
缓存穿透 :缓存穿透是指客户端请求的数据在缓存中和数据库中
都不存在
,这样缓存永远不会生效,这些请求都会访问数据库
。
解决方案:(访问这个不存在的数据,那么在redis中也能找到这个数据就不会进入到数据库了)
缓存空对象 实现简单,维护方便(额外的内存消耗,可能造成短期的不一致)
布隆过滤 内存占用较少,没有多余key (实现复杂,哈希思想,存在哈希冲突,有误判可能)
主动防止:
增强id的复杂度,避免被猜测id规律
做好数据的基础格式校验
加强用户权限校验
做好热点参数的限流
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
-
给不同的Key的TTL添加随机值
-
利用Redis集群提高服务的可用性
-
给缓存业务添加降级限流策略
-
给业务添加多级缓存
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key过期突然失效了,很多的请求访问会在瞬间给数据库带来巨大的冲击。
线程A在查询数据库并重新加载到缓存的
期间
,有大量的
其它线程来访问这些数据,缓存中没有,去数据库中找,导致数据库访问压力过大如下图,在这50ms内还没有写入缓存,导致大量的请求到达数据库
常见的解决方案有两种:
-
互斥锁
大量同时访问这些数据时,只能有线程A去访问数据库,在访问时加上锁,其它线程获取锁失败,处于循环等待中,直到线程A访问数据库完毕,写入了缓存,使得其它线程可以缓存命中了
互斥锁实现逻辑:
-
逻辑过期
出现这个缓存击穿问题,主要原因是在于我们对key设置了过期时间,逻辑过期是不设置过期时间,而是把过期时间设置在 redis 的value中,后续通过逻辑去处理
在线程A,发现逻辑时间过期,获取有锁成功,会另开一个线程去完成查询数据重建缓存,线程A返回旧数据;当其它线程来时,发现获取锁失败会直接返回旧数据,直到另开的线程锁释放后才能得到新数据
逻辑过期实现逻辑:
对比:
分布式锁
简单理解:满足分布式系统或集群模式下多进程可见并且互斥的锁
场景:当在一个业务场景下,多线程并发执行时,可能出现安全问题,在一个进程时使用synchronized关键字加锁即可,防止在执行本次业务时其它线程介入,导致出现安全问题。但这只是单个进程,当多个进程或者在分布式场景下,即有多个Java虚拟机(JVM)执行业务,依然会造成这种安全问题。此时需要利用到分布式锁,统一管理执行进程的锁获取和锁释放。
分布式锁他应该满足的一些条件:
- 可见性:多个线程都能看到相同的结果,注意:这个地方说的可见性并不是并发编程中指的内存可见性,只是说多个进程之间都能感知到变化的意思
- 互斥:互斥是分布式锁的最基本的条件,使得程序串行执行
- 高可用:程序不易崩溃,时时刻刻都保证较高的可用性
- 高性能:由于加锁本身就让性能降低,所有对于分布式锁本身需要他就较高的加锁性能和释放锁性能
- 安全性:安全也是程序中必不可少的一环
实现:
利用Redis中的setnx(只有在没有这个key时才能添加缓存成功,即只有在没有这个锁时才能获取锁成功)。还需要加上锁失效的时间,因为在某种意外情况出现错误或宕机时,锁没有得到释放,业务也就出现无法执行等问题。
在Redis中的命令便是:
set keyLock value ex 5 nx #设置keyLock的值为value,失效时间为ex,nx 表示没有这个key时才能添加成功
springboot项目中使用spring-boot-starter-data-redis实现
实现加锁和释放锁的代码:
public class SimpleRedisLock implements ILock{
private final StringRedisTemplate stringRedisTemplate;
private final String name;
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"-";
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
@Override
public boolean tryLock(long expireTime) {
//线程标识
String threadId = ID_PREFIX+Thread.currentThread().getId();
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, expireTime, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success); // BooleanUtil.isTrue(success) 防止自动拆箱为null时出错
}
@Override
public void unLock() {
//线程标识
String threadId = ID_PREFIX+Thread.currentThread().getId();
//获取锁中的标识
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
if (threadId.equals(id)){ //判断锁是不是当前线程的,若不是,则说明此线程的锁已过期释放,无需释放锁
//释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
}
这样分布式场景下,不在同一个进程中,也能用redis统一管理锁,是否能获取到锁则是在Redis决定
模拟高并发:JMeter
下载安装:Jmeter安装教程【5.5】【Windows】jmeter详细安装配置教程,装不好你打我-优快云博客