Redis相关

0 跳跃表

Redis的基础数据类型之一zset,内部的排序功能就是基于跳跃表来实现,跳跃表图示:

  • 性能:与二分查找类似,跳跃表能够在 O(㏒n)的时间复杂度之下完成查找,与红黑树等数据结构查找的时间复杂度相同,但是相比之下,跳跃表能够更好的支持并发操作,而且实现这样的结构比红黑树等数据结构要简单、直观许多。
  • 跳跃表体现了空间换时间的思想:跳跃表以有序的方式在层次化的链表中保存元素,效率和平衡树媲美:查找、删除、添加等操作都可以在对数期望时间下完成。,
  • 本质:跳跃表是在单链表的基础上在选取部分结点添加索引,这些索引在逻辑关系上构成了一个新的线性表,并且索引的层数可以叠加,生成二级索引、三级索引、多级索引,以实现对结点的跳跃查找的功能。
     

1 BitMap

Bit-map的基本思想就是用一个bit位来标记某个元素对应的Value,而Key即是该元素。由于采用了Bit为单位来存储数据,因此在存储空间方面,可以大大节省。(PS:划重点 节省存储空间)。假设有这样一个需求:在20亿个随机整数中找出某个数m是否存在其中,并假设32位操作系统,4G内存。在Java中,int占4字节,1字节=8位(1 byte = 8 bit)如果每个数字用int存储,那就是20亿个int,因而占用的空间约为 
(2000000000*4/1024/1024/1024)≈7.45G

如果按位存储就不一样了,20亿个数就是20亿位,占用空间约为  (2000000000/8/1024/1024/1024)≈0.233G

高下立判,无需多言。那么,问题来了,如何表示一个数呢?刚才说了,每一位表示一个数,0表示不存在,1表示存在,这正符合二进制,这样我们可以很容易表示{1,2,4,6}这几个数:

计算机内存分配的最小单位是字节,也就是8位,那如果要表示{12,13,15}怎么办呢?

当然是在另一个8位上表示了:

 这样的话,好像变成一个二维数组了

1个int占32位,那么我们只需要申请一个int数组长度为 int tmp[1+N/32] 即可存储,其中N表示要存储的这些数中的最大值,于是乎:

tmp[0]:可以表示0~31

tmp[1]:可以表示32~63

tmp[2]:可以表示64~95

。。。

如此一来,给定任意整数M,那么M/32就得到下标,M%32就知道它在此下标的哪个位置

添加 

Bitmap有什么用

大量数据的快速排序、查找、去重

快速排序

假设我们要对0-7内的5个元素(4,7,2,5,3)排序(这里假设这些元素没有重复),我们就可以采用Bit-map的方法来达到排序的目的。

要表示8个数,我们就只需要8个Bit(1Bytes),首先我们开辟1Byte的空间,将这些空间的所有Bit位都置为0,然后将对应位置为1。

最后,遍历一遍Bit区域,将该位是一的位的编号输出(2,3,4,5,7),这样就达到了排序的目的,时间复杂度O(n)。

优点:

  • 运算效率高,不需要进行比较和移位;
  • 占用内存少,比如N=10000000;只需占用内存为N/8=1250000Byte=1.25M

缺点:

  • 所有的数据不能重复。即不可对重复的数据进行排序和查找。
  • 只有当数据比较密集时才有优势

快速去重

20亿个整数中找出不重复的整数的个数,内存不足以容纳这20亿个整数。 

首先,根据“内存空间不足以容纳这05亿个整数”我们可以快速的联想到Bit-map。下边关键的问题就是怎么设计我们的Bit-map来表示这20亿个数字的状态了。其实这个问题很简单,一个数字的状态只有三种,分别为不存在,只有一个,有重复。因此,我们只需要2bits就可以对一个数字的状态进行存储了,假设我们设定一个数字不存在为00,存在一次01,存在两次及其以上为11。那我们大概需要存储空间2G左右。

