Redis,永远嘀神!

前言

前段时间在复习来着,过程中发现许多知识在脑子中很零散。并不是不知道,也或者曾经在某个地方看到过,突然想不起来,再次看到时又能记个大概。总结下来就是知识不成体系,总是东一块西一块,没有把他们串起来一起去理解记忆。刚好最近也在面试,就用自己的思路整理个redis笔记吧,哪怕是没有人看,对自己来说也是一次很好的归纳总结了,让自己的印象更深刻一点。

为什么标题要叫"redis,永远嘀神!"呢?因为当下,redis在作为缓存数据库这一块的地位,几乎可以称为一哥了(至少本人接触与朋友公司了解到的Nosql都是Redis)。而刚好,前段时间刚好也在Twitter上看到redis之父Antirez退休的消息,虽然他自己也没能想到当时just for fun的一个小玩意如今能成为现在这么多项目的重要组成部分之一。但是随着Redis越来越重要,Antirez的角色也逐渐从一个充满新奇的研发者,变成了一个专职维护者,日复一日的维护着Redis的稳定与安全,然而他的本心却并非想成为一个维护者,所以他“不干了”哈哈哈哈哈哈哈哈哈。

下载

让你维护一份项目维护十年,你干不干?我只能说:敬Antirez!

最近我自己整理了一张redis的思维导图,用来帮助自己记忆,同时也就根据这张导图和近来面试中问到的东西来看一看redis的“神之力量”。神之力量

导图
Redis
What?即:什么是Redis?
1. 什么是Redis?

Redis is often referred as a data structures server. What this means is that Redis provides access to mutable data structures via a set of commands, which are sent using a server-client model with TCP sockets and a simple protocol. So different processes can query and the same data structures in a shared way.

Data structures implemented into Redis have a few special properties:

  • Redis cares to store them on disk, even if they are always served and modified into the server memory. This means that Redis is fast, but that is also non-volatile.
  • Implementation of data structures stress on memory efficiency, so data structures inside Redis will likely use less memory compared to the same data structure modeled using an high level programming language.
  • Redis offers a number of features that are natural to find in a database, like replication, tunable levels of durability, cluster, high availability.

Another good example is to think of Redis as a more complex version of memcached, where the operations are not just SETs and GETs, but operations to work with complex data types like Lists, Sets, ordered data structures, and so forth.

-------------来自官方GItHub

官方的解释我已经给你贴过来了,怎么样贴心吧,别急着爱我,还有呢!

Redis经常被称为数据结构服务器。这意味着Redis可以通过一个使用带有Tcp套接字和简单协议的服务器-客户端模型,来使用命令访问一些可变的数据结构。所以不同的程序或是进程能够以相同的方式来查询、修改这些相同的数据结构。

在Redis中实现的数据结构有一些特殊的属性:

  • 即使Redis一般总是通过内存来提供服务和数据修改存储,但是Redis也能够将他们存储到磁盘上。这个意思是,Redis快而且也不容易丢失。
  • 数据结构的实现会提高内存效率,所以与其他高级语言的数据结构模型相比,redis的数据结构模型会使用更少的内存。
  • Redis提供了一些和数据库同样的功能,比如说主从复制,可控的持久性,集群,高可用。

一个更好的例子就是你可以把Redis看作一个高级版的Memcached,一个不仅仅只会SET和GET的,还提供更多例如List,Sets,有序SortedSet等等高级数据结构。

看到这里不知道你哭没哭,反正我是哭了!哭了

2. 数据结构

面试官:看你用过redis,说说redis有哪些数据结构?

你:redis的数据结构有String,List,Set,Sorted set,Hash。

面试官(等了一会,面面相觑):嗯,那你说说Sort,Hash一般怎么用?

你:额,这两个用的比较少,一般都用的String,List。

面试官:嗯,我一会有个会,你先回去,有消息我让人事联系你。

你:蛤?就这?这就完了?这就完了

