框架和中间件(MyBatisRedisRocketMQ)

Posted Zephyr丶J

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了框架和中间件(MyBatisRedisRocketMQ)相关的知识,希望对你有一定的参考价值。

MyBatis

MyBatis访问数据库,有很多数据库有关的配置,底层调用JDBC,可以理解为对JDBC更高级的封装
配置信息封装到Configuration
运行过程中,实时读配置,从内存中读
所以重新启动的时候,需要把配置信息从配置文件里读取,再读到内存中,再封装到对象里

写sql的时候,可以写到注解里,也可以写到配置文件里

核心组件SqlSession,提供了对数据库CRUD的API,底层封装了数据库的连接,能过实现对数据库的增删改查,是一个入口

Executor是具体的实现,执行sql
StatementHandler ,相当于sql处理器,在执行sql的时候,要传参,就调用ParameterHandler,如果返回值,就调用ResultSetHandler

MyBatis交互的对象是JDBC,从数据库中得到的是数据库的对象,在MyBatis层面,要转换成java对象,这里都依赖类型转换,工具是TypeHandler

还有一个东西叫做SqlSessionFactory,SqlSession是通过工厂创建的,但是最终干活的是SqlSession

Executor底层通常要对sql进行预编译,预编译可以防止注入非法的参数,导致sql语句被破坏

补充:
1.Statement、PreparedStatement和CallableStatement都是接口(interface)。
2.Statement继承自Wrapper、PreparedStatement继承自Statement、CallableStatement继承自PreparedStatement。
3.
Statement接口提供了执行语句和获取结果的基本方法;
PreparedStatement接口添加了处理 IN 参数的方法;
CallableStatement接口添加了处理 OUT 参数的方法。
4.
a.Statement:
普通的不带参的查询SQL;支持批量更新,批量删除;
b.PreparedStatement:
可变参数的SQL,编译一次,执行多次,效率高;
安全性好,有效防止Sql注入等问题;
支持批量更新,批量删除;
c.CallableStatement:
继承自PreparedStatement,支持带参数的SQL操作;
支持调用存储过程,提供了对输出和输入/输出参数(INOUT)的支持;

Statement每次执行sql语句,数据库都要执行sql语句的编译 ,
最好用于仅执行一次查询并返回结果的情形,效率高于PreparedStatement。

PreparedStatement是预编译的,使用PreparedStatement有几个好处

  1. 在执行可变参数的一条SQL时,PreparedStatement比Statement的效率高,因为DBMS预编译一条SQL当然会比多次编译一条SQL的效率要高。
  2. 安全性好,有效防止Sql注入等问题。
  3. 对于多次重复执行的语句,使用PreparedStament效率会更高一点,并且在这种情况下也比较适合使用batch;
  4. 代码的可读性和可维护性。

JDBC statement中的PreparedStatement的占位符对应着即将与之对应当值,并且一个占位符只能对应一个值,如果能对应多个就会引起混淆。sql语句是确定的,那么一个占位符必定只能对应一个值

Redis

Key永远是String,Value可以是不同的类型
String是用的最多的

为什么要设计一个类型对应多个编码方式
需要考虑:1.效率问题,访问这个类型效率要高,吞吐量要大
2.要节约内存
没有一种结构是同时满足这两条的,所以数据量小的时候要用一种结构,大的时候另一种,以达到一个平衡

元数据是描述字符串特征的数据,例如长度

list在3.2版本以后将前两个编码实现去掉了,作废了,只剩下一个快速列表

补充:

1.String:
(这个数据结构指的是value中的类型,不是指key)
String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.

如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。

2.List的数据结构为快速链表quickList。
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。
它将所有的元素紧挨着一起存储,分配的是一块连续的内存。
当数据量比较多的时候才会改成quicklist。
因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。

Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

3.Set数据结构是dict字典,字典是用哈希表实现的。
Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。

4.Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。

5.SortedSet(zset)是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map<String, Double>,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。

zset底层使用了两个数据结构:
(1)hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。
(2)跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。

跳跃表(跳表)

1、简介
有序集合在生活中比较常见,例如根据成绩对学生排名,根据得分对玩家排名等。对于有序集合的底层实现,可以用数组、平衡树、链表等。数组不便元素的插入、删除;平衡树或红黑树虽然效率高但结构复杂;链表查询需要遍历所有效率低。Redis采用的是跳跃表。跳跃表效率堪比红黑树,实现远比红黑树简单。
跳表全称为跳跃列表,它允许快速查询,插入和删除一个有序连续元素的数据链表。跳跃列表的平均查找和插入时间复杂度都是O(logn)。快速查询是通过维护一个多层次的链表,且每一层链表中的元素是前一层链表元素的子集。一开始时,算法在最稀疏的层次进行搜索,直至需要查找的元素在该层两个相邻的元素中间。这时,算法将跳转到下一个层次,重复刚才的搜索,直到找到需要查找的元素为止。
2、实例
对比有序链表和跳跃表,从链表中查询出51
(1)有序链表

