redis的底层使用的是c++
java如何跨语言调用redis?
Clients客户端概念。redis的java客户端有哪些?
Jedis/jredis/rjc/jedisplus/redisclient redis官方首选Jedis,官方唯一推荐,特点:支持redis cluster(redis3.0后的集群)
Jedis jedis = new Jedis("localhost", 6379);
jedis.set("zdd", "fly2021");
System.out.println(jedis.get("zdd"));
redis的技术本质:
客户端调用服务端(网络通讯的概念)java--网络通讯--c++
客户端:
1.transfer传输层 tcp/ip三次握手,安全性,java的实现有socket长链接,http短连接,这里使用的是socket长链接。
2.message protocol 消息协议层 链接建立后
3.api 操作层
QPS百万级redis:
transfer拆解:
消息协议层:暴力获取redis协议
协议如下:
*3 --数组3
$3 --字符3
SET
$3 --字符3
zdd
$7 --字符7
fly2021
官网文档Redis Protocol sepicification 的规范:RESP是我们客户端调用服务端的一个通讯协议。
Resp:
用单行回复,回复的第一个字节将是“+”
错误消息,回复的第一个字节将是“-”
整型数字,回复的第一个字节将是“:”
批量回复,回复的第一个字节将是“$” 字符串
多个批量回复,回复的第一个字节将是“*” --数组
每一个命令的结束都是换行
+ok表示set成功
Redis原理:
常识:
磁盘:1.寻址 ms级 2带宽:G/M
内存:1.寻址 ns 2.带宽:很大
磁盘比内存寻址慢了10万倍。
I/O buffer:成本问题
磁盘与磁道有扇区,一扇区512Byte。如果一个区域足够小,会造成成本变大。索引成本。
格式化磁盘时,可以使用4k,操作系统无论读多少,都是最小4k
随着文件变大,速度变慢:访问硬盘成为瓶颈即 IO成为瓶颈
因此后面的问题都是为了解决磁盘的瓶颈。
然后到数据库,数据库的数据存储使用data page,每个4k,定义小会影响性能,定义更大没有影响,查找时一个个4k遍历
建立索引:data page 4k
关系型数据库建表时:必须给出schema,即给出表的每一列的类型,字节宽度。存时倾向于行级存储。
数据和索引都是存储在磁盘的。
在内存内准备b+tree
索引和数据都存在磁盘。
内存内只存树干。这样只需要在内存中找到索引,并获取索引命中的数据放入内存,因此获取速度极快。 大大降低IO次数及寻址次数
如果数据表内数据很大,性能会降低?
:如果表有索引,增删改变慢。查询速度1.如果一个或少量查询,依然很快。2.并发大的时候会受磁盘宽带影响速度(每查一批数据放入到内存一次)
极端:SAP公司的HANA内存级别的关系型数据库。(数据在磁盘和内存体积不一样,内存内无需指针)
因此折中方案为:缓存。
基础常识:1.冯诺依曼体系的硬件 2.以太网 tcp/ip的网络
https://db-engines.com/en/ 数据库网站。 架构师:技术选型,技术对比
redis.io
扩展:memcached,redis取代了其。因为mc的value没有类型概念。
json 可以表示很复杂的数据结构。
世界上3中数据表示:
1.k = a k =1
2.k= [1,2,3] k=[a,x,f]
3.k={x=y} k=[{}, {}]
客户端如果想从缓存kv中取出value中的某一个元素。
memcahce需要返回所有的value到client,server 网卡IO,client端要有实现的代码去解码
redis,类型不是很重要,redisserver对每种类型都有自己的方法。 index(), lpop
根据(计算是向数据移动的):memcache的计算是放在客户端client去解析json的。而redis是直接在redisserver就完成计算的。
安装linux版本redis
...
redis是单进程,单线程,单实例,并发很多的请求,如何变得很快的呢?
多个客户端请求,通过tcp到达linux内核kernel。会有很多的socket。
所有的操作请求都是有顺序的,顺序处理。每连接内的命令顺序
内核与redis之间使用的同步非阻塞多路复用epoll。来遍历连接,谁有数据处理谁。
epoll的数据驱动并不是真正的数据驱动。
epoll插曲:
服务器内核kernel ----连接----客户端。所有连接先到达内核。每个连接存在标识符。服务器通过一个进程通过一个read(read是用户行为,在内核执行)从内核去读每一个标识符fd。因为socket在这个时期是阻塞的blocking。因此多个请求需要计算机开多个线程通过每一个的read去读一个标识符。即BIO。 一个线程的成本 1MB,可调节。1.CPU只有一颗,线程多了,调度成本cpu浪费;2.线程的内存成本。
任何进程都会有他的io标识符。
内核进化后:
内核中的socket可以是非阻塞的。文件标识符是nonblock。一个线程一个read遍历所有的fd,无数据下一个,有数据则处理数据(轮询发生在用户空间)。 同步非阻塞时期。 NIO,如果有1000个fd,代表用户进程轮询调用1000次内核,存在成本问题。
内核发展:
内核内增加一个系统调用select,服务器线程通过select调用内核内的select(用户行为,内核执行),只获取有无数据状态返回给计算机,服务器再通过NIO去遍历读取所有有数据的fd,减少调用内核次数(轮询发生在用户空间)。 多路复用的NIO。 线程/进程的select与read均发生在jvm内,jvm由c++
多路复用NIO的问题:
fd数据内核态<-->用户态的互相转换,由于在jvm内属于不同的内存区域,需要fd相关数据传参传递,即互相拷贝。
如果存在一个共享空间,通过内核的mmap实现。共享空间是jvm的一部分,也是服务器内核的一部分。
共享空间的增删改的操作在内核完成,查询是内核和jvm都可以查
共享空间内:红黑树,链表
用户空间有1000个fd,fd全部写入共享空间的红黑树,就不需要在jvm的私有地址内存放。内核就可以直接从这个空间内看到有多个文件标识符。有数据的都放到链表内,再由read来读取链表,不需要fd数据互相在两个空间拷贝。epoll 非阻塞多路复用NIO
jvm线程通过create epfd调用epoll的mmap创建共享空间,ctl遍历所有的fd数据放入到共享空间,再通过线程的增删改查命令,调用内核的相应的增删改查方法操作共享空间内的fd数据,返回给线程后,线程再调用内核执行read的NIO读取操作,遍历读取共享空间内的fd数据。
多路IO复用技术
另外多路IO复用技术类似于拨开关,多个任务通过拨开关的方式共用一条线程,哪个任务需要了开关就拨到哪个任务,避免了CPU在不同的线程中切换,提高效率。
I/O复用模型
1.select/poll
老李去火车站买票,委托黄牛,然后每隔6小时电话黄牛询问,黄牛三天内买到票,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,黄牛手续费100元,打电话17次
2.epoll
老李去火车站买票,委托黄牛,黄牛买到后即通知老李去领,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,黄牛手续费100元,无需打电话
redis的使用
redis默认16个库,可以通过 -n 库number来设置库,默认为0号库
select 8 表示选择9号库
help @<xxx> 可以查看操作帮助哦
type key 获取value类型
Object encoding key 返回用于存储与键关联的值的内部表示的类型。
String类型:
String类型分为三种:字符串,数值,bitmap(位图,二进制运算很重要))
字符串类型
set key value nx 分布式锁的set命令,不存在key才能新增
set key value xx 存在key才能修改
mget key1 key2 同时取多个key
apend key1 "a" 追加字符
getRange key1 6 "a" 偏移量截取value, 偏移量使用正反向索引
setrange key1 6 "a" 从第六个字符修改为a
strlen key获取value长度
数值类型
数值计算:incr key 增1 incrby key 2 增2 incrByfloat 加小数 decr 减与加同操作, decrby 自定义减操作
数值计算可应用于抢购秒杀,详情页下规避并发情况对数据库的事务操作
注意:redis基于二进制安全,只取字节流。redis -cli --raw 如果没有--raw启动使用redis,redis只能识别ASC码超出ASC码就按十六进制显示。带--raw可以触发编码集的格式化。
长度是不会变化的,key3的utf8依然长度为3,key2为2.
redis底层按字节存储,key做了编码优化。redis本身是没有数据类型的。
getset方法一次连接两个操作,减少连接次数。
msetNx,不存在key才批量set,有一个key存在,则该笔操作所有key都失败。
select与epoll的对比:
bitmap类型:
help setbin
setbit:同时存在字节索引和二进制位
例:k1的value只有一个字节,二进制位为8个
setbit k1 1 1 表示将k1的value的二进制位为1的位置,改为1 asci转码为@
setbit k1 7 1 长度仍然是1,没有超出1个字节 asci转码为A
setbit k1 9 1 超出一个字节 asci转码为A@ (因为超出一个字节长度后,会开辟一个字节为00000000,然后修改二进制位9为1)
字符集标准为ascii,其它为扩展字符集。扩展:其它字符集不再对ascii重编码。字节流以0开头,为ascii码,其它的需要取两个或者三个才能确定编码类型。
bitpos:寻找第一个二进制位
bitpos k1 1 0 0 寻找二进制1,从字节索引0到0中寻找
bitpos k1 1 1 1 从字节索引1到1中寻找二进制数1 返回的是二进制1在整个字符中的二进制位位置。
bitcount :统计二进制1出现的次数
bitcount k1 0 1 在字节索引0到1中统计二进制1的次数
bitop:二进制运算
bitop and k3 k1 k2 对k1和k2的二进制数进行与运算,结构存入k3
同样还有or 或操作。
位图应用场景:
1.统计用户登录天数,且窗口随机(时间范围随机,如十一前后一周等等)。
懒做法:数据库记录用户登录记录。
通过数据库统计的方式,浪费内存空间,且查询效率低。
每一天对应一个二进制位,一年按400个二进制位。400/8 = 50个字节。50个字节可以最大情况记录用户全年365天的登录状态。
setbit zhangsan 1 1 表示个张三第二天登录了
setbit zhangsan 7 1 第八天登录
setbit zhangsan 364 1
Strlen zhangsan = 46个字节 这样全年的登录情况都以46个字节存储在redis中。
一个字节代表8天,想知道用户十一前后一周有没有登录。只需要统计这个字节索引范围内的1的数量,即 bitcount zhangsan (365减去十一在该年的天数-7)/8 (365减去十一在该年的天数+7)/8
高效,且节省内存1000万用户一年的登录记录内存为400+M。
2.假如京东618做活动,凡是登录,就会送礼物。需求:假设有两亿用户,库存至少需要多少货物?
用户分为僵尸用户,冷热用户/忠诚用户
关键在于活跃用户统计。
活跃用户统计,随机窗口:如 1~2号活跃用户统计
setbit 20210701 1 1 ---A用户一号登录
setbit 20210702 1 1 ----A用户2号再次登录
setbit 20210702 7 1 ----B用户2号登录
bitop or countkey 20210701 20210702 给这两天做与运算,有一为一。
再统计所有的二进制1.
bitcount countkey 0 -1 就可以得到去重的活跃用户数
List,Set,Zset,Map使用略(各种存储类型的应用场景)
list场景:消息队列,阻塞队列,栈。
Set:统计交集,如微信朋友圈点赞人展示。A用户和B用户看到的朋友交集。
随机数抽奖。
Zset:
Map:
ZSet是怎么排序的?增删改查的速度
skipList跳跃表:
Redis的NIO
Redis的发布订阅的使用:
publish xxxx hello 向通道内推送消息
subscribe xxxx 需要开启该监控才能看到推送的消息
redis事务:
redis的事务无法回滚。
redis的模块儿扩展:布隆过滤器:用于解决缓存穿透。
布隆过滤器:bf 命令
作用:当缓存穿透时,所有请求都压到数据库,会给数据库造成压力。
每个商品用bitmap来标记,通过多个算法计算出商品的位置,在bitmap对应位置上设置为1.
查询时,多个算法计算位置,只有全部位置的bitmap值为1,才确认商品存在。
注意:概率解决问题,无论函数有多少,都无法解决函数碰撞的问题。1%的误差概率
实现方式:客户端实现布隆算法,自己承载bitmap,redis只做缓存
2.客户端只有布隆算法。redis维护bitmap
3.客户端什么都不做,redis来加载布隆算法和维护bitmap
其它模块:counting bloom, cukcoo过滤器,布谷鸟过滤器。
缓存与数据库有什么区别?
1.缓存数据不重要。
2.缓存不是全量数据
3.缓存应随着访问而变化
4.存储的为热数据
redis做为缓存时的使用的问题:
缓存应随着访问而变化。
保存热数据,内存是瓶颈。
1.key的有效期。
设置有效期,set key value ex 有效期s 查看有效期时间, ttl key
倒计时 expire
定时
注意:不设置过期时间,通过expire来设置倒计时,再更新key时,会取消倒计时。
到时清理有两种方式:被动和主动
被动清理:到期不会自动清除,在用户下一次调用该key时清除。
主动清理:随机确认20个key,判断是否超过25%的key过期,如果达到25%,则遍历所有key进行过期清理
2.内存满了后,的回收策略。LRU算法多久没有使用,LFU最少使用次数
Redis的内存持久化
RDB:快照,保存的是二进制文件,恢复比较快。
存在时点问题。
1.redis在执行RDB时,redis通过fork()(系统调用),开辟一个redis数据关系(指向数据的指针)的子进程(不会复制数据),通过copyOnWrite内存机制,持久化到磁盘,写入的是子进程数据,不会产生时点冲突。父进程对数据的修改,子进程看不到,也不会改该子进程的数据指向指针。
方式:
1.save 阻塞方式 (关机维护时可以用save)
2.bgsave: fork的异步非阻塞
3.配置文件中给出bgsave的规则:如下图
持久化地址:dir
弊端:不支持拉链,永远只有一个dump.db.
丢失数据问题:redis宕机会丢失到上一次rdb的数据。
优点:类须于java中的序列化,恢复的速度相对快
补充知识点:
LINUX管道:
1.衔接上一个命令的输出做为后一个命令的输入
2.管道会触发创建子进程, |前后两个子进程
Redis的fork():
使用linux的时候,父子进程。
正常情况下:父进程的数据,子进程看不到。
通过export,可以让子进程看到父进程的数据。
通过导出环境变量的方式,子进程的修改不会影响父进程的数据。父进程的修改也不会破坏子进程。
创建子进程的速度两个因素:
redis父进程,内存数据10G。
1.速度
2.内存空间
fork(): 速度快,内存小。
redis内存在虚拟地址,计算机存在所有进程共用的物理内存。
redis的所有key,都会存映射到物理内存的地址。
copyonwrite 写时复制
创建子进程,并不发生复制。创建进程变快了。
AOF:redis的写操作记录到文件中(指令日志)
优点:丢失数据少
redis中rdb和aof可以同时开启,如果开启了aof,只会用aof恢复。
4.0版本以后:AOF中包含RDB全量,增加记录新的写操作。
场景:redis运行了10年,redis挂掉,AOF多大,恢复要多久?如果只有两个k重复增删10年。
AOF数据量10T。如果恢复的话,可能要5年左右。
AOF缺点:
1.体量无限变大,恢复慢 可以通过设计一个方案让日志或AOF足够小。也可以通过hdfs或其它方式。4.0以前:重写,删除抵消的命令,合并重复的命令。最终也是一个纯指令的文件。
4.0以后:重写:将老的数据RDB到AOF文件中将增量的以指令的方式Append到AOF
redis内存写数据库会触发IO:可以调整三种写io的级别:NO,Always, 每秒。
Redis的AFK原理:
单机redis单进程,持久化通过RDB或AOF。
单机,单节点,单实例的问题:
1.单点故障
2.容量有限
3.连接压力
AFK:
x轴主备:全量镜像的 ,可以解决单点故障的问题。 ----主从
Y轴:数据分开。把不同的业务分到不同的实例。 ----分片
Z轴:按照优先级,逻辑再拆分,把不同的数据分到不同的z轴实例
X轴问题:主备模式,备份到备机,存在数据一致性问题!
强一致性会破坏redis的可用性问题。
如果容忍数据丢失一部分,可以采用异步非阻塞方式:
3.可以通过消息队列实现主备复制:最终一致性
在这种模式中,当主机中的数据发生改变后,会阻塞式的将修改数据的消息传递给一个消息中间件。然后马上将确认消息回复给客户端。
集群中的其他备机会消化消息队列中的消息。
这种方法可以快速的给予客户端响应,集群也会在未来的某一刻达到数据一致性,我们称之为最终一致性。
注意:最终一致性方案,在消息队列未完成备机消费情况下,主机挂掉,从备机取得数据会得到不一致的数据。
CAP理论与哨兵模式:
C:一致性
P:分区容错性
A:可用性
CAP理论:三个性质不能同时满足。
主备:客户端只能访问主机。
主从:客户端可以访问从机。
如何保证高可用:通过redis主从,可以通过手动来将从机设置为主机redis。
监控程序:哨兵模式。哨兵也是个集群,跟据CAP理论
为避免产生脑裂,设置为n/2 + 1太的势力范围来进行投票,保障了可用性和分区容错性,即AP理论。(单机redis采用CP模式)
Redis分片Sharding
主从复制,解决了redis的可用性问题。
跟据AKF,Y轴,存在容量问题!
解决方案:
数据角度:业务可以区分的话,客户端来决定操作不同的redis实例
数据无法拆分的角度:
1.算法区分 + 取模。即redis分片, sharding 客户端来操作。 弊端:取模的数必须固定,影响分布式下的扩展性。如果加redis服务器,需要重新设置取模数。
2.random区分。随机 (场景:发布订阅模式的消息队列,lpush进去list,客户端只需要rpop)
3.一致性hash算法:hash映射算法 CRC16位,CRC32位,FNV, Md5。
没有取模的过程,hash环
一致性hash算法优点:加节点可以分担其它节点的压力,不会造成全局洗牌。
缺点:新增节点会造成一小部分数据不能命中,隐藏缓存击穿问题,给mysql增加压力(只能作为缓存使用)
方案:取后两个物理节点。
上面虽然解决了Y轴容量分片问题,但是不能作为数据库使用。
使用redis预分区,算法取模,多取模一点。然后在redis实例中每个实例映射多个hash值,增加一台实例,将之前的实例映射内的取模值以及数据一并分一些给新的redis实例
redis集群
redis集群内没有主从概念,所有的redis节点,都存在自己的算法取模范围和别的节点的取模范围,请求只需要进入其中任一个节点,就会定位到数据存在于哪个节点。
缺点:聚合操作很难实现事务。
hashtag,通过给不同的key设置为hash可计算出同样的数据来强行放到一个节点。
缓存击穿:某个key失效,大量访问进来
解决方案:采用setNx锁,并且设置过期时间,加上续命。第一个请求进来获取不到key,加锁(其它请求循环get(key)),然后通过数据库获取数据后,更新到redis后,释放锁,其它请求再从redis访问。
缓存穿透:key不存在,直接访问客户端
解决方案:1.布隆过滤器 2.设置key值为空
缓存雪崩:大量key同时失效,大量访问访问db
解决方案:1.对于无关时间点的数据,设置随机失效时间,避免同时失效
2.对于必须失效的数据,采用缓存击穿的方式来做。