实际开发中我们确实一般只会用到一些简单的数据结构,因为这些简单的数据结构一般能够满足绝大部分的业务场景需求,但是一些高级的数据结构也需要去了解,万一有用到的时候,也能够第一时间反应可以怎么实现,只要有了思路,接下来就好办多了。

  • String

    String作为最简单的数据类型,一般的key/value直接存储都可以认作String类型。所以String类型可以像操作普通字符串一样,通过redis对String字符串进行拼接,截取,获取指定bit位内容等操作。

    实现方式:在redis内部,String使用sds(Simple Dynamic String)来实现。拥有可动态扩展,二进制安全,与传统C语言兼容三大特性。但是当一个String类型的值,这个值是一个数字的时候,此时Redis内部会把它转成Long型来存储,从而减少内存的开销。

  • List

    List是redis中用的第二多的数据结构了,在涉及到列表方面,例如商品列表,关注粉丝列表,分页查询都可以使用list来实现。

    同时通过list,也可以实现简单的队列功能,通过从List的一边生产,另一边消费来实现,而且通过redis的原子特性,会比加锁来的更加高效。

    实现方式:新版的redis中,list使用quicklist(快速列表)来作为列表的编码。quickList作为前身ziplist和双向链表的结合体,每一个节点都是一个ziplist。

  • Hash

    Hash我们可以根据一个key将一个map存放到redis中。例如存储对象的时候,最简单的我们可以将一个对象序列化成字符串,然后根据一个key将对象存储到redis中。但是这样做,每次修改对象中的某个值的时候,我们就必须将整个值取出来,反序列化成对象,修改完之后再次转成String字符串存储到redis中,增加了不必要的开销。这时候我们可以根据对象的唯一值作为key,对象里面的每一个内容单独作为map的一部分,来将整个对象放到redis中。这样就可以直接对对象的某个字段进行编辑更新了。同样,我们也可以稍微转化一下,用hash的形式来存储分页列表数据。

    实现方式:hash在redis中也有两种实现方式,第一种是占用内存较小的zipmap,类似一维数组结构紧凑的存储键和值,查找数据的时间复杂度为O(n),所以只能作为一个轻量级的hashmap来使用。

    当数据量大的时候,使用dict来实现hash。dict中使用一个entry数组来存储key和value,一般情况下key和value都是通过指针来间接持有键和值。但是遇到值是整数的情况下,还是将值直接存储在value中的。

  • Set

    Set集合,功能和list很类似,众所周知List可重复而Set不可重复。所以,Set集合的使用经常是一些业务上需要对数据去重的场景。因为Set开放一些接口可以直接判断元素是否存在,另外还有一些差集,并集之类的操作,例如可以处理一些共同好友啊,关注啊之类的需求。

    实现方式:Set也是利用哈希来实现,原理上类似一个值全为null的hashmap,key就是Set集合中的元素,利用计算hash的方式来去重。

    同样,set的编码方式也可以是intset,一看名字就知道,这是redis为了存储整数设计的数据结构。redis源码中定义了16,32,64三种数据大小,如果一开始你的元素小于16个,然后新增逐渐增加。intset可以执行一个resize从16往32扩容,再增加,再resize,从32往64扩容,再增加在扩容。但是一旦超过redis可配置的(set-max-intset-entries)intset最大容量就会转成hash存储。

  • Sorted Set

    了解了Set,我们再来看Sorted Set有序集合,又称‘’Zset"。Sorted Set与Set的区别就是Sorted Set是一个带有Score的Set集合,Socre称为优先级,权重,或者加权,所以Sorted Set插入是根据Socre来自动排序的。一个有序又不重复列表,想一想一般会有哪些业务场景呢?例如时间线相关,优先级队列啊等等。

    实现方式:ziplist:压缩列表,list中也使用过的压缩列表。每个set中的元素使用两个紧靠在一起的压缩列表保存,一个放值,一个放socre。

    skiplist:跳跃表,什么是跳跃表呢?我尽量用语言描述清楚一点。跳跃表是一个单链表加索引来实现,例如现在有一个链表,链表元素是1,2,3,4,5。。。10这十个元素。我们想要查找”6“这个元素的时候是不是需要从头往后一个个查直到查到数据呢,这样的话效率也太慢了。于是我们把这个链表每隔三个元素向上提升一层,就有1。。3。。5。。7。。9。。这样的上层索引数据。所以依次遍历到5比6小,7比6大,这样4次就找到了6,由原来一个个的直接遍历6次,变成4次提高了效率。当然,这只是个小例子来引出跳跃表概念,redis中的跳跃表由zskiplistNode和skiplist,稍稍比这个要复杂一点,有兴趣的同学可以深入研究一下~

  • HyperLogLog

    Redis HyperLogLog是用来做基数统计的算法,效果上和set的效果也很相似,去重计数?

    首先基数是什么意思?我猜都有一部分人不知道。基数的定义是,一个可重复的集合中不重复的元素的个数。既然关键是个数,那其实用set集合也能实现,把元素一个个塞进去,最后获取一下个数就行了。在数据量较小可控的时候,这个方法是可行的,得到的数据也是比较准确的。但是,面对十万,百万,甚至千万的时候呢?这个Set集合会占用多大的内存?

    HyperLogLog基于概率统计的一种算法,它的优点就是:**在目标元素集很大的或者说非常大,超级大的时,计算基数所需要的空间总是固定的、很小的。官方说法是:每个HyperLogLog键只需要花费12KB的内存,就能够计算接近2^64个不同元素的基数值。细思极恐

    怎么做到这么小内存就能统计这么多数呢?redis采用了对value进行hash,又对hash值进行进制转化,分桶。每个key都对应了16384个桶,通过计算每个桶的k_max,来估算整个key的基数值。当然,因为是基于概率统计,所以也会有一定的误差。

  • 发布订阅(pub/sub)

    pubsub

    上图中,首先开启两个redis客户端连接,使用SUBSCRIBE监听通道mychannel,然后再开启另一个客户端,使用PUBLISH往通道里推送mesg消息,可以看到监听通道的两个客户端立马收到了推送的消息,可以实现简单的发布订阅功能。但是这种消息机制还是有它的局限性的,会遇到丢失啊难以进行消息确认啊等问题,所以一般实际运用中,这种模式使用的还是比较少的,毕竟有更为专业的MQ异步消息处理。

  • 事务(Transcation)

    Redis从2.0开始支持事务操作,可以由MULTI标记事务开始,然后将接下来的一连串指令放入事务中。最后,由EXEC标记触发事务,开始执行整个事务串中的命令,由此来达到一个简单的事务操作。有一说一,这个事务功能用到的场景很小,比较鸡肋。他只能保证在单台服务器下多个命令是事务执行的,但是却不能保证整个事务的原子性。假如说,事务过程中执行到一半的时候出了异常,前面已经执行过的命令是不能够回滚的,尴尬呀。