要查找值为51的元素,需要从第一个元素开始依次查找、比较才能找到。共需要6次比较。
(2)跳跃表

从第2层开始,1节点比51节点小,向后比较。
21节点比51节点小,继续向后比较,后面就是NULL了,所以从21节点向下到第1层
在第1层,41节点比51节点小,继续向后,61节点比51节点大,所以从41向下
在第0层,51节点为要查找的节点,节点被找到,共查找4次。

从此可以看出跳跃表比有序链表效率要高

插入节点路程:
1.新节点和各层索引节点逐一比较,确定原链表的插入位置。O(logN)
2.把索引插入到原链表。O(1)
3.利用抛硬币的随机方式,决定新节点是否提升为上一级索引。结果为“正”则提升并继续抛硬币,结果为“负”则停止。O(logN)
总体上,跳跃表插入操作的时间复杂度是O(logN),而这种数据结构所占空间是2N,既空间复杂度是 O(N)。

删除节点流程:
1.自上而下,查找第一次出现节点的索引,并逐层找到每一层对应的节点。O(logN)
2.删除每一层查找到的节点,如果该层只剩下1个节点,删除整个一层(原链表除外)。O(logN)
总体上,跳跃表删除操作的时间复杂度是O(logN)。

线程模型

在内存上操作也不应该写一些非常耗时的命令,如keys *

支持并发,同一时刻产生多个socket,但是不是每一个都会就绪
例如有一万个socket接入,但是只有100个是就绪的,这个就绪指的是连接、关闭、写入、读取这种状态的就绪
IO多路复用程序就是一个程序可以监控多个文件描述符,或者说是socket描述符
一旦就绪,就将socket丢入socket队列里,
这里就是典型的生产者消费者模式,文件时间分派器去消费这个队列,拿到队列的socket
判断这个socket激活的是哪种状态,不同的状态用不同的命令请求处理器处理

所以单线程是说多路复用这里单线程,处理的时候是多线程的

持久化机制

redis支持持久化,是一个典型的特征
RDB(Redis DataBase)
在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里
每次将数据都搬入硬盘,是比较耗时的,会有较长的阻塞,不能解决实时性持久化问题

AOF(Append Only File)
以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

redis什么时候会被阻塞?

Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。
如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。

Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术
一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

bgsave执行的时候先会判断是否有子进程,如果有,说明可能刚刚执行了持久化,就直接返回;如果没有,父进程就要创建子进程
用fork创建子进程的过程里,父进程会被阻塞;创建出来以后,解除阻塞,继续响应其他命令;
子进程开始存储父进程产生的数据。

在持久化的过程中,可能父进程也在修改数据;那么如何保证不冲突呢?
这里用了一个写时复制技术,内存分为多页,假设当前子进程正好读取到了红色page页的数据,要拷贝这个数据;如果此时恰好父进程也要修改这个页的数据,那么就将这页数据复制一个副本,去改这个副本;
在子进程被创建的那一刻,内存好像被照了一张像,不会发生改变了

AOF持久化流程
(1)客户端的请求写命令会被append追加到AOF缓冲区aof_buf内;
(2)AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
(3)AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
(4)Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;

重点是AOF重写:
Rewrite压缩

  1. 是什么:
    AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制, 当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集。可以使用命令bgrewriteaof
  2. 重写原理,如何实现重写
    AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),redis4.0版本后的重写,实际上就是把rdb 的快照,以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作。

no-appendfsync-on-rewrite:

如果 no-appendfsync-on-rewrite=yes ,不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)
如果 no-appendfsync-on-rewrite=no, 还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)

触发机制,何时重写
Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发
重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。

auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)
auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写。
例如:文件达到70MB开始重写,降到50MB,下次什么时候开始重写?100MB

系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,
如果Redis的AOF当前大小>= base_size +base_size*100% (默认) 且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。

3、重写流程
(1)bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。
(2)主进程fork出子进程执行重写操作,保证主进程不会阻塞。fork时父进程阻塞
(3)子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区,保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。
(4)1).子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。2).主进程把aof_rewrite_buf中的数据写入到新的AOF文件。
(5)使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。

缓存淘汰策略

惰性删除有一个问题:就是如果不访问过期的key,就永远不会删除这个key

redis分配的内存空间满了(maxmemory)
volatile的意思是从设置了过期时间的key中,选择一些淘汰,后缀代表选择方式不同
allkeys是从所有的键中,选择一些淘汰
ttl 是选择将要过期的key淘汰

redis往往承接的是缓存的任务,缓存很多时候要和数据库同步;
被动是说,如果缓存的数据因为到期被淘汰,那么下一次查询的时候,在缓存中查不到,就会查数据库,然后同步到缓存;被动一般是基于查询而触发的同步

主动是更新数据库以后,缓存中的数据不一样了,就要把缓存删除,下次再查的时候,会走被动方式
当然,也可以先删缓存再更新数据库,或者更新缓存,但是最建议的方式是先更新数据库,再删除缓存

