赞
踩
目录
Redis是一种支持key-value等多种数据结构的存储系统。可用于缓存,事件发布或订阅,高速队列等场景。支持网络,提供字符串,哈希,列表,队列,集合结构直接存取,基于内存,可持久化。
Redis:读的速度是110000次/s,写的速度是81000次/s
Redis支持Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行
Redis支持简易订阅通知(Pub/Sub) ,按key设置过期时间,过期后将会自动删除。
Redis支持RDB, AOF等持久化方式
单线程的原因:
Redis的瓶颈不是CPU,而是网络和内存。
多线程就会存在死锁、线程上下⽂切换等问题,甚⾄会影响性能。
单线程编程容易并且更容易维护。
Redis快的原因:
1、redis完全基于内存:绝大部分请求是纯粹的内存操作,非常快速。
2、数据结构简单:redis中的数据结构是专门进行设计的。
3、采用单线程模型,避免了不必要的上下文切换和竞争条件:也不存在多线程或者多线程切换而消耗CPU, 不用考虑各种锁的问题, 不存在加锁, 释放锁的操作, 没有因为可能出现死锁而导致性能消耗
4、使用了单线程的多路IO复用模型,一个线程通过select选择器维护多个客户端的io操作,底层采用Linux的epool实现事件驱动回调,避免空轮询的情况。
缓存是Redis最常见的应用场景,主要是因为Redis读写性能优异。而且逐渐有取代memcached,成为首选服务端缓存的组件。而且,Redis内部是支持事务的,在使用时候能有效保证数据的一致性。
redis中可以使用expire命令设置一个键的生存时间,到时间后redis会删除它。利用这一特性可以运用在限时的优惠活动信息、手机验证码等业务场景。
redis由于incrby命令可以实现原子性的递增,所以可以运用于高并发的秒杀活动、分布式序列号的生成、具体业务还体现在比如限制一个手机号发多少条短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次等等。
这个主要利用redis的setnx命令进行,setnx:"set if not exists"就是如果不存在则成功设置缓存同时返回1,否则返回0 。因为我们服务器是集群的,定时任务可能在两台机器上都会运行,所以在定时任务中首先 通过setnx设置一个lock,如果成功设置则执行,如果没有成功设置,则表明该定时任务已执行。 当然结合具体业务,我们可以给这个lock加一个过期时间,比如说30分钟执行一次的定时任务,那么这个过期时间设置为小于30分钟的一个时间就可以,这个与定时任务的周期以及定时任务执行消耗时间相关。
expire:设置key的过期时间;persist:设置key永久有效
底层数据结构:SDS(动态字符串):获取字符串长度的时间复杂的为O(1),最大容量512M。
应用场景:分布式锁、计数器(如访问次数、点赞转发数量)
常用方法:set 、get、strlen 、incr 、 decr 、setnx、setex、exists
底层数据结构:QuickList(快表)是双向链表的符合结构体
应用场景:消息队列 、文章列表
常用方法:lpush 、lrang 、rpush 、lpop 、rpop 、llen、lindex、rpoplpush、linsert
底层数据结构:哈希表、zipList(压缩列表):是双向列表,且内存地址是连续的新增或更新时可能会出现连锁的情况(连锁:如空间不足,需要对整个列表进行重新分配)
应用场景:缓存用户信息、购物车等。
常用方法:hset 、hget 、hkeys 、hvals 、hmset 、hmget 、hgetAll 、hlen 、hdel 、hsetnx 、hexists
底层数据结构:哈希表、IntSet(整数集)
应用场景:抽奖、共同关注、QQ内推等。
常用方法:sadd 、smembers 、sismember 、scard 、srem 、srandmember、spop、smove、sdiff、sinter、sunion
zset(有序集合):有序不重复的集合,和 set 相⽐增加了⼀个score权重参数,可以根据score进行排序,但score是可以重复的,value是不可以重复的。
底层数据结构:zipList(压缩列表)、skipList(跳表):在原有的有序列表中增加了多级索引,通过索引可以实现快速的查询,类似于二分查找,时间复杂度为O(logn),但索引会占用额外的储存空间
应用场景:排行榜、抖音热搜
常用方法:zadd 、zrange 、zrangebyscore(按score从小到大) 、zrem 、zcard 、zcount 、zrevrange(按score从大到小)
bitmap:更细化的一种操作,以bit为单位。
hyperloglog:基于概率的数据结构。 # 2.8.9新增
Geo:地理位置信息储存起来, 并对这些信息进行操作 # 3.2新增
流(Stream):# 5.0新增
是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。
触发方式:触发rdb持久化的方式有2种,分别是手动触发和自动触发。
手动触发:
自动触发:
save m n
,即在m秒内有n次修改时,自动触发bgsave生成rdb文件;优点:
只有一个文件 dump.rdb,方便持久化。
容灾性好,一个文件可以保存到安全的磁盘。
性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。
相对于数据集大时,比 AOF 的启动效率更高。
缺点:
数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候
将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。AOF日志是一种写后日志,“写后”的意思是Redis是先执行命令,把数据写入内存,然后才记录日志
为什么采用写后日志?
但这种方式存在潜在风险:
AOF的实现:
AOF日志记录Redis的每个写命令,步骤分为:命令追加、文件的写入和同步。
命令追加:当AOF持久化功能打开了,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器的 aof_buf 缓冲区。
文件写入和同步: 关于何时将 aof_buf 缓冲区的内容写入AOF文件中,Redis提供了三种写回策略:
AOF的重写 :
Redis通过创建一个新的AOF文件来替换现有的AOF,新旧两个AOF文件保存的数据相同,但新AOF文件没有了冗余命令。
AOF重写会阻塞吗?
AOF重写过程是由后台进程bgrewriteaof来完成的。主线程fork出后台的bgrewriteaof子进程,fork会把主线程的内存拷贝一份给bgrewriteaof子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。
所以aof在重写时,在fork进程时是会阻塞住主线程的。
AOF日志何时会重写?
有两个配置项控制AOF重写的触发:
auto-aof-rewrite-min-size:
表示运行AOF重写时文件的最小大小,默认为64MB。
auto-aof-rewrite-percentage:
这个值的计算方式是,当前aof文件大小和上一次重写后aof文件大小的差值,再除以上一次重写后aof文件大小。也就是当前aof文件比上一次重写后aof文件的增量大小,和上一次重写后aof文件大小的比值。
优点:
- 安全,所有写入的数据都不会丢失。
- AOF文件易读,可修改。
缺点:
AOF 文件比 RDB 文件大。
恢复速度慢。
性能消耗高。
定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。对内存很友好,但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
惰性删除:当key被访问时检查该key的过期时间,若已过期则删除;已过期未被访问的数据仍保持在内存中。可以最大化地节省CPU资源,却对内存非常不友好。
定期删除:每隔一段时间,会扫描一定数量设置了过期的key,并删除已过期的key。是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
默认的淘汰策略:no--enviction(永不回收)
LFU 算法的基本原理:
LFU 算法是根据数据访问的频率来选择被淘汰数据的,所以 LFU 算法会记录每个数据的访问次数。当一个数据被再次访问时,就会增加该数据的访问次数。访问频率小的优先被淘汰
不过,访问次数和访问频率还不能完全等同。访问频率是指在一定时间内的访问次数,也就是说,在计算访问频率时,我们不仅需要记录访问次数,还要记录这些访问是在多长时间内执行的。否则,如果只记录访问次数的话,就缺少了时间维度的信息,进而就无法按照频率来淘汰数据了。
我来给你举个例子,假设数据 A 在 15 分钟内访问了 15 次,数据 B 在 5 分钟内访问了 10 次。如果只是按访问次数来统计的话,数据 A 的访问次数大于数据 B,所以淘汰数据时会优先淘汰数据 B。不过,如果按照访问频率来统计的话,数据 A 的访问频率是 1 分钟访问 1 次,而数据 B 的访问频率是 1 分钟访问 2 次,所以按访问频率淘汰数据的话,数据 A 应该被淘汰掉。
所以说,当要实现 LFU 算法时,我们需要能统计到数据的访问频率,而不是简单地记录数据访问次数就行。
Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
开启:以MULTI开始一个事务
入队:将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面
执行:由EXEC命令触发事务
Redis官方文档给的理解是,Redis的事务是原子性的:所有的命令,要么全部执行,要么全部不执行。而不是完全成功。
redis事务可以保证命令失败的情况下得以回滚,数据能恢复到没有执行之前的样子,是保证一致性的,除非redis进程意外终结。
redis事务是严格遵守隔离性的,原因是redis是单进程单线程模式(v6.0之前),可以保证命令执行过程中不会被其他客户端命令打断。但是,Redis不像其它结构化数据库有隔离级别这种设计。
redis事务是不保证持久性的,这是因为redis持久化策略中不管是RDB还是AOF都是异步执行的,不保证持久性是出于对性能的考虑。
指要查找的数据既不在缓存当中,也不在数据库中,那么当短时间内有大量这样的请求到来时,会出现不停的访问数据库导致数据库的压力增大,缓存就没有存在的意义。
出现原因:
用户恶意攻击请求
误操作把Redis
和数据库里的数据删除
解决方案:
Redis
缓存中查询不到数据时,再从数据库查询,如果同样没有数据,就直接缓存一个空间使用N个哈希函数对所要标记的数据进行哈希值计算。
将计算到的哈希值对bit数组的长度取模,这样可以得到每个哈希值在bit数组的位置。
把bit数组中对应的位置标记为1。
在判断值是否存在时,就看对应算出的多个下标是否都为1(可能存在),或者任意一个为0(绝对不存在)
某个热点的数据过期,此时有大量的请求都去访问这个热点的数据,导致所有的请求都落在了数据库中,导致数据库的压力增大,进而影响性能
解决方案:
指大量的key在缓存中失效,请求全部落在了数据库中,导致数据库的压力增加;
原因及解决方案:
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。
作用:
缺点:当主服务器宕机后,需要手动把一台服务器切换为主服务器,需要人工干预,费时费力,还会造成一段时间内服务器不可用
同步方式:
1、全量复制:比如第一次同步时
2、增量复制:只会把主从库网络断连期间主库收到的命令,同步给从库
Redis 为什么主从全量复制使用RDB而不使用AOF?
RDB文件小,同步的速度较快;而AOF是执行所有的操作命令,会降低Redis的性能
哨兵是一个独立的进程,作为进程,他会独立运行,其原理是哨兵通过发送命令,等待redis服务器响应,从而监控运行的多个redis实例,当主节点发生故障时会自动选举并切换新的主节点
哨兵的作用:
方式一:可能会导致锁一直没有释放
- //加锁:如果键存在则不操作,否则操作
- Boolean setIfAbsent(K, V );
- setnx(K,V);
-
- //释放锁:
- Boolean delete(K);
- del(K);
方式二:可能出现上完锁后,服务器异常,不能设置过期时间(redis的命令不是原子操作)
- //加锁:如果键存在则不操作,否则操作
- Boolean setIfAbsent(K, V );
- setnx(K,V);
-
- //设置过期时间
- Boolean expire(K key, final long timeout, final TimeUnit unit)
- expire(K,Time);//单位:毫秒
-
- //释放锁:
- Boolean delete(K);
- del(K);
方式四:上锁同时设置过期时间;可能会发生锁误删除。
- //加锁并设置过期时间
- Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit);
- setex(K,Time,V);//单位:毫秒
-
- //释放锁:
- Boolean delete(K);
- del(K);
方式五:上锁同时设置过期时间;将值设置为uuid,在进行释放锁时,先判断当前的uuid和要释放锁的uuid,若一致进行释放,否则不释放。
- //1.进入方案
- String requestId = IdWorker.getIdStr();
- Boolean ret = redisTemplate.opsForValue().setIfAbsent( "key",requestId,5L,TimeUnit.SECONDS);
- //2.判断为true 表示加锁成功,false标识锁已经存在
- if(ret){
- //3.执行业务
-
- //4.执行结束
- Object o = redisTemplate.opsForValue().get(key);
- if(o !=null){
- if(requestId.equals((String)o)){
- redisTemplate.delete(key);
- }
- }
- }else{
- //5.做异常容错处理
- Long expire = redisTemplate.opsForValue().getOperations().getExpire(key);
- if(expire !=null && expire.intValue()<0){
- redisTemplate.delete(key);
- }
- //6.日志打印该模块未执行任务!
- log.info("模块:"+MODULE+",定时任务已在执行!");
- }
先更新数据库,在删除缓存
为什么是删除缓存,而不是更新缓存?
原因很简单,很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值。比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。
另外更新缓存的代价有时候是很高的
先修改数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。
使用消息队列订阅MySQL的binlog变更日志文件来做重试
1、开启Redis的过期回调功能。
notify-keyspace-events Ex
2、编写一个配置类
- @Configuration
- public class RedisListenerConfig {
-
- @Bean
- RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
-
- RedisMessageListenerContainer container = new RedisMessageListenerContainer();
- container.setConnectionFactory(connectionFactory);
- return container;
- }
- }
-
3、编写具体的监听代码:
- /**
- * Redis事件监听器
- */
- @Component
- public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
-
-
- public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
- super(listenerContainer);
- }
-
- /**
- * 重写omMessage方法,当Redis中的Key过期时会执行该方法
- */
- @Override
- public void onMessage(Message message, byte[] pattern) {
- // 过期的key
- String expireKey = message.toString();
- // 根据key,执行自己需要实现的功能。
- }
- }
-
注意:当redis的数据被淘汰策略淘汰时,尽管时间没有过期,但是也会触发事件。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。