Why?即:为什么是Redis?

面试的时候经常会有面试官问:你们为什么用Redis不用XXXX?

你该怎么回答?总不能每次都说不知道,因为公司用的就是Redis,所以就用的Redis吧。(好像也没什么毛病嗷~)

为了避免这种尴尬的情况,我们还需要Redis的特性有一点了解。

1.redis与memcache的区别

为什么选redis而不选memcache?

  1. redis比memcache拥有更多的数据类型选择,memcache仅仅支持简单的key-value存储,复杂的对象需要用户自己在本地操作后存入memcache。
  2. 存储方式上Memcache官方都说道自己是一个in-memory key-value store,这就意味着断电之后数据很容易丢失。而Redis在这一块可以进行数据持久化存储。
  3. Redis支持集群化,集群化就意味更好的使用体验。而Memcache的伪集群仅仅是数据上的分库,通过自己内部的算法把数据存到不同的节点上,节点与节点之间不会互相通信,所以高可用什么的基本上不要想太多。
2.redis为什么速度快

这个问题想必大家都不陌生,可以说是新手入门每次必问了。

我们先从表面上看,Redis是一个内存数据库,所有的数据操作都是基于内存来实现的。内存的速度有多快大家一定都是知道的,毕竟都是玩电脑的,每次发现电脑快不行了,反应迟钝,打个游戏卡的不行的时候,二话不说先加个内存,又能撑三年,嘿嘿~由此再相比读取存储在内存上的数据和读取存储在磁盘介质上的数据,谁快谁慢就一目了然了。

另外,此处应该放大,求求你不要再吹Redis的快是因为单线程了好不好,这听了可真是一个男默女泪的故事!

Redis的单线程模型可以说在我个人来看可以说是历史遗留的一个BUG,但是这个BUG似乎并不是那么的致命,似乎初期还有那么的一点香?真香