删除缓存要比更新缓存更合理,因为更新缓存,缓存数据结构可能比较复杂,需要特殊处理,而删除很简单;或者缓存更新以后很长一段时间不被使用,这次更新就没什么用,效率低

如果先删除缓存,再更新数据库的话:如果第一步删除缓存成功了(图上写错了,应该是del),第二步更新数据库没成功,而却有线程查询缓存中的数据,但因为缓存数据已经被删除了,所以需要到数据库中查,而查后又将数据放在了缓存中;而异步重试更新以后的数据库,和缓存中的数据不一致了,不同步了

如果先更新数据库,再删除缓存的话:如果第一步更新成功了,但是删除缓存失败了,如果此时有人查数据,查到的是老数据;异步调试以后缓存删除成功,再查就会是新的数据了。我们认为是同步的

如果没有出错:
先删缓存,再更新数据库,仍然会出现不同步的问题

还需要注意的一点是:
如果在缓存和数据库同步的过程中,如果出错,为了保证两者的一致性,会重试,是异步的
会把这件事提交给一个消息队列,用一个线程消费,去解决这个问题

这种同步缓存的方式适用于允许一定延迟、有一定脏数据存在的场景,例如在查看淘宝时一个手机的信息做了更改,但是刷新以后看到的信息还是原来的信息
但是如果是库存的情况,就要尽快的同步,还要保证同步的成功率,还有回查的机制,所以要用特殊的机制,库存严格来说不是缓存
库存是敏感数据

分布式缓存常见问题

布隆过滤器也是基于缓存的,很节省内存,他是一个估算

解决方案:
一个一定不存在缓存及查询不到的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
解决方案:
(1)对空值缓存:如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟
(2)设置可访问的名单(白名单)
使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。
(3)采用布隆过滤器:(布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。
布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)
将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。
(4)进行实时监控:当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务

逻辑上设置过期时间是自己在一个单独的地方存一个过期时间,用一个线程去轮询,如果发现过期了,就重建缓存,但不会删除;过期的事由我们自己来做

key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。
解决问题:
(1)预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长
(2)实时调整:现场监控哪些数据热门,实时调整key的过期时长
(3)使用锁

(1)就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db。
(2)先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key
(3)当操作返回成功时,再进行load db的操作,并回设缓存,最后删除mutex key;
(4)当操作返回失败,证明有线程在load db,当前线程睡眠一段时间再重试整个get缓存的方法。

缓存雪崩与缓存击穿的区别在于这里针对很多key缓存,后者则是某一个key

缓存失效时的雪崩效应对底层系统的冲击非常可怕!
解决方案:
(1)构建多级缓存架构nginx缓存 + redis缓存 +其他缓存(ehcache等)
(2)使用锁或队列
用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况
(3)设置过期标志更新缓存
记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。
(4)将缓存失效时间分散开
比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

RocketMQ

存储基于两个东西,一个CommitLog
生产者往redis的节点里发数据,是把它放在commitLog文件里
消费者消费数据,不是从CommitLog里拿,为什么呢,因为commitLog里的数据,很多数据是有不同的主题的,因为是发布订阅模式,生产者按照主题发布消息,消费者按照主题订阅消息,如果要直接从CommitLog中去消费的话,需要遍历查看是哪一个主题而去消费,效率低;如何把分散的。随即的数据访问方式变成有序的?
就有了消费者队列ConsumerQueue,这个队列使得同一个主题的消息逻辑上顺序的组织在一起,这样就可以按顺序消费了

不是基于内存的,是基于磁盘的,所以是持久化的,基于磁盘的随即读写效率很差,所以才有这样的设计

生产者顺序写磁盘性能很好,同一个消费队列里存放的是相同主题的有序的数据,消费者消费的时候是顺序读,顺序读写磁盘的性能不亚于随即读写内存,所以性能能够得到保证

消费队列可以看成是CommitLog的一个索引,按照偏移量访问

消息存储都是有页缓存的,都是先写到缓存页里,再刷到磁盘中去,

生产和消费者是通过NameServer知道有哪些Broker,所以要注册

NameServer统一维护了Broker的状态,所以Broker要向NameServer上报状态

同一个主题,可以指定不同的标签,如果要消费某一个指定的标签,就需要过滤

只要是为了解决分布式系统下的事务,现在的思路都是两阶段提交
第一阶段:先尝试提交一下,如果服务器没收到这个消息
第二阶段:回查,不断督促要完成这个任务,并向我汇报
回查有一定次数限制,如果超过了,就放弃这个消息

首先得收到半消息(待进一步处理的消息),如果迟迟没有收到确认的话,就回查,依然没有收到确认就再回查


以上是关于框架和中间件(MyBatisRedisRocketMQ)的主要内容,如果未能解决你的问题,请参考以下文章

Django框架-中间件

Django框架 之 中间件

有人用 koa2 框架吗

Django和Flask这两个框架在设计上各方面有啥优缺点

框架和中间件(Spring BootSpringSpringMVC)

PHP 框架中间件实现