一、认识数据类型和编码方式
有序集合,相当于除了存储member之外,还需要存储一个score(权重,分数)
Redis底层在实现上述数据结构的时候,会在源码层面,针对上述实现进行特定的优化,来达到节省时间/节省空间的效果。特定的优化:内部具体实现的数据结构(编码方式),会有一定的变数。redis承诺,现在我这里有个hash表,你进行查询,插入,删除,操作都保证是O(1)。但是这个背后的实现,不一定就是一个标准的hash表。可能在特定场景下,使用别的数据结构实现,但是仍然保证时间复杂度符合承诺。
数据结构:redis承诺给你的,也可以理解成数据类型。
编码方式:redis内部底层的实现。
同一个数据类型,背后可能的编码实现方式是不同的,会根据特定场景进行优化。
raw:最基本的字符串。底层就是持有一个char数组,或者byte数组。
int:redis通常可以用来实现一些技术这样的功能。当value就是一个整数的时候,此时可能redis会使用int来保存。
embstr:针对短字符串进行的特殊优化。redis会自动适应,程序员在使用redis的时候没有感觉
hashtable:最基本的哈希表。redis内部的哈希表的实现。虽然和java实现方式可能不太一样,但是整体思想适合之前学过的一致的。
ziplist:压缩链表。在哈希表里面元素比较少的时候,可能优化成ziplist了。压缩列表,能够节省空间。为啥要压缩?redis上有很多的key,可能是某些key的value是hash,此时,如果key特别多,对应的hash也特别多,但是每个hash又不大的情况下,就尽量去压缩,压缩之后就可以让整体占用的内存更小了。
linkedlist:链表,元素个数多使用链表的结构实现。从redis3.2开始,引入了新的实现方式quicklist,同时兼顾了linkedlist和ziplist的优点。quicklist就是一个链表,每个元素又是一个ziplist,把空间和效率都折中兼顾到。quicklist比较类似于C++中的std::deque。
intset:集合中存的都是整数
skiplist:链表笔试题,有一个经典的题目,复杂链表的复制。跳表也是链表,不同于普通的链表,每个节点有多个指针域,巧妙的搭配这些指针域的指向,就可以做到,从跳表上查询元素的时间复杂度为O(logN)。
使用object encoding 查看key的value的实际的编码方式。redis会自动根据当前的实际情况选择内部的编码方式,自动适应的。我们记住思想就可以了。
二、redis的单线程模型
redis只使用一个线程,处理所有的命令请求,不是说一个redis服务器进程内部真的就只有一个线程,其实也有多个线程,多个线程是在处理网络IO。假设,有多个客户端,同时操作一个redis服务器:
在上面的场景中,两个线程尝试对一个变量进行自增操作表面上看是自增两次,实际上可能只自增了一次。
当前这两个客户端,也相当于并发的发起了上述的请求,幸运的是并不会发生线程安全问题,redis服务器实际上是单线程模型,保证了当前收到的多个请求是串行执行的!多个请求同时到达redis服务器,也是要先在队列中排队,再等待redis服务器一个一个的取出里面的命令再执行。微观上讲,redis服务器是串行/顺序执行多个命令的。所以不会生成线程安全问题。
redis能够使用单线程模型,很好的工作原因主要在于redis的核心业务逻辑都是短平快的,不太消耗cpu资源也就不太吃多核了。
有很多的弊端:redis必须要特别小心,某个操作占用时间长,就会阻塞其他命令的执行!
redis虽然是单线程模型,为啥效率这么高,速度这么快呢?【重要的面试题】
参照物是数据库。
1、redis访问内存,数据库访问硬盘。
2、redis核心功能,比数据库的核心功能更简单。数据库对于数据的插入删除查询,都有更复杂的支持,这样的功能势必要花费更多的开销。redis干的活少,提供的功能相比于mysql也是少了不少。
3、单线程模型:避免了一些不必要的线程竞争开销。redis每个基本操作,都是短平快的就是简单操作一下内存数据,不是什么特别消耗cpu的操作。就算搞多个线程,提升不大。
4、处理网络IO的时候,使用了epoll这样的IO多路复用机制。一个线程,就可以管理多个socket,针对TCP来说,服务器这边每次要服务一个客户端,都需要给这个客户端安排一个socket,一个服务器服务多个客户端,同时就有很多个socket,这些socket上都是无时不刻的都在传输数据吗?很多情况下,每个客户端和服务器之间的通信也没那么频繁。此时这么多socket大部分事件都是静默的。上面是没有数据需要传输的。所以,同一时刻只有少数socket是活跃的。
以下谈到的数据结构类型都是谈value的类型的
三、Redis中的string
Redis中的字符串,直接就是按照二进制数据的方式存储的。不会做任何的编码转换,存的是啥,取出来还是啥。讲mysql的时候,知道mysql默认的字符集,是拉丁文。插入中文就会失败,Redis就没有这种机制,存的啥就是啥。一般来说,redis遇到乱码问题的概率更小。不仅仅可以存储文本数据,整数,普通的文本字符串,JSON,xml,二进制数据(图片,视频,音频……)。音频视频体积可能会比较大,Redis对于strin类型,限制了大小最大是512M,Redis是单线程模型,希望进行的操作都能比较快速。
string类型的命令学习:
set:
set key value ex 10 表示 set key value, expire key 10。执行一个命令的原子性很强。
NX 如果key不存在,才设置,如果key存在,则不设置返回nil
XX如果key存在才设置相当于更新key的value。如果key不存在,则不设置返回nil
redis文档给出的语法格式说明
[ ]相当于一个独立的单元,表示可选项(可有可无的),其中 | 表示或者的意思,多个只能出现一个。[ ]和[ ]之间是可以同时存在的
也提供了一些命令来设置NX或者XX
SETNX
SETEX
SETPX
如果key不存在,创建新的键值对。如果key存在,则是让新的value覆盖旧的value,可能会改变原来的数据类型。原来这个key的ttl(生存时间)也会失效。
一个快速失去年终奖的技巧,清楚redis上所有的数据-删库 =》mysql里的drop database。
FLUSHALL可以把reids上所有的键值对都删除。以后在公司这是一个非常危险的命令,你少个几千甚至几万块钱你应该是不愿意的。当然不排除一些富二代。但是你不难受,公司就难受了。
get:和通用命令的get相同。对于GET来说,只是支持,字符串类型的value如果value是其他类型,使用GET获取就会出错!
MSET和MGET 一次操作多组键值对
事件复杂度为O(N),此处的N不是整个redis服务器中所有key的数量,而只是当前命令中给出的key的数量。
,
虽然可以一次操作多组键值对,但是不能设置太多,有可能把redis给阻塞住了。
SETNX:不存在才能设置,存在则返回失败
SETEX:指定一个key的生存时间,单位second
PSETEX:和上一个一样,但是单位是毫秒
针对set的一些常见用法,进行了缩写。之所以这样设计,就是为了让操作更符合人的直觉(使用者的门槛就越低,要背的东西就越少)。符合人的直觉,编程语言中,很多的关键字,都是和自然语言相关的。后续我们去设计一些库,设计一些工具,代码给别人使用的时候,也要尽量符合直觉,不要设计反人类,反直觉。
INCR 针对value + 1 语法:INCR key。此时key对应的value必得是整数,此操作的返回值,就是+1之后的值。incr操作的key如果不存在,就会把这个key的value当做0来使用。以下命令都是这样的操作。
INCRBY 针对value + n 加负数也是可以的。
DECR 针对value -1 返回值也是计算后的值。
DECRBY 针对value - n
INCRBYFLOAT 针对value +/-小数 ,把key对应的饿value进行+ -运算,运算的操作数可以是浮点数,虽然此处没有提供减法版本的命令,但是使用redis进行的计数操作,一般都是针对整数来进行的。
上述操作的时间复杂度都是O(1)的。由于redis处理命令的时候,是单线程模型,多个客户端同时针对同一个key进行incr操作,不会引起 "线程安全" 问题。
字符串也支持一些常用的操作,拼接,获取/修改字符串的部分内容,获取字符串长度。
append 如果key已经存在并且是一个string,命令会将value追加到原有string的后边
append返回值,长度的单位是字节!redis的字符串不会对字符编码做任何处理,当前xshell终端,默认的字符变编码是utf8,在终端中输入汉字之后,也就是按照utf8编码的,一个汉字再utf8字符集中,通常是3个字节的。
在启动redis客户端的时候,加上--raw这样的选项。就可以使redis客户端能够自动的把二进制数据尝试翻译。
getrange 获取子串 是一个闭区间。C++和JAVA中,谈到一个区间,大多都是前闭后开。 正常下标都是从0开始的整数,redis的下标使可以支持负数的。-1倒数第一个元素。
setrange 切出的字串进行修改 offset偏移量,从第几个字节开始进行替换。替换多长看实际value的长度。返回值是替换之后,新的字符串的长度。如果当前咱们value是一个中文字符串,进行setrange的时候,是可能有问题的。会导致编码失败。针对不存在的key凭空生成了一个字节,这个字节里的内容就是0x00,aaa就被追加到0x00后面了。setrange针对不存在的key也是可以操作的,不过会把offset之前的内容填充成0x00。
strlen 获取到字符串的长度,单位是字节。C++中,字符串的长度本身就是用字节为单位,java中字符串的长度则是以字符为单位的,MySQL中varchar(char)此处N的单位就是字符,MySQL中的字符,也是完整的汉字,这样的一个字符,也可能是多个字节。
,当key的value不是string时会报错。
string命令总结
string的编码方式
int :8个字节的长整型
embstr :小于等于39个字节的字符串
raw:大于39个字节的字符串
通过object encoding key查看编码格式。不建议大家去记,长度大于39这样的数字。某个业务场景,有很多的key,类型都是string,但是每个value的string长度都是100左右,更关注于整体的内存空间,因此这样的字符串使用embstr来存储也不是不能考虑,上述效果具体怎么实现?
1、先看redis是否提供了对应的配置项,可以修改39这个数字。
2、如果没有提供配置型,就需要针对redis源码进行魔改。
reids存储小数,本质上还是当作字符串来存储,这就和整数相比差别很大了,整数直接使用int来存。小数则是使用字符串来存,意味着每次进行算数运算,都需要把字符串转换成小数,进行运算,结果再转会字符串进行保存。
string的应用场景
缓存功能
整体的思路:应用服务器访问数据的时候,先查询Redis,如果Redis上数据存在了,就直接从Redis取数据交给应用服务器,不继续访问数据库了。如果Redis上数据不存在,再读取MySQL把读到的结果,返回给应用服务器,同时,把这个数据也写入到Redis中。Redis这样的缓存,经常用来存储热点数据,高频被使用的数据,这个定义方式,结合业务场景时有很多种方式。刚才上述描述的过程,相当于是把最近使用到的数据作为热点数据。暗含了一层假设,某个数据一旦被用到了,那么很可能在最近这段时间就会被反复用到。
上述策略存在一个明显的问题:
随着时间的推移,肯定会有越来越多的key在redis上访问不到,从而从mysql中读取写入redis了。此时redis中的数据会越来越多。针对这种情况我们会有一定的解决方法。
1)把数据写给redis的同时,给这个key设置一个过期时间。
2)Redis也在内存不足的时候,提供了淘汰策略。详细内容后面再说。
所以我们不用太担心上述的问题。
计数(Counter)功能
企业为啥乐意收集用户的数据?
统计 =》 进一步明确用户的需求=》根据需求改进和迭代产品。Redis并不擅长数据统计。比如想在上述的Redis中,统计,播放量前100的视频有哪个 --- 基于Redis搞就很麻烦。相比之下,如果是mysql来存储上述数据,一个sql就搞定了。
异步方式:这里写入统计数据仓库的步骤。异步的,不是说,来一个播放请求,这里就必须立即马上写一个数据。
实际中要开发一个成熟、稳定的真实计数系统,要面临的挑战远不止如此简单:防作弊,按照不同维度计数,避免单点问题,数据持久化到底层数据源等。
共享会话
Session会话
Cookie浏览器存储数据的机制,Session服务器这边存储数据的机制,都是键值对的形式出现的。
如果每个应用服务器,维护自己的会话数据,此时彼此之间不共享。用户请求访问到不同的服务器上,就可能会出现一些不能正确处理的情况。
此时所有的会话数据,都被各个服务器共享了。
短信验证码:
手机验证码:
1、生成验证码,用户输入以下手机号,点击获取验证码,限制1分钟之内,最多获取5次验证码。用户每次获取验证码必须间隔30秒
2、检查验证码。把短信收到的验证码这一串数,提交到系统中,系统进行验证,验证码是否正确。
像发送短信这样的操作,都是由专门的SDN来实现的(第三方平台来提供服务的),但是需要充值使用。
业务是什么?
业务其实就是一个公司/一个产品,是如何解决一个/一系列问题的。解决问题的过程就成为是业务。一个公司/产品要想生存,就得赚钱,要想赚钱,就得能帮别人解决问题。