Redis的快真正原因其实是IO的多路复用机制所带来的,非阻塞的IO带了的高性能的网络通信,内存的IO又比文件的IO速度高了一大截。Redis中监听多个Socket连接,将多个Socket的指令封装成内部事件来操作,然后通过单线程来操作。单线程有一个优势,那就是完完全全避免了线程切换所带来的开销。Redis官网最新的消息已经开始尝试多线程了,可以想象将来多线程的Redis加上io的多路复用机制,Redis的性能说不定又能提升一大截。那么问题来了,为什么Redis一开始是单线程呢? 简单啊,谁都不想一开始就搞复杂,都是一步步迭代来的啊,单线程不是也给你用了10年了么。

3.redis如何进行持久化

Redis的持久化分为两块,洋名:RDB和AOF。土名:冷备和热备。

Redis的持久化和我们经常用的传统型数据库备份原理很像。首先通过RDB全量持久化数据到磁盘文件中,但是RDB并不是实时的数据,它只是在某个时间点触发同步开始保存下来的数据,中间可能有一段时间的数据不全。然后又可以通过AOF增量持久化数据,这个就有点类似mysql的binlog日志了。

RDB:全称,Redis Database,RDB全量持久机制也分为两种操作,同步与异步。

  1. 同步,通过触发save命令来触发开始持久化数据,在数据持久化过程中,Redis会阻塞其他操作指令,直至数据持久化完成。这就会导致,在Redis数据量大的时候,全量持久化时间过长,导致假死的情况出现,生产需谨慎啊,兄dei。

  2. 异步,通过触发bgsave命令来触发开始持久化数据,这个时候Redis会在主线程之外fork出一个子线程来进行持久化数据。因为是子线程,所以这个时候主线程还是可以接受其他指令的。但是也有例外,假如fork子线程的时间长了呢,还是会假死哦。其实很好理解,说到底,现在的Redis还是单线程模型的,你任意一个耗时过大的操作都会阻塞其他的操作。

    savebgsave命令都是可以在redis控制端上直接操作的,有兴趣的小伙伴可以试一下。

AOF:全称,append only file,就是动态记录Redis的每一次操作命令,类比binlog日志,提供三种AOF策略。

  1. always,就是客户端的每一个写操作都保存到aof文件当,最安全,但是IO太频繁,效率低。
  2. everysec,每一秒,默认策略,每秒写入一次,如果刚好这一秒挂机,也就是这一秒的数据丢了。
  3. no,不处理,由操作系统来决定什么时候写入aof文件。(这个是啥没理解,不懂,嘿嘿嘿~)

综上所述,生产环境不差钱的升级机器,把两种持久化操作全部开启,如此强大的持久化能力,再配合上Redis的集群模式,强,无敌!差钱的,看看自身需求,要快还是要全。注意的就是,当两个模式全部开启的时候,Redis会优先使用AOF日志来恢复数据,因为AOF保存的文件比RDB文件更完整。

另外有一些小问题,比如Redis什么时候触发RDB?AOF的写读操作,对同一个key多次写,会不会记录多次?这些不知道大家有没有想过,或者被问道过,建议去看看redis的配置conf文件,会有不一样的发现。

4.pipeline的优势是什么

Pipeline是什么?管道

Pipeline实际中用到的并不是很多,稍微大致了解一下就行了。Redis说到我们用起来一般都是单个命令的set与get,往Redis里面存一个数据,返回结果成功或失败,从Redis里面取一个数据,返回成功或失败,每次都是一个请求对应一个响应。假如有大量的数据需要进行相同的操作,往Redis里面存入数据的时候,常规情况,有多少条数据,就有多少次请求响应交互,这中间无疑增加了许多的RTT(Round-Trip Time)交互往返时间延迟。Pipeline的出现,让我们可以把多条命令进行打包,一次性发给Redis服务端,从而减少RTT时间。

5.redis如何做到高可用

