Redis学习笔记
文章学习笔记内容来源:拉勾教育大数据开发高薪训练营。
记录整理一下Redis的一些的学习笔记以方便后续复习
RDB和AOF持久化
- RDB持久化以指定的时间间隔内的Key的变化量,对内存中的数据进行全量持久化一次。
- AOF持久化记录Redis接收的每一个写操作,记录到aof文件中,对key过期的,会写入一条del命令,当执行AOF重写时,会忽略过期的key和del命令。
RDB
RDB的配置,在redis.conf中配置
save “” #不使用RDB存储 不能主从
save 900 1 #表示15分钟(900秒钟)内至少1个键被更改则进行快照。
save 300 10 #表示5分钟(300秒)内至少10个键被更改则进行快照。
save 60 10000 #表示1分钟内至少10000个键被更改则进行快照
RDB原理
- RDB是二进制压缩文件,占用空间小,便于传输
- RDB主进程Fork子进程,可以最大优化Redis性能。Redis中数据量不能太大,复制过程中主进程会阻塞
- RDB不保证数据完整性,会丢失最后一次快照以后更改的所有数据
AOF
AOF配置,在redis.conf中配置
是否开启aof
appendonly yes
#⽂文件名称
appendfilename “appendonly.aof”
#同步⽅方式
appendfsync everysec
#aof重写期间是否同步
no-appendfsync-on-rewrite no
#重写触发配置
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
#加载aof时如果有错如何处理理
#yes表示如果aof尾部⽂文件出问题,写log记录并继续执⾏行行。no表示提示写⼊入等待修复后写⼊入
aof-load-truncated yes
#⽂文件重写策略略
aof-rewrite-incremental-fsync yes
appendfsync同步模式
always:把每个写命令都⽴立即同步到aof,很慢,但是很安全
everysec:每秒同步一次,是折中⽅方案,可能会丢失1秒数据
no:redis不不处理理交给OS来处理理,⾮非常快,但是也最不安全
缓存问题
缓存的读写模式
Cache Aside Pattern
最经典的缓存加数据库读写模式
读取数据模式:读的时候,先写缓存,缓存没有的话,就读取数据库,然后取出数据后放入缓存,同时返回响应
更新数据模式:更新的时候,先更新数据库,然后再删除缓存
此种模式下面高并发脏读三种情况
- 1、先更新数据库在更新缓存:update与commit之间,更新缓存缓存,commit失败则DB与缓存数据不一致
- 2、先删除缓存在更新数据库:update与commit之间,有新的读,缓存空,读DB数据到缓存数据是旧数据,commit后DB为新数据,而DB与缓存数据不一致
- 3、先更新数据库在删除缓存(推荐):update与commit之间,有新的读,缓存空,读DB数据到缓存 数据是旧的数据,commit后DB为新数据,则DB与缓存数据不一致。可以采用延时双删策略
Read/Write Through Pattern
应用程序只操作缓存,缓存操作数据库
- Read-Through(穿透读模式/直读模式):应用程序读取缓存,缓存没有,有缓存去查询数据库,并写入到缓存。
- Write-Through(穿透写模式/直写模式):应用程序写缓存,缓存写数据库。该种模式需要提供数据库的handler,开发复杂。
Write Behind Caching Pattern
应用程序只更新缓存,缓存通过异步的方式将数据批量或合并后更新到DB中,不能实时同步,可能丢失数据。
缓存过期和淘汰策略
- 通过在redis.conf中 设置 maxmemory 如 maxmemory 1024mb 进行设置Redis最大内存限制
- 通过expire命令的设置key的生存时间: expire key ttl(单位秒)
- 通过在redis.conf文件中可以配置key主动删除策略,默认是no-enviction(不删除):maxmemory-policy allkeys-lru
- volatile为前缀的策略都是从已经过期的数据集中进行淘汰。
- allkeys为前缀的策略都是面向所有key进行淘汰
- lru最近最少用到的
- lfu最不常用
- 触发条件都是Redis使用内存达到设置阈值时
名称 | 描述 |
---|---|
volatile-lru | 从已经设置过期时间的数据集中挑选最近最少使用的数据进行淘汰 |
volatile-lfu | 从已经设置过期时间的数据集中挑选最不经常使用的数据进行淘汰 |
volatile-ttl | 从已经设置过期时间的数据集中挑选将要过期的数据进行淘汰 |
volatile-random | 从已经设置过期时间的数据集中随机挑选任意数据的数据进行淘汰 |
allkeys-lru | 当内存不足新数据写入时淘汰最近最少使用的key |
allkeys-lfu | 当内存不足新数据写入时淘汰最不经常使用的key |
allkeys-random | 当内存不足新数据写入时随机淘汰任意数据的key |
allkeys-eviction | 当内存不足新数据写入时,写入报错,但是不会清楚任何数据 |
缓存淘汰策略选择:
- allkeys-lru : 在不确定时一般采用策略。 冷热数据交换
- volatile-lru : 比allkeys-lru性能差 存 : 过期时间
- allkeys-random : 希望请求符合平均分布(每个元素以相同的概率被访问)
- 自己控制:volatile-ttl 缓存穿透
缓存穿透
缓存穿透是指在高并发下查询Key不存在的数据,会穿透查询数据。导致数据压力过大而宕机。
解决方案:
- 1、对查询结果为空的情况也进行缓存。缓存时间(ttl)设置短一些,或者该key对应的数据insert了之后清理缓存。但是会带来一定的问题,缓存太多空值占用了更多的空间。
- 2、使用布隆过滤器BloomFilter。在缓存之前加一层布隆过滤器,在查询的时候先去布隆过滤器查询key 是否存在,如果不存在就直接返回,存在在查询缓存和DB.
Redis的BloomFilter 原理:
- 是由一个很长的二进制数组,初始数组每个值为0。
- 新增,通过hash算法,计算这个key对应的下标值设置为1。
- 查询时,通过hash算法,计算这个key对应数组下标的值,如果对应下标值为0,则这个key一定不存在,如果下标值为1则可能存在。
- 由于hash碰撞,有一定的误判率,一般通过计算多个函数值减少误判率。它的优点是空间效率和查询时间都远远超过一般的算法。
缓存雪崩
- 缓存雪崩:Redis服务器重启或者大量缓存集中在某一个时间段失效,会给DB造成很大压力。也就是说,突然间大量的key失效或者redis重启(redis重启加载持久化的数据也需要一定时间),这个时间大量请求访问,会造成DB瞬间的巨大的压力,数据库崩溃。
- 解决方案:1、key的失效期分散开 不同的key设置不同的有效期;2、设置二级缓存(数据不一定一致);3、高可用(脏读)
缓存击穿
- 缓存击穿是针对少量key,缓存雪崩是针对大量key。缓存击穿:对一些设置了过期时间的key,在某一段时间点被大量访问,恰好在这个时间点过期了,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决方案:- 1、用分布式锁控制访问的线程。使用redis的setnx互斥锁先进行判断,这样其他线程就处于等待状态,保证不会有大并发操作去数据库。
- 2、不设置超时时间,单会造成写一致问题。当数据库发生更新时,缓存中的数据不会及时更新,这样会造成数据库中的数据与缓存中的数据的不一致,樱花会从缓存中读取到脏数据。可以采用双删策略处理。
数据不一致问题
缓存和DB的数据不一致的根源:数据源不一样。
先更新数据库再更新缓存或者先更新缓存再更新数据库,本质上不是一个原子操作,高并发情况下会产生不一致
保证数据的最终一致性(延时双删)
- 1、先更新数据库同时删除缓存项(key),等读的时候在填充缓存
- 2、几秒后在删除一次缓存项(key)
- 3、设置缓存过期时间Expired Time比如10秒或者1分钟等
- 4、将缓存删除失败记录到日志中,利用脚本提取失败记录再次删除
- 5、通过数据库的binlog来异步淘汰key,利用工具(canal)将binlog日志采集发送到MQ中,然后通过ACK机制确认处理删除缓存。
数据并发竞争
Redis的多个client同时set 同一个key引起的并发问题。
- 同时并发写一个key,一个key的值是1,本来按顺序修改为2,3,4,最后是4,但是顺序变成了4,3,2,最后变成了2。
第一种方案:分布式锁+时间戳
- 分布式锁可以用setnx进行实现,加锁把并行读写改成串行读写的方式,从而来避免资源竞争。如系统AB需要进行set值
- 系统A key 1 {ValueA 8:00} 系统B key 1 { ValueB 8:05},假设系统B先抢到锁,将key1设置为{ValueB 8:05}。接下来系统A抢到锁,发现自己的key1的时间戳早 于缓存中的时间戳(8:00<8:05),那就不做set操作了.
第二种方案:利用消息队列
- 在并发量过大的情况下,可以通过MQ进行处理,把并行读写进行串行化,把Redis的set操作放在队列中使其串行化,必须的一个一个执行。
Hot Key
当有大量的请求(几十万)访问某个Redis某个key时,由于流量集中达到网络上限,从而导致这个redis的服务宕机。造成缓存击穿,接下来对这个key的访问会造成数据库崩溃,或者访问数据库回填Redis再访问Redis,继续崩溃。
如何发现热Key:
- 1、预估热key,如秒杀商品等;
- 2、在客户端进行统计;
- 3、如果是代理,可以在代理端统计;
- 4、基于实时计算进行发现
如何处理热key:
- 1、变分布式缓存为本地缓存,如Ehcache、Guava Cache;
- 2、在每个Redis主节点上备份热key数据,读取时采用随机读取,将访问压力分发到各个节点;
- 3、利用对热点数据访问进行限流熔断保护措施,限制每秒限制最多限制读多杀次[如4W次],一次超过就可以熔断,不让请求缓存集群,直接返回静态信息,用户稍后重试,进行保护缓存系统。
Big Key
大Key指的是存储的值(Value)非常大、常见场景:热门话题的讨论、大V的粉丝列表、序列化后的图片等
大key的影响:
- 1、大key会大量占用内存,在集群中无法均衡;
- 2、Redis的性能下降,主从复制异常;
- 3、在主动删除或过期删除时会操作时间过长而引起服务阻塞
如何发现大key:
- 1、使用Redis自带命令:redis-cli --bigkeys命令,当redis中key比较多时,执行慢。以找到某个实例5种数据类型(String、hash、list、set、zset)的最大key;
- 2、获取生产Redis的rdb文件,通过rdbtools分析rdb生成csv文件,再导入MySQL或其他数据库中进行分析统计,根据size_in_bytes统计bigkey
大key的处理:
- 1、减少String的字符串长度,list、hash、set、zset等的成员。string 类型的大key可以存放在其他的服务上如CDN。或者把大key存在在单独的Redis节点进行存储,和其他的key进行分开。
- 2、删除大key时不要使用del命令,del是阻塞命令、删除时会影响性能。使用lazy delete(unlink命令)
分布式锁
分布式锁特性
互斥性:任意时刻,只能有一个客户端获取锁,不能同时有两个客户端获取到锁。redis:setnx 如果如果不存在就不设置
同一性:锁只能被持有改锁的客户端删除,不能由其它客户端进行删除。redis:lua实现原子性
可重入性:持有某个锁的客户端可继续对该锁加锁,实现锁的续租
容错性:锁失效后(超过生命周期)自动释放锁(key失效),其他客户端可以继续获得该锁,防止死锁。redis:set key value NX PX
watch
利用Watch实现Redis乐观锁。乐观锁(CAS):比较并替换,是不具有互斥性,不会产生锁等待而消耗资源,但是需要反复的重试,但也是因为重试的机制,能比较快的响应。
// 模拟秒杀场景
public class TestWatch {
private static String rediskey = "lock_mx";
public static void main(String[] args) {
LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>(1100);
ThreadPoolExecutor executor = new ThreadPoolExecutor(20, 40, 10, TimeUnit.SECONDS, queue);
Jedis jedis = new Jedis("127.0.0.1", 6379);
// 初始化
jedis.set(rediskey, "0");
jedis.close();
for (int i = 0; i < 1000; i++) {
executor.execute(()->{
myWatch();
});
}
}
public static void myWatch() {
Jedis jedis = new Jedis("127.0.0.1", 6379);
try {
jedis.watch(rediskey);
String val = jedis.get(rediskey);
int num = Integer.parseInt(val);
String userInfo = UUID.randomUUID().toString();
if(num < 20) {
Transaction tr = jedis.multi();
tr.incr(rediskey);
List<Object> list = tr.exec();
if(list != null && list.size() > 0) {
// 秒成功 失败返回空list而不是空
System.out.println("用户:" + userInfo + ",秒杀成功!当前成功人数:" + (num + 1));
} else {
// 版本变化,被别人抢了
System.out.println("用户:" + userInfo + ",秒杀失败");
}
} else {
System.out.println("已经有20人秒杀成功,秒杀结束");
}
} catch (NumberFormatException e) {
e.printStackTrace();
}finally {
jedis.close();
}
}
}
Redisson分布式锁的使用
- Redisson是架设在Redis基础上的。Redisson在基于NIO的Netty框架上,可以生产环境使用分布式锁。Redisson使用的是lua脚本,保证原子性。Redisson有自动延时机制 只要客户端A一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端A还持有锁key,那么就会不断的延长锁key的生存时。
- 使用Redis分布式锁存在一个问题。如果在Master写了数据,客户端A加锁成功,Redis异步复制给slave过程中,Master节点挂了,数据还未完成复制,slave切换成Maser节点,此时其他客户端在来加锁也会加成功。
/**
* <p>RedissonClient 分布式锁实现</p>
* <p>使用单列模式</p>
*
*/
public class RedisLock {
private static final String LOCK_TITLE = "redisLock_";
private static Config config = new Config();
private static RedissonClient redisson = null;
// linux101 192.168.56.101
private RedisLock() {
config.useClusterServers().
// 集群状态扫描时间,单位毫秒
setScanInterval(2000)
// cluster方式至少6个节点(3主3从,3主做sharding,3从用来保证主宕机后可以高可用)
.addNodeAddress("redis://linux101:8001").addNodeAddress("redis://linux101:8002")
.addNodeAddress("redis://linux101:8003").addNodeAddress("redis://linux101:8004")
.addNodeAddress("redis://linux101:8005").addNodeAddress("redis://linux101:8006")
.addNodeAddress("redis://linux101:8007").addNodeAddress("redis://linux101:8008");
redisson = Redisson.create(config);
}
// 获取redisson对象的方法
public RedissonClient getRedisson() {
return redisson;
}
public static RedisLock getInstance() {
return Singleton.getInstance();
}
private static class Singleton {
private static RedisLock instance;
static {
instance = new RedisLock();
}
private static RedisLock getInstance() {
return instance;
}
}
// 锁的获取和释放
public boolean lock(String lockName) {
// 声明key对象
String key = LOCK_TITLE + lockName;
// 获取锁对象
RLock mylock = redisson.getLock(key);
// 加锁 并设置锁过期时间3秒 防止死锁的产生 uuid+threadId
// lock提供带timeout参数,timeout结束强制解锁,防止死锁
mylock.lock(3L, TimeUnit.SECONDS);
// tryLock,第一个参数是等待时间,5秒内获取不到锁,则直接返回。 第二个参数 60是60秒后强制释放
// mylock.tryLock(5L,60L, TimeUnit.SECONDS);
// 加锁成功
return true;
}
public void unlock(String lockName) {
// 必须是和加锁时的同一个key
String key = LOCK_TITLE + lockName;
// 获取所对象
RLock mylock = redisson.getLock(key);
// 释放锁(解锁)
mylock.unlock();
}
}