接下来的任务就是把这20亿个数字放进去(存储),如果对应的状态位为00,则将其变为01,表示存在一次;如果对应的状态位为01,则将其变为11,表示已经有一个了,即出现多次;如果为11,则对应的状态位保持不变,仍表示出现多次。

最后,统计状态位为01的个数,就得到了不重复的数字个数,时间复杂度为O(n)。

快速查找

这就是我们前面所说的了,int数组中的一个元素是4字节占32位,那么除以32就知道元素的下标,对32求余数(%32)就知道它在哪一位,如果该位是1,则表示存在。
原文:Bitmap简介 - 废物大师兄 - 博客园

2 HyperLogLog

  • 业务场景:基数统计 通常是用来统计一个集合中不重复的元素个数,统计整个网站web页面的UV。UV在PV的基础上根据用户id去重,这就要求了每一个网页请求都需要带上用户的ID
  • 方案选择:
    方案一:set数据结构,自带去重:为每一个页面设置一个独立的set集合来存储所有当天访问过此页面的用户ID。
    方案一缺点:1、存储空间巨大(网站访问量一大、单个set集合就会非常大,页面再一多…)2、set内部处理去重功能非常耗费资源、set集合聚合函数统计也复杂

    方案二,使用B树数据结构,最大的优势就是插入和查找效率很高,要计算基础值,只需要计算 B 树的节点个数就行了。
    方案二缺点:1、存储空间大(将B树结构维护到内存中,能够解决统计和计算的问题,但是并没有节省内存)

    方案三:HyperLogLog:提供不精确的去重计数方案(用于访问量比较大的页面的UV统计,HyperLogLog数据结构最多占用12KB),虽不精确,但也不是非常离谱,标准误差时0.81%
    HyperLogLog优点:在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的
    import redis.clients.jedis.Jedis;
    public class HyperLogLog {
        public static void main(String[] args) {
            Jedis jedis = new Jedis();
            for (int i = 0; i < 1000; i++) {
                jedis.pfadd("codehole", "user" + i);
                long total = jedis.pfcount("codehole");
                if (total != i + 1) {
                    System.out.printf("%d %d\n", total, i + 1);
                }
            }
            jedis.close();
        }
    }

1分布式锁的几种实现

(1)zookeeper分布式锁,基于自增节点
(2)redis分布式锁,基于setnx命令;

2 分布式锁的基本功能

(1)同一时刻只能存在一个锁
(2)需要解决意外死锁问题,也就是锁能超时自动释放;
(3)支持主动释放锁
(3)分布式锁解决什么问题:多进程并发执行任务时,需要保证任务的有序性或者唯一性
加锁者需要设置过期时间避免自己挂了不释放锁,释放锁判断value是自己设置的,避免锁已经自动过期且被别人加锁而自己再去释放锁导致误删

3 缓存被“击穿”问题

(1)概念:缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一 般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
(2)如何解决:业界比较常用的做法,是使用mutex(互斥)。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。类似下面的代码:

public String get(key) {
     String value = redis.get(key);      
     if (value == null) { //代表缓存值过期
         //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
         if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表设置成功
              value = db.get(key);
              redis.set(key, value, expire_secs);
              redis.del(key_mutex);
         } else {//代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
              sleep(50);
              get(key);  //重试
         }
     } else {              
        return value;      
     }
 }
  • 缓存雪崩:是因为大面积的缓存失效,打崩了DB,
  • 缓存击穿:不同的是缓存击穿是指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞。
  • 缓存穿透:是指缓存和数据库中都没有的数据,而用户不断发起请求,我们数据库的 id 都是1开始自增上去的,如发起为id值为 -1 的数据或 id 为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大,严重会击垮数据库。

4 Redis内存淘汰策略