用一句话说Redis的高可用就是,主从同步的机制和哨兵模式的集群。

  • 高可用第一把🗡:主从同步。我们都知道如果只在一台机器上安装Redis,万一这个Redis挂了,那整个服务都将不可用,所以一般情况下都将Redis进行多节点部署。部署结构很简单,如图:主从

    然后选择其中一台作为主节点,主节点是不用改什么配置的,打开子节点的配置文件可以找到slaveof <masterip> <masterport>这样的一句配置,配置主节点的IP和端口,然后这台机器就成了主节点的子节点。然后子节点向主节点发送sync命令,主节点接受到命令之后开始fork出一个子进程开始进行RDB数据备份,备份完将数据发送给子节点进行同步。同时将备份过程中发生的数据变更缓存起来,最终一起发送给子节点,使终态同步完后主子节点数据保持一致。同步完成之后,对主节点的每一次数据变更都能实时同步到子节点。总结一下可以分布一下几个步骤:

    1. 子节点的配置主节点连接信息
    2. 子节点发送socket连接与主节点建立连接
    3. 子节点发送同步命令开始同步主节点数据
    4. 最终主节点数据变更与子节点实时同步

    于是开始装逼,现在我可以配置好多台Redis实例,再也不怕Redis挂掉了,流啊0f0227e4011bdf0569cbef28ed9f8ffc那么问一下,万一主节点挂掉呢,你是不是得一个个去改配置,把某一台升为主节点,其他所有的在改到这个新的节点下面去,如果你真是这么做的话,那你很棒棒哟!你们leader一定爱死你了呢!(一开口就是老阴阳了)

  • 高可用第二把🗡:哨兵,在这个时候就应运而生了,首先我们要配置一个sentinel.conf文件。

    哨兵拥有两个最重要的角色,相应的也就是它要做的是事情,一就是对所有主子节点进行监控,监控所有主子节点是否是在正常运行。二是选举切换,当主节点出现异常的时候,哨兵选举出新的子节点,并通过API向其他的应用发送通知。

    来了,他又来了,对,就是配置文件。在配置文件中,我们找到sentinel相关配置内容。

    类似sentinel monitor mymaster对应参数类型分别是名称,IP,端口,选举次数,比如sentinel monitor mymaster 192.168.10.202 6379 2,这样就是当master节点挂了之后,必须经过2个哨兵的确认,才能认为主节点确实挂了,然后启动故选举新的master,故障迁移等操作。

    sentinel down-after-milliseconds mymaster N,比如代表每隔N秒检查一次集群等等,通过相关命令来配置哨兵。而且,哨兵有个前提,就是至少3台机器,原因也很简单,如果只有两台机器,一台挂了,剩下一个独苗了,还选个锤子选。

  • 高可用第三把🗡:cluster,Redis分布式解决方案

    分布式数据存储,就是把数据按照相应的规则存储到不同的槽区里面去,至于分区的规则,HASH可以说是最普遍的了,当然还有其他的规则。cluster的目的是什么呢?前面我们都是说主节点,子节点,是不是都潜移默化的把数据都放在主节点上,子节点单纯作为备胎?这样当出现单机内存,并发量过大,流量瓶颈的时候,主节点会扛不住。cluster将数据按照规则分片存储,以达到一个负载均衡的目的。

    这三把剑第一把解决单机问题,第二把解决多机环境下主从切换问题,第三把解决单机负载过高问题。通过这三把利剑,一个Redis的高可用架构就搭建起来了。

6.redis数据怎么过期的

Redis中存储的数据都可以设置过期时间,也可以设置永不过期。那么Redis是怎么来删除那些过期的key值的呢?

  • 定期删除

    就是每隔一个指定的时间,Redis去缓存中拿出一部分数据,来检查Key是否过期,过期即将它删除

  • 惰性删除

    就是Rdeis自己不主动删除key,通过下一次查询的时候,在查询之前,检查这个key是否过期,过期即将它删除

  • 内存淘汰

    内存淘汰这个就是用户可以自己通过配置文件配置的了,我们可以依据自己部署Redis实例的机器内存大小来配置触发内存淘汰的时机。

    首先我们先来看内存淘汰的流程是什么样子呢?简单画个流程图吧。内存淘汰

    这样是不是看的就很明白了。Redis判断配置文件中配置的最大内存使用,来判断是不是快满了,到了触发阈值,就开始执行内存淘汰。

    内存淘汰的策略官网给了6中,这里也就列一下吧。

    1. volatile-lru从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
    2. allkeys-lru从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
    3. volatile-ttl从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
    4. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中随机选择数据淘汰
    5. allkeys-random:从数据集(server.db[i].dict)中随机选择数据淘汰
    6. no-enviction(默认策略):不驱逐数据,永远不过期,仅对写操作返回一个错误
