复习整理————redis

复习整理——redis

redis

redis为什么这么快

1、完全基于内存,绝大部分请求是纯粹的内存操作。

2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;

3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

4、使用多路I/O复用模型非阻塞IO

5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制
(**原因:**1.redis对象远小于一个page(4k),所以不能用OS的内存交换,并且list类型,set类型可能位于多个page上,不利于换出。2.redis可以将对象压缩后再IO操作 3.OS交换会阻塞线程,redis可以设置让工作线程完成,主线程可以继续接受请求 )
因为用一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。

多路 I/O 复用模型

详细细节见我另一篇博客。

多路I/O复用模型是利用 select、poll、epoll等系统调用函数。可以同时监控多个描述符的读写就绪情况。

可以同时监察多个流的 I/O事件的能力。
在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。

这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让 单个线程高效的处理多个连接请求(只处理真正事件的流,尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量。

redis为什么要使用单线程(指的是处理网络请求的线程)

官方QA:
Redis是基于内存的操作,瓶颈不是CPU,最有可能是机器内存的大小或者网络带宽
既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)。
多线程还要考虑原子性,考虑同步,考虑线程间的配合。单线程已经这么快,就没必要了。多核CPU 的问题,可以通过在单机开多个Redis 实例来完善!

redis有哪些常用的数据结构

String、List、Set、Hash、ZSet这5种。有序集合(Sorted Set或者是ZSet)与范围查询,Bitmaps,Hyperloglogs 和地理空间(Geospatial)

设计一个缓存,你该怎么设计,get和set的时间复杂度怎么算的(答了用LinkedHashMap实现,分析了一下LinkedHashMap但是也没怎么说清)

Redis有哪些方法

Redis事务

使用:
MULTI 开始一个事务, 然后将多个命令入队到事务中,中途可以使用DISCARD放弃事务,表示不玩了最后由 EXEC 命令触发事务,一并执行事务中的所有命令。

特性:
事务中有语句执行错误了,不会影响上下文的执行,不回滚。
MYSQL的原子性:中途执行失败,回滚保证原子性。
redis的原子性:中途执行失败,不管,继续执行完所有的命令。保证原子性。
一致性:添加了错误命令到事务队列中,事务会被拒绝执行。执行事务时停机,会根据AOF文件恢复。
隔离性:事务之间不会相互影响,是因为redis是单线程的方式执行事务,并且执行事务中不会中断,事务总是以串行的方式运行。

持久性:
因为redis事务不过是简单的用队列包裹起来一组redis命令,redis并没有为事务提供任何额外的持久化功能,所以redis事务的耐久性由redis使用的模式决定。

redis的持久化模式

通过fork子进程来协助完成持久化

  • 无持久化:事务不具有耐久性,一旦服务器停机,包括事务数据在内的所有服务器数据都将丢失
  • RDB持久化模式(bgsave做镜像全量持久化)特点:耗时长,需要配合aof。原理:fock和cow,fock创建子进程,cow用来copy on write 服务器只会在特定的保存条件(定时或者定点保存)满足的时候才会执行BGSAVE命令,对数据库进行保存操作,并且异步执行的BGSAVE 不能保证事务数据被第一时间保存到硬盘里面,因此RDB持久化模式下的事务也不具有耐久性 ( 相比于AOF机制,如果数据集很大,RDB的启动效率会更高。)
  • AOF持久化模式,appedfsync的选项的值为always:程序总会在执行命令之后调用同步函数,将命令数据真正的保存到硬盘里面,因此
    这种配置下的事务是具有耐久性的。 (每修改同步,消耗性能)
  • AOF持久化模式,并且appedfsync的选项的值为everysec:程序会每秒同步一次命令数据到磁盘因为停机可能会恰好发生在等待同步的那一秒内,这种可能造成事务数据丢失,所以这种配置下的事务不具有耐久性

