Redis

作者: Goooooooooooal | 来源:发表于2018-10-21 12:13 被阅读0次

    Redis是什么

    1. Redis是1个内存数据库,非关系型,基于Key-Value存储,速度快
    2. 支持多种数据类型,String、Hash、List、Set和ZSet
    3. Redis支持主从复制,集群;支持分布式系统下的高可用
    4. Redis提供了多种附加功能,包括事务, 发布-订阅,消息队列和分布式锁

    Redis基于事件驱动

    Redis是1个事件驱动程序,主要处理2种事件,文件事件和时间事件

    1. 文件事件,Client通过socket连接到Server,对Server进行读写操作,由文件事件负责执行Client的请求并响应
    2. 时间事件,Redis中的一些操作需要定时执行,例如负责管理Redis资源的serverCron()默认100ms执行1次
      (1)更新Redis内存使用情况
      (2)检查Client是否连接超时,将超时的连接释放掉
      (3)删除过期key
      (4)将aof_buf缓冲区中的数据写入到AOF文件

    数据结构

    String,最常规的get/set
    Hash,
    List,可以从左或从右插入,底层是1个双向链表
    Set,可用于分布式系统中的全局去重,JVM只能做单个应用的去重
    ZSet,排序Set,有1个权重参数score,Set中元素以score排序。可用于排行榜,取Top N应用

    过期策略和内存淘汰进制(结合做的分布式session说)

    持久化策略

    Redis是1个内存数据库,如果Redis挂掉或停机,Redis中的数据会丢失,因此Redis提供了两种持久化策略,RDB和AOF,各有利弊。Redis官网推荐同时使用RDB和AOF

    1. RDB,将某1时间点的数据库快照保存到1个RDB文件中
      RDB会将数据写入到1个临时文件中,写入操作完成后,用临时文件替换上次持久化的文件

    Redis使用SAVE/BGSAVE命令,创建RDB文件

    SAVE会阻塞主线程,SAVE期间,Client的一切读写命令都会被阻塞

    BGSAVE会创建1个子线程执行RDB文件的创建和写入工作,不会阻塞主线程,BGSAVE期间,Redis能够正常的响应Client的读写请求

    Redis配置文件中,默认3种情况下会执行RDB:
    (1)900s内,key发生1次变化 save 900 1
    (2)300s内,key发生10次变化 save 300 10
    (3)1s内,key发生10000次变化 save 1 10000

    1. AOF,保存Redis所有执行过的写命令
      当Redis故障重启时,会将所有的写命令重新执行,来恢复数据库状态。 AOF默认是关闭的
      (1)配置文件redis.conf中, appendonly设置为yes,则打开AOF
      (2)写命令会先被放入aof_buf缓冲区,默认1s进行1次fsync,将aof_buf中存放的写命令写入AOF文件
      (3)因为采取追加方式,所以AOF文件会越来越多,当超过设定的阈值时,会采取AOF重写,将多条写命令合并为1条,来减小AOF文件大小

    2. 优缺点
      (1)RDB可能导致数据不完整,2次RDB之间,如果Redis故障,上次RDB之后,Redis接收的所有写命令会丢失
      (2)相同的数据规模,AOF文件比RDB文件大,且AOF恢复时间慢
      (3)Redis规定,如果开启AOF,会优先使用AOF恢复数据库状态;未开启AOF时,才使用RDB

    主从模式

    1. 目的
      提高性能,主服务器用来执行写命令,从服务器设置为只读;主从同步,异步进行,同步不会影响主的处理性能

    2. 做法
      1主2从3Sentinel
      (1)配置文件redis.conf中,使用slaveof <master_ip> <master_port>的方式让从服务器来复制主服务器
      (2)配置文件默认,从服务器是只读的,slave-only-read值为yes
      (3)配置文件默认,从服务器每隔10s向主发送心跳
      (4)Sentinel是Redis哨兵,用来监视Redis主服务器和从服务器,当被监视的主服务器处于下线状态时,哨兵会从主服务器下的从服务器中挑选1个,让其成为主服务器

    3. 旧版复制SYNC
      分为同步和命令传播

      同步,发生在初次复制(从向主发送slaveof命令),将从的数据库状态,更新到主所处的数据库状态
      (1)从向主发送SYNC
      (2)主执行BGSAVE命令,使用子线程创建RDB文件,并使用缓冲区记录执行BGSAVE期间,主所执行的所有写命令
      (3)创建完RDB文件,主会发生给从,从接收并载入,从将自己的数据库状态更新到主执行BGSAVE之前的状态
      (4)主将缓冲区中存放的写命令发送给从,从执行,将自己的状态更新到与主一致

      命令传播,发生在同步后,当Client向主写入数据,主从状态不一致,为了再次让主从状态一致,主会将造成不一致的写命令发送给从,让从执行,使主从状态再次一致

      旧版复制的缺陷
      对于已经完成同步,处于命令传播阶段的主从服务器。当主从连接断开后,重新建立连接时,会像初次复制一样,使用BGSAVE生成RDB文件,生成的是全量的数据
      但是断线重连后,造成主从不一致的数据,是断线期间Client写入主的数据,再次执行初次同步,非常浪费主从服务器的资源。主执行BGSAVE,浪费主的CPU、内存和磁盘I/O;从载入RDB文件,会阻塞Client请求;传输RDB文件,还会浪费网络资源

    4. 新版复制PSYNC
      分为完全重同步和部分重同步,完全重同步就是旧版复制SYNC的初次复制
      部分重同步,用于断线后重连,如果条件允许,主会将断线期间发生的写命令发给从,让从执行,使主从状态达到一致。从而避免了旧版复制SYNC断线重连,重新生成RDB,浪费主从服务器资源和网络资源的情况
      (1)复制偏移量offset
      主从都维护1个offset,主发送n个字节,offset+n,从接收n个字节,offset+n。通过比较主从offset,可知主从是否一致
      (2)复制积压缓冲区
      主维护1个固定长度为1MB的先进先出队列(FIFO)
      命令传播时,主不但向从发送写命令,还将写命令放入复制积压缓冲区,复制积压缓冲区的这些写命令的每个字节都有复制偏移量offset标记。但是复制积压缓冲区这个FIFO是有大小限制的,默认1MB,断线重连后,从通过PSYNC命令向主发送自己的offset,主会根据这个offset来决定执行完全重同步还是部分重同步
      (1)若offset之后的数据在复制积压缓冲区,执行部分重同步
      (2)否则,执行完全重同步

      (3)Server ID
      不论主从,Redis服务器都有Server ID,在Redis启动时自动生成,是1个40位的随机16进制String
      初次复制时,主将ID发送给从,从保存;断线重连时,从发送保存的主ID,如果ID相同,尝试执行部分重同步;否则在断线重连期间,Redis主可能发生了变化,执行完全重同步

    过期key淘汰策略和内存淘汰策略

    Redis可以为key设置生存时间,当key超时,会自动删除key。Redis有3种key淘汰策略

    1. 定时删除
      主动删除,设置key生存时间的同时,设置1个定时器Timer,定时器会在key过期时,自动删除key
      优点:对内存友好,保证key在过期时立刻被删除,节省内存空间
      缺点:对CPU不友好,创建定时器Timer浪费CPU资源
    2. 惰性删除
      被动删除,放任key不管,在每次从Redis中取值时,才会检查key是否过期,如果过期会删除key
      优点:对CPU友好,在获取key时才检查key是否过期
      缺点:对内存不友好,只有不去获取过期key,过期key会一直存放在内存中,造成内存泄漏
    3. 定期删除
      主动删除,每隔一段时间,扫描1次,检查key是否过期,对过期key进行删除
      定时删除会浪费CPU资源,惰性删除会浪费内存资源,定期删除是定时删除和惰性删除的折中

    Redis采用惰性删除和定期删除结合的方式,来淘汰过期key。Redis的serverCron()每隔100ms,随机抽查部分key,检查是否存在过期key,有则删除;另外,当获取key的时候,会检查key是否过期,如果过期则删除

    惰性删除结合定期删除,无法完全的解决问题,如果定期删除没有删除过期key,并且客户端也没有获取过该key,那么Redis的内存会越来越高,此时会执行Redis的内存淘汰策略,Redis一共有6种内存淘汰策略:

    1. NoEviction, 内存不足时,拒绝新的写入数据
    2. AllKeys-lru, 移除最近最少使用的key
    3. AllKeys-random,随机移除key
    4. Volatile-lru,在设置过期时间的key中,移除最近最少使用的key
    5. Volatile-random,在设置过期时间的key中,随机移除key
    6. Volatile-ttl,在设置过期时间的key中,优先移除离过期时间最近的key

    如果没有设置key的过期时间, 不满足先决条件, volatile-*和noeviction(不删除)一致

    热点数据缓存

    热点数据是经常被查询,很少被修改的数据;查询时,因为Redis基于内存,速度非常快,我们会先查询缓存Redis,如果缓存中有,返回。缓存中没有,需要查询MySQL,并将结果放入到缓存,供下次查询使用

    [缓存了哪些热点数据]

    MySQL和Redis双写一致

    对于数据的操作,就是对数据的CRUD,其中查属于读,增删改属于写

    1. 读操作,就是查询时先查询Redis,如果Redis中存在数据,直接返回,不存在则查询MySQL并将结果存放到Redis。所以读操作不涉及双写不一致的情况

    2. 新增时,只需要将数据插入到MySQL,不需要管Redis,因为如果有线程在Redis中查询不到该数据,会查询MySQL,并将结果放入Redis供下次查询使用
      当对数据进行更新和删除时,会出现MySQL和Redis双写不一致的情况。不管是先删缓存,再写MySQL;还是先写MySQL,再删缓存,都会出现不一致。Redis只能保证最终一致性,而不能保证强一致,所以要求强一致的数据不能放入缓存

      (1)先写MySQL,再删缓存
      如果写完MySQL,线程崩溃,那么只更新了MySQL的值,未更新缓存的值。所有查询操作使用的都是原来的脏数据,MySQL和Redis数据不一致
      (2)先删缓存,再写MySQL
      线程A删除缓存之后,写MySQL之前,线程B查询数据,此时Redis中的数据被删除,重新从MySQL中读取,因为线程A还未写MySQL,所以查询的还是原数据,线程B将原数据再次放入缓存。导致MySQL和Redis数据不一致

    对于MySQL和Redis双写不一致这种情况,解决方案有很多,我选择了最简单的"双删+超时"的方式

    在写MySQL操作之前和之后,都对Redis进行删除。先删除Redis,再写MySQL,为了避免了在写MySQL期间,有线程读取到MySQL中的旧值,并将其放入缓存,在写入MySQL成功后,再删除缓存。在JAVA代码中,调用写MySQL的函数,到真正的在数据库中将数据修改,需要一段时间,因此调用写MySQL函数之后,让线程等待一段时间,待写入MySQL成功后,再2nd删除缓存

    双删+超时可以满足不部分需求,如果追求完美,它还有2个小的缺点
    (1)在调用完写MySQL方法之后,但是操作数据库未生效之前,可能出现脏数据,但是这是非常小概率出现的
    (2)在删除Redis缓存中数据时,如果删除失败,如何解决
    需要提供1个重试机制,保证Redis中数据肯定被删除。可以将需要删除的key放入到消息队列中,自己生产数据,然后自己消费,保证一定删除key

     另一种比较实用的做法是,使用消息队列,订阅MySQL的binlog。这方面没有进行深入研究
    

    缓存穿透和缓存雪崩

    穿透: 当查询1个肯定不存在的数据时,因为Redis中没有,所以会去查询MySQL,又因为查询MySQL无结果,所以不会放入缓存。导致对不存在数据的每次请求,都会查询MySQL。这种情况称为缓存穿透

    雪崩:多线程条件下,Redis在某个时间点,大量数据集体失效。产生大量的缓存穿透,所有的查询都落在MySQL上,造成MySQL负载过重而崩溃

    1. 缓存穿透
      对于缓存穿透,有2种解决方式
      (1)暴力方法,查询的数据肯定不存在,查询Redis和MySQL都不会得到结果。即使查询MySQL得不到结果,也会将空结果放入到Redis,只不过要设置1个生存时间ttl,让它在ttl到期时被删除。这种做法治标不治本,不过在一定程度上,降低了查询MySQL的次数

    (2)使用布隆过滤器

    1. 需要k个哈希函数,k个哈希函数可以将key值散列成k个整数
    2. 还需要1个位数组,初始化时,每位都是0
    3. 当加入key时,使用k个哈希函数得到k个整数,并将数组中对应位置改为1
    4. 判断1个key是否在集合中时,使用k个哈希函数对key进行散列,得到k个整数,如果所有的位置都为1,认为该key在集合中

    使用Google Guava类库下的BloomFilter实现,我们只需要指定向BloomFilter放入的值的个数,它会为我们自动生成哈希函数,和位数组。做了1个测试,向BloomFilter存入100万个int型数字,底层会创建1个700多万位的位数组,1个int占4字节,32位,正常存储100万个int需要3200万位,而BloomFilter只使用了1/5

    1. 初始化时,将所有数据库中的值都放入BloomFilter
    2. 查询时,会先使用BloomFilter检查查询的key是否存在,如果存在才会查询DB,否则不允许查询
    3. 新增数据时,也会将key加入到BloomFilter
    1. 缓存雪崩
      对于雪崩,采用Zookeeper实现了1个分布式锁,控制同时访问线程的个数,避免过多的线程对MySQL进行操作,导致MySQL崩溃

    在JVM中,可以使用Semaphore来控制同时访问共享资源的线程个数。线程需要获取permit,才能操作共享资源,获取不到的线程阻塞,直到获取到permit才能执行

    在分布式系统中,无法使用Semaphore来控制同时访问的线程个数,因为应用程序单独部署,运行在不同的进程上,无法使用JVM做线程同步。Apache Curator提供了分布式系统下的线程辅助类,使用的是InterProcessSemaphoreV2,它是1个分布式信号量,类似于JVM的Semaphore的permit,分布式系统下的进程需要获取到lease租约,才能运行,否则一直等待

    InterProcessSemaphoreV2使用acquire()和release()来获取和释放租约lease
    获取时,在zk上创建1个临时有序节点,如果子节点个数<lease个数,则获取成功;否则,阻塞等待
    释放时,进程会将自己创建的临时节点删除,导致子节点个数发生变化

    相关文章

      网友评论

          本文标题:Redis

          本文链接:https://www.haomeiwen.com/subject/dtavzftx.html