一、什么是Redis
Redis (REmote DIctionary Server) 是用 C 语言开发的一个开源的高性能键值对(key-value)数据库。
特征:
- 数据间没有必然的关联关系
- 内部采用单线程机制进行工作
- 高性能,官方提供测试数据,50个并发执行100000 个请求,读的速度是110000 次/s,写的速度是 81000次/s。
- 持久化支持。可以进行数据灾难恢复
- 多数据类型支持
数据类型:
- 字符串类型 string
- 列表类型 list
-
散列类型 hash
-
集合类型 set
-
有序集合类型 sorted_set
二、Redis 数据类型
redis 自身是一个 Map,其中所有的数据都是采用 key : value 的形式存储
数据类型指的是存储的数据的类型,也就是 value 部分的类型,key 部分永远都是字符串 key的语法:
- 在一个项目中,key最好使用统一的命名模式
- key区分大小写
- key不要太长,尽量不要超过1024字节。不仅消耗内存,也会降低查找的效率
- key不要太短,太短可读性会降低
String
- 存储的数据:单个数据,最简单的数据存储类型,也是最常用的数据存储类型
- 存储数据的格式:一个存储空间保存一个数据
- 存储内容:通常使用字符串,如果字符串以整数的形式展示,可以作为数字操作使用
hash
- 新的存储需求:对一系列存储的数据进行编组,方便管理,典型应用存储对象信息
- 需要的存储结构:一个存储空间保存多个键值对数据
- hash类型:底层使用哈希表结构实现数据存储
hash存储结构优化:
- 如果field数量较少,存储结构优化为类数组结构
- 如果field数量较多,存储结构使用HashMap结构
List
- 数据存储需求:存储多个数据,并对数据进入存储空间的顺序进行区分
- 需要的存储结构:一个存储空间保存多个数据,且通过数据可以体现进入顺序
- list类型:保存多个数据,底层使用双向链表存储结构实现
Set
- 新的存储需求:存储大量的数据,在查询方面提供更高的效率
- 需要的存储结构:能够保存大量的数据,高效的内部存储机制,便于查询
- set类型:与hash存储结构完全相同,仅存储键,不存储值(nil),并且值是不允许重复的
sorted_set
- 新的存储需求:数据排序有利于数据的有效展示,需要提供一种可以根据自身特征进行排序的方式
- 需要的存储结构:新的存储模型,可以保存可排序的数据
- sorted_set类型:在set的存储结构基础上添加可排序字段
三、事务
Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中, 不会被其他客户端发送来的命令请求所打断。Redis事务的主要作用就是串联多个命令防止别的命令插队。
特点:
- Redis事务没有没有隔离级别的概念
- 所有的命令在事务中,并没有直接被执行,只有发起执行命令的时候才会执行!Exec
- Redis单条命令式保存原子性的,但是事务不保证原子性!
常用命令:
命令 | 描述 |
multi | 标记一个事务的开始 |
exec | 执行所有事务块内的命令 |
discard | 取消事务,放弃执行事务块内的所有命令 |
watch key | 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动, 那么事务将被打断。类似乐观锁 |
unwatch | 取消watch命令对所有 key 的监视。 |
四、redis持久化
Redis 是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以 Redis 提供了持久化功能!
RDB
在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。
RDB手动
save指令
手动执行一次保存操作
bgsave指令
bgsave命令是针对save阻塞问题做的优化。Redis内部所有涉及到RDB操作都采用bgsave的方 式,save命令可以放弃使用。
RDB自动
save second changes
满足限定时间范围内key的变化数量达到指定数量即进行持久化
优点
- RDB是一个紧凑压缩的二进制文件,存储效率较高
- RDB内部存储的是redis在某个时间点的数据快照,非常适合用于数据备份,全量复制等场景
- RDB恢复数据的速度要比AOF快很多
- RDB节省磁盘空间
缺点
- Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
- 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能
- RDB方式无论是执行指令还是利用配置,无法做到实时持久化,具有较大的可能性丢失数据
- Redis的众多版本中未进行RDB文件格式的版本统一,有可能出现各版本服务之间数据格式无法兼容现象
AOF
AOF(append only file)持久化:以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中命令 达到恢复数据的目的;与RDB相比可以简单描述为改记录数据为记录数据产生的过程AOF的主要作用是 解决了数据持久化的实时性,目前已经是Redis持久化的主流方式。
AOF写数据三种策略
-
always(每次)
- everysec(每秒)
- no(系统控制)
AOF重写
随着命令不断写入AOF,文件会越来越大,为了解决这个问题,Redis引入了AOF重写机制压缩文件体积。AOF文件重写是将Redis进程内的数据转化为写命令同步到新AOF文件的过程。简单说就是将对同 一个数据的若干个条命令执行结果转化成最终结果数据对应的指令进行记录。
重写作用
- 降低磁盘占用量,提高磁盘利用率
- 提高持久化效率,降低持久化写时间,提高IO性能
- 降低数据恢复用时,提高数据恢复效率
区别
持久化方式 | RDB | AOF |
占用存储空间 | 小(数据级:压缩) | 大(指令级:重写) |
存储速度 | 慢 | 快 |
恢复速度 | 快 | 慢 |
数据安全性 | 会丢失数据 | 依据策略决定 |
资源消耗 | 高/重量级 | 低/轻量级 |
启动优先级 | 低 | 高 |
官方推荐两个都启用,如果对数据不敏感,可以选单独用RDB,不建议单独用 AOF,因为可能会出现 Bug ,如果只是做纯内存缓存,可以都不用。
五、Redis 删除策略
过期数据
Redis是一种内存级数据库,所有数据均存放在内存中,内存中的数据可以通过TTL指令获取其状态。
XX :具有时效性的数据
-1 :永久有效的数据
-2 :已经过期的数据或被删除的数据或未定义的数据
数据删除策略
在内存占用与CPU占用之间寻找一种平衡,顾此失彼都会造成整体redis性能的下降,甚至引发服务器宕机或内存泄露。
定时删除
- 创建一个定时器,当key设置有过期时间,且过期时间到达时,由定时器任务立即执行对键的删除 操作
- 优点:节约内存,到时就删除,快速释放掉不必要的内存占用
- 缺点:CPU压力很大,无论CPU此时负载量多高,均占用CPU,会影响redis服务器响应时间和指 令吞吐量
- 总结:用处理器性能换取存储空间(拿时间换空间)
惰性删除
- 数据到达过期时间,不做处理。等下次访问该数据时如果未过期,返回数据,发现已过期,删除,返回不存在
- 优点:节约CPU性能,发现必须删除的时候才删除
- 缺点:内存压力很大,出现长期占用内存的数据
- 总结:用存储空间换取处理器性能(拿空间换时间)
定期删除
周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删 除频度
- 如果key超时,删除key
- 如果一轮中删除的key的数量>W * 25%,循环该过程
- *如果一轮中删除的key的数量≤W * 25%,检查下一个expires[*],0-15循环
- W取值=ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP属性值
- 优点1:CPU性能占用设置有峰值,检测频度可自定义设置
- 优点2:内存压力不是很大,长期占用内存的冷数据会被持续清理
- 总结:周期性抽查存储空间 (随机抽查,重点抽查)
删除策略比对
- 定时删除 节约内存,无占用 不分时段占用CPU资源,频度高 拿时间换空间
- 惰性删除 内存占用严重 延时执行,CPU利用率高 拿空间换时间
- 定期删除 内存定期随机清理 每秒花费固定的CPU资源维护内存 随机抽查,重点抽查
六、逐出算法
Redis使用内存存储数据,在执行每一个命令前,会调用freeMemoryIfNeeded()检测内存是否充 足。如果内存不满足新加入数据的最低存储要求,redis要临时删除一些数据为当前指令清理存储 空间。清理数据的策略称为逐出算法。
注意:逐出数据的过程不是100%能够清理出足够的可使用的内存空间,如果不成功则反复执行。 当对所有数据尝试完毕后,如果不能达到内存清理的要求,将出现错误信息。
检测易失数据(可能会过期的数据集server.db[i].expires )
① volatile-lru:挑选最近最少使用的数据淘汰
② volatile-lfu:挑选最近使用次数最少的数据淘汰
③ volatile-ttl:挑选将要过期的数据淘汰
④ volatile-random:任意选择数据淘汰
检测全库数据(所有数据集server.db[i].dict )
⑤ allkeys-lru:挑选最近最少使用的数据淘汰
⑥ allkeys-lfu:挑选最近使用次数最少的数据淘汰
⑦ allkeys-random:任意选择数据淘汰
放弃数据驱逐
⑧ no-enviction(驱逐):禁止驱逐数据(redis4.0中默认策略),会引发错误OOM(Out Of Memory)达到最大内存后的,对被挑选出来的数据进行删除的策略
七、企业级解决方案
缓存预热
缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查 询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来 巨大压力。
解决方案:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无 数的请求访问会在瞬间给数据库带来巨大的冲击。
解决方案:
- 互斥锁:由于保证了互斥性,所以数据一致,且实现简单,因为仅仅只需要加一把锁而已,也没其 他的事情需要操心,所以没有额外的内存消耗,缺点在于有锁就有死锁问题的发生,且只能串行执行性 能肯定受到影响
- 逻辑过期:线程读取过程中不需要等待,性能好,有一个额外的线程持有锁去进行重构数据,但是 在重构数据完成前,其他的线程只能返回之前的数据,且实现起来麻烦
缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这 些请求都会打到数据库。
解决方案:
缓存空对象
当我们客户端访问不存在的数据时,先请求redis,但是此时redis中没有数据, 此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库,我们都知道数据 库能够承载的并发不如redis这么高,如果大量的请求同时过来访问这种不存在的数据,这些请求就都会 访问到数据库,简单的解决方案就是哪怕这个数据在数据库中也不存在,我们也把这个数据存入到redis 中去,这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据就不会进入到缓存了。
- 优点:实现简单,维护方便
- 缺点: 额外的内存消耗可能造成短期的不一致
布隆过滤
布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问 redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中, 假设布隆过滤器判断这个数据不存在,则直接返回这种方式优点在于节约内存空间,存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲突。
- 优点:内存占用较少,没有多余key
- 缺点: 实现复杂 存在误判可能
八、Jedis
Jedis是一个用Java语言编写的Redis客户端,它实现了Redis的所有命令,并支持连接池、分片等高级特性。通过Jedis,Java开发人员可以方便地与Redis进行交互,实现高效的数据存储和读取操作。
<!-- redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- yml文件 -->
spring:
redis.host=localhost
port=6379
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<Object, Object> jsonRedisTemplate(
RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
//1.创建自定义模板类
RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
//配置json类型的序列化工具
template.setKeySerializer(new StringRedisSerializer());//这样key会用字符串方式保存
template.setDefaultSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootRedis01ApplicationTests {
//专用对象
@Autowired
public RedisTemplate redisTemplate;
//专用字符串key value均是String
@Autowired
public StringRedisTemplate stringRedisTemplate;
//自定义
@Autowired
public RedisTemplate jsonRedisTemplate;
/**
* 测试stringRedisTemplate+
* */
@Test
public void stringRedisTemplate(){
//1.key相关
Set<String> keys = stringRedisTemplate.keys("*");
for (String key : keys) {
System.out.println(key);
}
//2.各种类型支持
stringRedisTemplate.opsForValue();
stringRedisTemplate.opsForList();//List
stringRedisTemplate.opsForHash();//hash
stringRedisTemplate.opsForSet();//set
stringRedisTemplate.opsForZSet();//zset
//3.举例字符串
stringRedisTemplate.opsForValue().set("name","许老师");
String name = stringRedisTemplate.opsForValue().get("name");
System.out.println(name);
//4.操作list列表
stringRedisTemplate.opsForList().leftPush("myList1","曹丕");
stringRedisTemplate.opsForList().leftPush("myList1","曹植");
stringRedisTemplate.opsForList().leftPushAll("mylistAll","曹操","曹丕","曹植");
List<String> list = stringRedisTemplate.opsForList().range("mylistAll",0,2);
System.out.println(list);
}
/**
* 测试RedisTemplate
* 法意:
* 1.测试RedisTemplate与stringRedisTemplate存的数据相互独立
* 2.redisTemplate默认使用key序列化方式和value的序列化方式都使用的是jdk serializer序列化
* 所以存对象会乱码
*
* */
@Test
public void show2(){
ValueOperations valueOperations = redisTemplate.opsForValue();
valueOperations.set("name","薛老师");
String name = (String) valueOperations.get("name");
System.out.println(name);
redisTemplate.opsForValue();//String
redisTemplate.opsForList();//List
redisTemplate.opsForHash();//hash
redisTemplate.opsForSet();//set
redisTemplate.opsForZSet();//zset
Student stu1 = new Student(1,"薛老师","读书");
redisTemplate.opsForValue().set("stu1",stu1);
Student ss1 = (Student)redisTemplate.opsForValue().get("stu1");
System.out.println(ss1);
redisTemplate.opsForList().leftPushAll("mylist","睡觉","游戏");
List<String> list = redisTemplate.opsForList().range("mylist",0,-1);
for (int i = 0; i < list.size(); i++) {
String s = list.get(i);
System.out.println(s);
}
System.out.println("打印默认序列策略"+redisTemplate.getDefaultSerializer());
}
/**
* 测试自定义
* 法意:定义配置类JavaConfig,自定义序列化策略
*
* */
@Test
public void show3(){
//保存对象
Student stu = new Student(1,"彭老师","拉面");
jsonRedisTemplate.opsForValue().set("stu",stu);
//获取对象
Object s1 = jsonRedisTemplate.opsForValue().get("stu");
String jsonS1 = JSONObject.toJSONString(s1);
Student s11 = JSONObject.parseObject(jsonS1,Student.class);
System.out.println(s11);
Student stu2 = new Student(2,"彭老师","拉面");
Student stu1 = new Student(2,"彭老师","拉面");
Student stu3 = new Student(2,"彭老师","拉面");
List<Student> students = Arrays.asList(stu1, stu2, stu3);
jsonRedisTemplate.opsForValue().set("stus",students);
//必须Object接受,利用ObjectMapper对象转换,如果强制转换会报错
Object data = jsonRedisTemplate.opsForValue().get("stus");
String dataJson = JSONObject.toJSONString(data);
//将JSON类型转为List
List<Student> stus = JSONObject.parseArray(dataJson, Student.class);
System.out.println(stus);
}
}