1 Redis介绍
Redis(Remote Dictionary Server)是一个基于内存的开源键值存储数据库,也被称为数据结构服务器。很像memcached,因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value 数据库。
Redis支持保存多种数据结构,此外单个value的最大限制是1GB。 Redis的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
上面提到了memcached,那我们将其和redis做个比较吧。
redis和memcached共同点:
都是基于内存的数据库,都有过期策略,两者性能都比较高
redis | memcached |
---|---|
支持多种数据类型 | 只支持key-value数据类型 |
支持原生集群模式 | 无原生集群模式 |
单线程多路IO复用模型 | 多线程 非阻塞IO复用模型 |
支持Lua脚本,事务等功能 | 不支持 |
过期数据删除策略使用 惰性和定期删除 | 过期数据删除策略只有惰性删除 |
上面提到了惰性删除和定期删除,这里进行对比
惰性删除: 只会在取出key时,对数据过期检查。 对CPU友好
定期删除: 每隔一段时间抽取一批Key执行删除过期key。 对内存更加友好
2 Redis 常见的数据结构以及使用场景
数据结构 | 使用场景 |
---|---|
String | 一般用作计数时,比如 用户访问次数 转发数等 |
list | 发布和订阅 消息队列 |
hash | 系统中对象数据存取 |
set | 存放的数据不重复 以及获取多个数据源 |
zset(sorted set) | 按照分数有序排列,对权重排序的场景 |
3 Redis一个字符串类型的值能存储最大容量是多少
答案是 512M
4 Redis 集群如何选择数据库
Redis 集群目前无法选择数据库,默认在 0 数据库。
5 Redis事务
Redis事务是一组原子性操作的集合,可以保证这组操作要么全部执行成功,要么全部不执行。在执行事务期间,Redis会将客户端发送的多个命令打包成一个事务,并按照顺序执行这些命令,期间不会被其他客户端的操作打断。
需要注意的是,Redis的事务并不是像关系型数据库中的事务那样支持回滚和锁定功能。Redis的事务机制更多地用于批量执行一系列命令,保证这些命令的原子性,但不提供隔离级别和回滚功能。 Redis不满足持久性
redis事务相关命令有 MULTI、EXEC、DISCARD、WATCH。
Redis事务通过MULTI命令开启事务,然后通过EXEC命令提交事务。
6 Redis持久化机制
《1》 快照持久化(RDB)
redis默认的持久化机制,通过创建快照来获得存储内存里的数据在某个时间点的副本
特点:
快速和高效:RDB持久化将Redis的内存数据保存到硬盘上的二进制文件。相比于AOF持久化方式,RDB持久化在恢复数据时速度更快。
适用于备份和恢复:由于RDB持久化生成的文件是完整的数据库快照,它非常适用于对Redis数据进行定期备份和恢复。
低写入延迟:由于RDB持久化是通过快照的方式进行的,它不需要每次写入操作都进行磁盘写入,因此具有较低的写入延迟。这对于要求高性能的应用场景非常有利。
《2》 只追加文件(AOF)持久化
redis默认未开启,开启AOF命令 appendonly yes
以日志形式记录每个更新操作,Redis重新启动时读取这个文件,重新执行新建、修改数据的命令恢复数据
特点:
比起RDB占用更多的磁盘空间
恢复备份速度要慢
每次读写都同步的话,有一定的性能压力
存在个别Bug,造成恢复不能。
如何选择合适的持久化方式呢?
如果对数据不敏感,可以选单独用RDB;不建议单独用AOF,因为可能出现Bug。如果只是做纯内存缓存,可以都不用。 AOF命令以redis协议追加保存每次写的操作到文件末尾。Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。
也可以同时开启两种持久化方式,redis4.0开始支持RDB和AOF混合持久化。在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。同时开启RDB和AOF,快速加载的同时避免丢失过多的数据,但是可读性下降了。
7 缓存穿透 && 缓存击穿 && 缓存雪崩
《1》 缓存穿透: 大量请求的key不存在于缓存,请求直接到了数据库,没有经过缓存。
解决1: 对于无效的key也进行缓存, 但是缓存太多空值,占用更多内存空间。于是给这些key设置过期时间进行解决。
解决2: 将数据库中所有的查询条件,放布隆过滤器中。当一个查询请求来临的时候,先经过布隆过滤器进行查,如果请求存在这个条件中,那么继续执行,如果不在,直接丢弃。
《2》 缓存雪崩: 缓存在同一时间大面积失效,请求全部落到了数据库,造成数据库短时间承受大量请求。
解决1: 如果redis服务不可用: 采用redis集群 限流,避免同时处理大量请求
解决2: 热点数据缓存失效: 对key设置不同的过期时间
《3》 缓存击穿: 过期的key,有可能被高并发访问 请求同时进来之前正好失效,对这个key的数据查询都落到数据库。
**解决:**在分布式的环境下,应使用分布式锁来解决,分布式锁的实现方案有多种,比如使用Redis的setnx、使用Zookeeper的临时顺序节点等来实现
这里顺便提一下 ZooKeeper分布式锁的实现原理
使用zookeeper创建临时序列节点来实现分布式锁,创建临时序列节点,找出最小的序列节点,获取分布式锁,程序执行完成之后此序列节点消失,通过watch来监控节点的变化,从剩下的节点的找到最小的序列节点,获取分布式锁,执行相应处理。
8 MySQL里有大量数据,如何保证Redis中的数据都是热点数据?(Redis内存淘汰策略)
redis内存数据大小上升到一定时,就会施行数据淘汰策略,保证redis的数据都是热点数据。
数据淘汰策略:
noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
allkeys是所有的key,包括未过期的和过期了的key。Volatile是过期了的key。
allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
allkeys-random: 回收随机的键使得新添加的数据有空间存放。
volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
9 Redis常见性能问题和解决方案
主从复制时候,Master机最好不要做任何持久化工作。
如果数据比较重要,主从复制时候,Slave机开启AOF备份数据,策略设置为每秒同步一次
为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内
尽量避免在压力很大的主库上增加从库。
主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3…
这样的结构方便解决单点故障问题,实现Slave对Master的替换。
10 redis是单线程的,为什么那么快
基于内存,非常快速。类似于HashMap,查找和操作的时间复杂度都是O(1)。
数据结构简单,对数据操作也简单。
采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。
使用多路I/O复用模型,非阻塞IO
使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
11 Redis在项目中的应用
作为缓存,将热点数据进行缓存,减少和数据库的交互,提高系统的效率。
作为分布式锁的解决方案,解决缓存击穿等问题。
作为消息队列,使用Redis的发布订阅功能进行消息的发布和订阅。
这里提一下,项目中Redis应用分布式锁的案例。
由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效。应用redis进行分布式锁,setnx:当key不存在的时候生效; 下面是一个案例
服务层写了一个接口,测试分布式锁。
redis数据库存入num,现在需要进行对num操作。 模拟5000次请求,对num进行加一操作
@Override
public void testLock() {
// 1. 从redis中获取锁,setnx
Boolean lock = this.redisTemplate.opsForValue().setIfAbsent("lock", "111");
if (lock) {
// 查询redis中的num值
String value = (String)this.redisTemplate.opsForValue().get("num");
// 没有该值return
if (StringUtils.isBlank(value)){
return ;
}
// 有值就转成成int
int num = Integer.parseInt(value);
// 把redis中的num值+1
this.redisTemplate.opsForValue().set("num", String.valueOf(++num));
// 2. 释放锁 del
this.redisTemplate.delete("lock");
} else {
// 3. 每隔1秒钟回调一次,再次尝试获取锁
try {
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
通过网关压力测试:出现问题,setnx刚好获取到锁,业务逻辑出现异常,导致锁无法释放。于是优化方案,设置过期时间,自动释放锁。
于是 使用命令Setex。
在set key时指定过期时间 用setex
==Setex key seconds value == eg: Sexex age 12 20 key是age 年龄20 过期时间12秒;
于是,将上述获取锁的代码改为
Boolean lock = this.redisTemplate.opsForValue().setIfAbsent("lock", "111",3,TimeUnit.SECONDS) ;
其他不变。再次测试,发现问题: 可能会释放其他服务器的锁 解决: setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁
于是使用UUID 防止误删锁。
@Override
public void testLock() {
// 1. 从redis中获取锁,setnx
// 设置UUID,
String uuid = UUID.randomUUID().toString();
Boolean lock = this.redisTemplate.opsForValue().setIfAbsent("lock", "111",3,TimeUnit.SECONDS);
if (lock) {
// 查询redis中的num值
String value = (String)this.redisTemplate.opsForValue().get("num");
// 没有该值return
if (StringUtils.isBlank(value)){
return ;
}
// 有值就转成成int
int num = Integer.parseInt(value);
// 把redis中的num值+1
this.redisTemplate.opsForValue().set("num", String.valueOf(++num));
// 2. 释放锁 del
if(uuid.equals((String)redisTemplate.opsForValue().get("lock"))){ // 判断是自己的锁
this.redisTemplate.delete("lock");
}
} else {
// 3. 每隔1秒钟回调一次,再次尝试获取锁
try {
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试,出现新的问题, 删除操作缺乏原子性。
再次进行优化: LUA脚本保证删除的原子性。
@Override
public void testLock() {
// 设置uuId
String uuid = UUID.randomUUID().toString();
// 缓存的lock 对应的值
Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock", uuid,1, TimeUnit.SECONDS);
// 判断flag
if (flag){
// 说明上锁成功! 执行业务逻辑
String value = redisTemplate.opsForValue().get("num");
// 判断
if(StringUtils.isEmpty(value)){
return;
}
// 进行数据转换
int num = Integer.parseInt(value);
// 放入缓存
redisTemplate.opsForValue().set("num",String.valueOf(++num));
// 定义一个lua 脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 准备执行lua 脚本
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
// 将lua脚本放入DefaultRedisScript 对象中
redisScript.setScriptText(script);
// 设置DefaultRedisScript 这个对象的泛型
redisScript.setResultType(Long.class);
// 执行删除
redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);
}else {
// 没有获取到锁!
try {
Thread.sleep(1000);
// 睡醒了之后,重试
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试,发现没有问题。但是,== redis+lua在集群的情况下不能保证删除的原子性==。因为,先前是单个微服务,删除保证了原子性。在集群下,就应该使用redlock;
在案例中,使用redisson 解决分布式锁
Redisson是一个在Redis的基础上实现的Java驻内存数据网格。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。
使用 Redisson,需要引入依赖
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.11.1version>
dependency>
改造上述案例的代码 如下:
@Override
public void TestRedisson() {
RLock lock = redissonClient.getLock("lock"); // 获取lock对象;
lock.lock(); // 上锁
// 业务代码;
try{
String value = (String)this.redisTemplate.opsForValue().get("num");
// 没有该值return
if (StringUtils.isBlank(value)){
return ;
}else {
// 有值就转成成int
int num = Integer.parseInt(value);
// 把redis中的num值+1 用set;
this.redisTemplate.opsForValue().set("num", String.valueOf(++num));
}
}finally {
lock.unlock(); //解锁;
}
}
测试,发现,问题解决。
今天先到这里吧,下期再见。