How?即:怎么用Rdeis?

Talk is cheap,show me the code。下面就用针对我们业务中经常遇到的一些场景,用一些伪代码来实现,看一下到底在项目中实际怎么去使用Redis(RedisCache为利用Jedis实现的工具类)。

  1. 首先我们先来看最最最经典的使用模式最最最伪代码如下:

    private static void queryUserDetails(String userId) {
        //拿到固定业务key,假如是"user:id:"
        String userKey = "user:id"+userId;
        //根据key从缓存中查询
        String redisJson = RedisCache.get(userKey);
        //判断时候命中缓存
        if (StringUtils.isNotBlank(redisJson)){
            //JSON格式转对象
        }else {
            //从DB查询
    
            //将查询结果放入缓存,同时设置过期时间(这里的意思是5分钟)
            RedisCache.setEx(userKey,JSONObject.toJSONString(obj),5, TimeUnit.MINUTES);
        }
        //返回业务结果
    }
    

    怎么样还是挺简单的吧,不多说,起码一半的使用场景都是这样的。既然用到了缓存,我们同时也要知道缓存使用缓存所带来的问题,比如上例中,缓存数据的时间是5分钟,那么在接下来的5分钟内,假设一切正常的情况下,外部对这个case的请求始终从缓存中获取。但是万一在这5分钟内,这个case的数据发生变更了呢,是不是就会出现双写一致性的问题。

    其实这个问题很好解决,就是在你每次更新这条数据的时候,将缓存的删除就行了。至于为什么不是同步更新缓存而是去删除缓存呢,大多数情况下缓存的查询不是很频繁,没必要每次都去更新缓存。直接删除缓存,让下一次进来的查询请求,来主动给你重新设置就行了。

  2. 用Redis实现商品列表页面

    商品列表,列表列表无非也就是一个list,这个问题的关键在于什么呢?分页。因为商品过多的时候,我们总不能一下子返回很多数据给前端,前端肯定也是翻页来请求我们后端的,所以就有了两个很重要的参数,pageNo(第几页)和pageSize(每页的条数)。

    private static void queryGoodsList(String pageNo, String pageSize) {
        //首先选择用哪种数据结构
        //1:既然是list第一种可以直接选用List查询出所有满足条件的数据,然后放到缓存中,
        //然后根据pageNo和pageSize计算需要从哪里开始截取,从list中只取出我们想要的那一段就可以了
        //2:我们还可以选择hashMap的数据结构,用pageNo做key,每页的list作为值,这样下次我们直接
        //根据pageNo去取对应的值就行了
        //那我们就来实现第二种吧
        String key = "goodsList";
        String redisJson = JSONObject.toJSONString(RedisCache.hGet(key,pageNo));
        if (StringUtils.isNotBlank(redisJson)){
            //JSON格式字符串转list
        }else {
            //查询db
            //存入缓存
            RedisCache.hPut(key,pageNo,JSONObject.toJSONString(list));
            //设置过期时间
            RedisCache.expire(key,2L, TimeUnit.MINUTES);
        }
    }
    

    是不是瞬间感觉大同小异,也就那么回事吧。

  3. 然后我们再来看如何用Redis实现锁呢?

    用redis来实现锁又分两种情况,可以同时发生,先发生的先执行,后发生的等待执行。不可以同时发生,先发生的先执行,后发生的限制执行。

    //假如页面上有一个按钮,点一下修改一次数据,然后有个人操作的时候
    //由于网路抖动以为点了没反应,又用单身多年的手速连点了几十下。这几十下的请求全部
    //打到程序中不就是对这一条记录修改几十次,这时候可以使用SetNx直接阻塞请求
    String key = userId;
    //将当且仅当key不存在的时候,将key的值设置成1。key存在的时候设置失败
    Boolean flag = RedisCache.setNx(key,"1");
    //返回true代表设置成功,即拿到锁
    if (flag){
        //设置过期时间,比如只限定5S只能不能重复相同KEY不同重复请求
        //5S过后自动删除key,下一次可以继续操作
        RedisCache.expire(key,5, TimeUnit.SECONDS);
        try {
            //业务逻辑
        }finally {
            //更据业务选择操作完了是否要删除该KEY,
            //删除该key可以认为,下一次的请求是正常请求
            RedisCache.delete(key);
        }
    }
    
    /*针对那种可以同时发生的情况呢,最典型的扣减库存了,多个人同时下单,如何保证不会发生超卖呢
    RedissonLock基于redis的分布式锁实现,有点类似ReentrantLock可重入锁 */
    //建立redisson连接
    String goodsKey ="";
    //获取锁
    RLock rLock = redissonClient.getLock(goodsKey);
    try {
        //在try内部上锁,避免加锁过程中出现异常不能够被捕捉
        //高并发的情况下,先拿到锁的线程开始执行,其他线程开始自旋
        rLock.lock();
        //执行业务逻辑
    }finally {
        //finally里面主动释放锁,避免出现死锁的情况
        rLock.unlock();
    }
    

    会用起来,起码能干活了是不,但我们的目标不只是能干活啊,还得深入一点,才能再遇到问题的时候游刃有余。比如看看为什么加锁的时候lock.lock为什么要放在try代码块里面啊?lock.lock加锁的时候做了哪些操作啊?lock加锁多久,又是怎么进行锁续期的呢?这些也都是面试的时候有可能问到的高频问题哦。

  4. 秒杀?一看到这个慌不慌?高并发,大流量的情况才既要保证服务稳定运行,DB不会被打挂掉,又要保证公平,还要不能出现超卖的情况。这中间有任何环节发生错误,导致线上出现问题,就等着复盘吧。我太难了其他先不说,先用Redis来实现一个简单的队列先,其实也就是几行代码的事啦。

    //既然是秒杀,肯定离不开限流,架构方面先抛开不谈我们先看看从后端角度如何避免无效请求
    //首先,秒杀的商品数量一定不会太多,对这种大热商品我们一般提前将库存等数据预热到缓存
    //然后首先去Redis中扣减库存,Redis扣减成功再去操作DB
    //先获取库存
    String count = RedisCache.get(goodsKey);
    if (Integer.valueOf(count) <= 0){
        //已卖完直接返回
        return;
    }
    //增加(自增长), 负数则为自减
    /*redisTemplate.opsForValue().increment(key, increment);*/
    //否则减库存
    RedisCache.incrBy(goodsKey,-1L);
    try {
        //利用悲观锁操作DB扣减库存,走正常业务逻辑
    }catch (Exception e){
        //骚操作,业务没操作成功给Redis库存还回去。防止出现redis扣成功了
        // ,DB操作失败,最后redis扣完了,DB一个都没卖。。。。
        RedisCache.incrBy(goodsKey,1L);
    }
    
  5. 利用Redis实现队列,这里就简单介绍几条命令,用好了,有奇效

    //Redis的list有类似栈和队列一样的结构特性
    //栈:先进后出 队列:先进先出
    //比如我们有一个程序,接受线上大量请求,我们把请求的入参全部
    //放入一个共同的key里面去
    Object param = new Object();
    String key = "requestParamKey";
    //存储在list头部
    RedisCache.lLeftPush(key, JSONObject.toJSONString(param));
    
    
    //然后我们启动另一个程序,从这个list中一个个弹出参数对象来处理
    //从头部获取,就好像是栈,从左边进从左边出
    String leftJson = RedisCache.lLeftPop(key);
    if (StringUtils.isNotBlank(leftJson)){
        //处理业务
    }
    //从尾部部获取,就好像是队列,从左边进从右边出
    String rightJson = RedisCache.lLeftPop(key);
    if (StringUtils.isNotBlank(rightJson)){
        //处理业务
    }
    