作用:

  • 批量操作 在发送 EXEC 命令前被放入队列缓存。
  • 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。(主要作用)

Redis和Kafka区别

  • kafka用于对于一些常规的消息系统,
  • “网站活性跟踪”将网页/用户操作等信息发送到kafka中.并实时监控,或者离线统计分析等
  • kafka的特性决定它非常适合作为”日志收集中心”

redis事务锁 (WATCH,监视数据)

场景:你想防止某个字段不被修改。WATCH “name”。然后开启事务,若修改了name,在执行EXCT的时候,会拒绝事务操作,返回空值。

WATCH是一个乐观锁,可以在EXEC命令执行之前,监视任意数量的数据库键,并且在执行时,检查被监视的键是否有至少一个被修改了,是的话就拒绝执行事务。(保护 数据库键,拒绝修改,拒绝事务)
此处输入图片的描述

  • 如果name被修改,那么所有监视键name的客户端的REDIS_DIRTY_CAS标识将会被打开,表示该客户的事务安全性已经被 表示事务已经不再安全。客户端会拒绝事务的执行。

Redis的底层的单线程模型和持久化的方式,再深入一点自己模拟实现一个简单的Redis可以吗(LRU算法,再自己想想时间复杂度)

主从复制 分布式

为什么要主从复制

原则: Master会将数据同步到slave,而slave不会将数据同步到master。Slave启动时会连接master来同步数据。

这是一个典型的分布式读写分离模型。
利用master来插入数据
slave提供检索服务。这样可以有效减少单个机器的并发访问数量

通过slaveof命令复制,主→从。从服务器上:SLAVEOF 127.0.0.1 6379
旧版主从复制
1.同步:将从服务器的数据库状态更新至主服务器当前所处状态
1)发送SYNC命令
2)主服务器执行BGSAVE命令,在后头生成RDB文件,并使用一个缓冲区几率从现在驾驶执行的所有写命令
3)完成BGSAVE后,将RDB发送给从服务器,从服务器更新。
4)缓冲区写命令发送给从服务器,更新。
此处输入图片的描述
2.命令传播:同步之后可能会因请求又导致主从不同,故主服务器会将自己执行的命令,同时发给从服务器执行。
旧版缺陷:当主从服务器断线,从服务器需要重新SYNC,很消耗性能。

新版主从复制: 2.8版本之后,使用PSYNC替代SYNC
PSYNC命令具有完整重同步部分重同步两种模式:
完整重同步:处理初次复制情况,和SYNC基本相同
部分重同步:处理断线问题,当断线重连后,不是执行同步,而主服务器将断线期间命令发送给从服务器。通过复制偏移量、复制积压缓冲区、服务器运行ID实现。
命令传播程序在发送写命令给从服务器时,也会备份一份到积压缓冲区队列。
当重连时,根据从服务器提供的复制偏移量来选择执行何种操作。如果偏移量存在于缓存区,那么则部分重同步,如果偏移量之后不存在缓冲区,则执行完整重同步。
此处输入图片的描述

读写分离模型

通过增加Slave DB的数量,读的性能可以线性增长。为了避免Master DB的单点故障,集群一般都会采用两台Master DB做双机热备,所以整个集群的读和写的可用性都非常高。

缺陷:不管是Master还是Slave,每个节点都必须保存完整的数据,如果在数据量很大的情况下,集群的扩展能力还是受限于单个节点的存储能力,而且对于Write-intensive类型的应用,读写分离架构并不适合。

数据分片模型

将每个节点看成都是独立的master,然后通过业务实现数据分片。
结合上面两种模型,可以将每个master设计成由一个master和多个slave组成的模型。

Redis哨兵:高可用性解决方案