redis 内存数据集大小上升到一定大小的时候,就会进行数据淘汰策略。
通过配置redis.conf中的maxmemory(config set maxmemory 100000:设置最大内存)这个值来开启内存淘汰功能(maxmemory为0的时候表示我们对Redis的内存使用没有限制)。
通过配置redis.conf中的maxmemory-policy设置淘汰策略设置:策略类型:
 1、最近最少使用(设置、不设置了过期时间的key数据集)
 2、将要过期的数据(设置、不设置设置了过期时间的key数据集)
 3、任意选择数据(设置、不设置了过期时间的key数据集)
 4、不可写入任何数据集(也不删除)

5 Redis数据持久化方案

RDB:是Redis默认的持久化方式。按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件。即Snapshot快照存储,对应产生的数据文件为dump.rdb,通过配置文件中的save()参数来定义快照的周期。

AOF:Redis会将每一个收到的写命令都通过Write函数追加到文件最后。当Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。

RDB

优点:他会生成多个数据文件,每个数据文件分别都代表了某一时刻Redis里面的数据,这种方式,有没有觉得很适合做冷备,完整的数据运维设置定时任务,定时同步到远端的服务器,比如阿里的云服务,这样一旦线上挂了,你想恢复多少分钟之前的数据,就去远端拷贝一份之前的数据就好了。RDB对Redis的性能影响非常小,是因为在同步数据的时候他只是fork了一个子进程去做持久化的,而且他在数据恢复的时候速度比AOF来的快。

缺点:RDB都是快照文件,都是默认五分钟甚至更久的时间才会生成一次,这意味着你这次同步到下次同步这中间五分钟的数据都很可能全部丢失掉。AOF则最多丢一秒的数据,数据完整性上高下立判。还有就是RDB在生成数据快照的时候,如果文件很大,客户端可能会暂停几毫秒甚至几秒,你公司在做秒杀的时候他刚好在这个时候fork了一个子进程去生成一个大快照,哦豁,出大问题。

AOF

优点:上面提到了,RDB五分钟一次生成快照,但是AOF是一秒一次去通过一个后台的线程fsync操作,那最多丢这一秒的数据。AOF在对日志文件进行操作的时候是以append-only的方式去写的,他只是追加的方式写数据,自然就少了很多磁盘寻址的开销了,写入性能惊人,文件也不容易破损。AOF的日志是通过一个叫非常可读的方式记录的,这样的特性就适合做灾难性数据误删除的紧急恢复了,比如公司的实习生通过flushall清空了所有的数据,只要这个时候后台重写还没发生,你马上拷贝一份AOF日志文件,把最后一条flushall命令删了就完事了。

缺点:一样的数据,AOF文件比RDB还要大。AOF开启后,Redis支持写的QPS会比RDB支持写的要低,他不是每秒都要去异步刷新一次日志嘛fsync,当然即使这样性能还是很高,我记得ElasticSearch也是这样的,异步刷新缓存区的数据去持久化,为啥这么做呢,不直接来一条怼一条呢,那我会告诉你这样性能可能低到没办法用的,大家可以思考下为啥哟。
 

6 Redis高可用设计:主服务器可能存在单点故障,加入Sentinel(哨兵)

哨兵模式实现原理?(2.8版本或更高才有)
1.三个定时监控任务:

  1. 每隔10s,每个S节点(哨兵节点)会向主节点和从节点发送info命令获取最新的拓扑结构
  2. 每隔2s,每个S节点会向某频道上发送该S节点对于主节点的判断以及当前Sl节点的信息,同时每个Sentinel节点也会订阅该频道,来了解其他S节点以及它们对主节点的判断(做客观下线依据)
  3. 每隔1s,每个S节点会向主节点、从节点、其余S节点发送一条ping命令做一次心跳检测(心跳检测机制),来确认这些节点当前是否可达

2.主客观下线:

  1. 主观下线:根据第三个定时任务对没有有效回复的节点做主观下线处理
  2. 客观下线:若主观下线的是主节点,会咨询其他S节点对该主节点的判断,超过半数,对该主节点做客观下线