以上简单列举了几种场景吧,虽然是一些伪代码,但是也还是能够从中学到一些使用Redis方面的大体的思路。小伙伴们也可以在此基础上发散思维,灵活变通,去应对更多更复杂的业务场景。

现在我们知道了什么事Redis,也了解了怎么用Redis,同时也应该要了解使用Redis过程中会遇到哪些问题呢?
  • 缓存雪崩

    使用缓存的目的是什么,就是为了避免大量的请求直接打到数据库上,造成数据库压力。数据库是持久化磁盘介质存储,一般我们都是将业务上能前置处理的尽量前置处理的,数据库作为最后一道防线作为兜底的操作。

    雪崩,就是本来缓存用的好好的,只有一些零散的请求打到DB,DB能够有条不紊的处理。但是突然某个时间点,缓存的key出现大面的失效情况,这时候所有的请求跳过缓存直接打到了DB上面。瞬间DB的使用量开始彪增,发出告警,大量表行等出现锁死的情况,DB一挂,外层服务直接歇菜,引起连锁反应,灾难就这么诞生了。

    怎么来处理呢?阿里巴巴的Redis使用规范上针对Key值有这样一项建议,针对每一个Key都要明确控制生命周期,Redis中不是垃圾桶,不能什么都往里面丢。也就是说,我们要根据业务的场景给每一个Key添加合适的过期时间,即使是相同条件下的Key我们也可以在大时间不变的情况下随机散列的设置一下秒级,甚至是毫秒级的时间参数。因为我们的目的很明确,就是为了避免同一时间大量的Key失效。

  • 缓存击穿

    缓存击穿跟缓存雪崩很类似,雪崩嘛就是大面积缓存失效,击穿就是某一个热点key失效。就好像正常情况下,我们key总是有过期时间的,既然有过期时间肯定就会在某一个时间点失效。一个承载着亿万流量的热点key,它失效的一瞬间,不就有大量的请求越过缓存直接到了DB,这下DB又双叒挂了。

    这种情况怎么办呢,结合我上一个月做的需求来说,给个提示:互斥锁+二重校验有奇效。

  • 缓存穿透

    说到缓存穿透,我想说,这TM不是我的锅呀。缓存穿透是指外界直接查询一个在缓存和DB中都不存在的数据,既穿透了缓存,也穿透了DB。举个例子我们基于Restful形式的接口经常会暴露一个get/put接口,他们的区别就不用我多说了。Get请求我们是能够直接在url里面看到请求的参数和值的,有心机的人,甚至可能是竞争对手啊等等,直接通过某种不可描述的手段用一个根本查不到的参数对你发起大量请求,对于这种请求,缓存中差不多到,直接打到DB,DB就有可能会慢慢的被一点点打死。

    所以,对于这种情况,我们对参数的有效性校验啊,权限认证啊各种限制级操作就需要做好,避免各种无效请求。另外还有一种有效的方法就是布隆过滤器了,布隆过滤器利用一种思想,就是你通过我过滤器的校验,校验通过的那就代表你要查的东西有可能存在,但是如果你通不过校验,那么你要查的东西一定不存在,那就直接返回好了。

  • 数据一致性

    数据一致性一直是我们比较关心的一个问题,因为缓存的时效性问题,有时候我们获取的数据并不一定是最新的数据。所以在设计初期,我们就要明确的知道,当前业务场景下能不能使用缓存,这些数据对实时性的要求有多少,最多能接受多久的数据不一致情况。就好比说,我商城首页的推荐商品啊多久刷新一次啊,用户是否真正关心实时的销量啊,某个商品的销量是不是卖一件就要告诉别人我卖了一件呢?

    其他大多数情况下我们并不关心,你去淘宝买东西会看你要买的东西卖了多少么,虽然销量代表一定问题,你能保证他给你看的就一定是真实销量嘛?你会定着不停的刷新页面,看看销量数字会不会一直变嘛?大部分情况下我们并不关心这些东西,所以结合实际业务场景,来判断业务下能容忍的数据不一致时间,给缓存设置一个合理的缓存时间就行了。

完结撒花

Redis十年之路,而且还在不断地演化进步,这么一点内容肯定是记录不完的。这些内容也是自己在使用过程中遇到不会的一点点去翻阅资料总结所得,虽然不是什么了不起的东西,但是也希望能够帮助你快速入门,上手实践。

同时,不排除日后对此内容的优化以及新增。如果有什么理解不到位的情况,也欢迎大家给我指出。

毕竟,一个人能够决定走多快,一群人才能够决定走多远。

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值