由一个或多个哨兵实例组成的系统 监视任意多个主服务器。
当主服务器下线时,自动将被监视服务器 下属 某个服务器(从服务器)升级为新主服务器,代替下线服务器处理请求。
此处输入图片的描述
当server1的下线时长超过用户设定的下线时长上限,哨兵系统就会对其进行故障转移操作
1.挑选一个从服务器成为主服务器
2.Sentinel系统发送指令让其他从服务器成为新主服务器的从服务器
3.Sentinel系统继续监控下限的server1,重新上线后让其成为新主服务器的从服务器。
此处输入图片的描述

Sentinel本质上只是一个运行在特殊模式下的Redis服务器,只是初始化过程不同,执行其专用代码。
1.获取主服务器信息,哨兵每十秒一次,以及其从服务器信息
2.当发现主服务器有新从服务器出现时,也会向从服务器创建订阅及命令连接。
3.发送以及接收主从服务器消息。
4.监测主服务器主观下线状态(从主服务器获取),检查主服务器客观下线状态(从其从服务获取是否真的下线)
5.选取领头Sentinel去解决下线服务器故障转移:都有可能,先到先得。

redis集群

作用:提供的分布式数据库方案,通过分片来进行数据共享,提供复制和故障转移操作。

开始的节点是分散在各自独立集群。向节点发送CLUSTER MEET,让节点进行握手,形成集群。
1.启动节点
此处输入图片的描述
节点可以使用所有单机模式组件。

优势:自动分割数据到不同的节点上。
整个集群的部分节点失败或者不可达的情况下能够继续处理命令。

分布式锁

线程锁(JVM):主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。
进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。
分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。

分布式锁一般有三种实现方式:
- 数据库乐观锁;
- 基于Redis的分布式锁;
- 基于ZooKeeper的分布式锁。

redis分布锁

特性:
- 1.互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 具有容错性。 只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

代码实现

public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:
锁,钥匙,存在则不操作,设置过期时间,时间长度

  1. key:我们使用key来当锁,因为key是唯一的。
  2. value:我们传的是requestId,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。(也就是requestId是加锁客户端独有的)
  3. nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
  4. expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。(我们不允许有永久的锁)
  5. time,与第四个参数相呼应,代表key的过期时间。

总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。

满足了上述的所有特性,不会发生死锁

解锁代码

public class RedisTool {

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }
}

第一行代码,我们写了一个简单的Lua脚本代码(能够在服务器端 原子地执行多个Redis命令)详细介绍见《redis原理和实战》一书第20章。

第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS1赋值为lockKey,ARGV1赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。
Lua语言能确保上述操作是原子性的。

关于非原子性会带来什么问题,一个场景举例:
在判断判断锁和解锁中间,插入了锁过期和其他设置锁。
问题在于如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。
比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。

一系列问题

缓存雪崩

解释:由于原有缓存失效,新缓存未到期。这俩时间段之间,大量访问数据库。
造成原因:设置缓存时采用了相同的过期时间,某一时间点大量缓存过期。
解决方案:

  1. 分析用户行为,尽量让失效时间点均匀分布。避免缓存雪崩的出现。
  2. 并发量小时,加锁排队,治标不治本的方法
  3. 并发量大时,为缓存加标记记录缓存是否失效,失效则重新更新数据。
  4. 二级缓存:做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期

缓存穿透

造成原因:狂TM查询缓存和数据库都没有的东西。穿透攻击。缓存命中率极低。
解决方案:

  1. 布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。(多个has算法,验证所有bitmap对应位置上是不是都是1,都是的话才存在,会误伤正确访问,但是绝不会通过无效访问)节省空间。

  2. 快速过期的空值也放到缓存中!如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓存中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴!

缓存预热(开机或者挂了重启加缓存)

系统上线后,提前将相关的缓存数据直接加载到缓存系统。
解决方案:

  1. 直接写个缓存刷新页面,上线时手工添加缓存
  2. 定时刷新缓存

缓存更新

除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
(1)定时去清理过期的缓存;(适合少量缓存
(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。

两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!

缓存降级

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值