3.选举出某一哨兵节点作为领导者,来进行故障转移。选举方式:raft算法。每个S节点有一票同意权,哪个S节点做出主观下线的时候,就会询问其他S节点是否同意其为领导者。获得半数选票的则成为领导者。基本谁先做出客观下线,谁成为领导者。

4.故障转移(选举新主节点流程):
 

7 Redis为什么使用单进程单线程方式也这么快:

Redis快的主要原因是:
   1、完全基于内存;
   2、数据结构简单,对数据操作也简单;
   3、使用多路 I/O 复用模型(nio的Selector也是基于select/poll模型实现)
   4、单线程操作,避免了昂贵的线程上下文切换

8 redis的setnx锁到了超时时间失效,并发死锁问题

redis的setnx锁到了超时时间失效,并发的问题

9 Redis如何做数据分片

Redis集群使用数据分片(sharding)而非一致性哈希(consistency hashing)来实现:一个Redis集群包含16384个哈希槽(hash slot),数据库中的每个键都属于这16384个哈希槽的其中一个,集群使用公式CRC16(key)%16384来计算键key属于哪个槽,其中CRC16(key)语句用于计算键key的CRC16校验和。
图片123456
节点A负责处理0号至5500号哈希槽。
节点B负责处理5501号至11000号哈希槽。
节点C负责处理11001号至16384号哈希槽。
而对于mysql来说,一般情况下,如果某个表的数据有明显的时间特征,比如订单、交易记录等,则他们通常比较合适用时间范围分片,因为具有时效性的数据,我们往往关注其近期的数据,查询条件中往往带有时间字段进行过滤,比较好的方案是,当前活跃的数据,采用跨度比较短的时间段进行分片,而历史性的数据,则采用比较长的跨度存储。

10 epoll为啥比select/poll好

Redis IO多路复用技术以及epoll实现原理

  • 在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。
  • 这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效(如此,一颗红黑树,一张准备就绪句柄链表,少量的内核cache ,就帮我们解决了大并发下的socket处理问题)

    执行epoll_create时,创建了红黑树和就绪链表
    执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据
    执行epoll_wait时立刻返回准备就绪链表里的数据即可。
     

11 sentinel实现redis切换的原理那就是

sentinel心跳检测到主节点出现异常后,通过修改redis.conf配置文件的形式实现redis主从节点之间的故障切换

12 主从复制原理

Redis主从复制原理总结

全量同步
Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下: 
-  从服务器连接主服务器,发送SYNC命令; 
-  主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令; 
-  主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令; 
-  从服务器收到快照文件后丢弃所有旧数据,载入收到的快照; 
-  主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令; 
-  从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;

增量同步
Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。 
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
Redis主从同步策略
主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。

当主服务器不进行持久化时复制的安全性

在进行主从复制设置时,强烈建议在主服务器上开启持久化,当不能这么做时,比如考虑到延迟的问题,应该将实例配置为避免自动重启。为什么不持久化的主服务器自动重启非常危险呢?为了更好的理解这个问题,看下面这个失败的例子,其中主服务器和从服务器中数据库都被删除了。设置节点A为主服务器,关闭持久化,节点B和C从节点A复制数据。这时出现了一个崩溃,但Redis具有自动重启系统,重启了进程,因为关闭了持久化,节点重启后只有一个空的数据集。节点B和C从节点A进行复制,现在节点A是空的,所以节点B和C上的复制数据也会被删除。当在高可用系统中使用Redis Sentinel,关闭了主服务器的持久化,并且允许自动重启,这种情况是很危险的。比如主服务器可能在很短的时间就完成了重启,以至于Sentinel都无法检测到这次失败,那么上面说的这种失败的情况就发生了。如果数据比较重要,并且在使用主从复制时关闭了主服务器持久化功能的场景中,都应该禁止实例自动重启。

主从复制:Redis主库如果挂了,你还是得「手动」将从库升级为主库

接下来就到了「哨兵」登场了:

Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。

Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。

redis 的线程模型了解么?
Redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 Socket,根据 Socket 上的事件来选择对应的事件处理器进行处理。

文件事件处理器的结构包含 4 个部分:

多个 Socket
IO 多路复用程序
文件事件分派器
事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
多个 Socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 Socket,会将 Socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
 

Redis做限流:

/**
     * 其实限流涉及的最主要的就是滑动窗口,上面也提到1-10怎么变成2-11。其实也就是起始值和末端值都各+1即可。
     *
     * 而我们如果用Redis的list数据结构可以轻而易举的实现该功能
     *
     * 我们可以将请求打造成一个zset数组,当每一次请求进来的时候,value保持唯一,可以用UUID生成;
     * 而score可以用当前时间戳表示,因为score我们可以用来计算当前时间戳之内有多少的请求数量。
     * 而zset数据结构也提供了range方法让我们可以很轻易的获取到2个时间戳内有多少请求
     */
    public String limitFlow(){
        Long currentTime = System.currentTimeMillis();
        System.out.println(currentTime);
        if(redisTemplate.hasKey("limit")) {
            Integer count = redisTemplate.opsForZSet().rangeByScore("limit", currentTime -  60*1000, currentTime).size();        // intervalTime是限流的时间
            System.out.println(count);
            if (count != null && count > 5) {
                return "每分钟最多只能访问5次";
            }
        }
        redisTemplate.opsForZSet().add("limit", UUID.randomUUID().toString(),currentTime);
        return "访问成功";
    }
    /**
     * 通过上述代码可以做到滑动窗口的效果,并且能保证每N秒内至多M个请求,缺点就是zset的数据结构会越来越大
     *
     * ZSet的内部原理是通过跳跃列表
     */



// 依靠List的leftPop来获取令牌:输出令牌
    public String limitFlow2(Long id){
        Object result = redisTemplate.opsForList().leftPop("limit_list");
        if(result == null){
            return "当前令牌桶中无令牌";
        }
        return "有";
    }
    // 再依靠Java的定时任务,定时往List中rightPush令牌,当然令牌也需要唯一性,所以我这里还是用UUID进行了生成
    // 10S的速率往令牌桶中添加UUID,只为保证唯一性
    @Scheduled(fixedDelay = 10_000,initialDelay = 0)
    public void setIntervalTimeTask(){
        redisTemplate.opsForList().rightPush("limit_list",UUID.randomUUID().toString());
    }

List数据类型作为队列:

  • 队列空了怎么办?客户端通过队列的pop操作来获取消息,然后进行处理。可是如果队列空了,客户端就会陷入pop的死循环,不停地pop;这就是浪费生命的空轮询。空轮询不但拉高了客户端部 CPU消耗,Redis的QPS也会被拉高。通常我们使用sleep来解决这个问题,让线程睡一会,睡个1s 就可以了,不客户端的CPU消耗能降下来,Redis的QPS也降下来了。用上面睡眠的办法可以解决问题。但是又有个小问题,那就是睡眠会导致消的延迟增大。如果只有1个消费者,那么这个延迟就是Is。如果有多个消费者,这个延迟会有所下降,因为每个消费者的睡眠时间是岔开的。redis提供了阻塞读:blpop brpop,这两个指令的前级字符b代表的是blocking,也就是阻塞读。阻塞读在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立知醒过来。消息的延迟几乎为零。用blpop/brpop 替代前面的Ipop/rpop,就完美解决上面的问题。
  • 空闲连接自动断开?如果线程一直阻塞在那里,Redis的客户端连接就成了闲置连接,闲置过久,服务器一般会主动断开连接,减少闲置资源占用。这个时候 blpop/brpop 会抛出异常。使用者需要自己处理异常,比如重试
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值