关注订阅

数据库表

t_following_group用于存贮分组类别,预设三个分组: type=1,name=悄悄关注 ;type=2,name=默认分组 ; type=3用于用户自定义分组,预设分组每个用户都有,不需要关联用户;自定义分组为某个用户特有,需要与创建该分组的特定用户使用usrId关联。由于经常根据用户ID查询该用户拥有的分组,userId经常被查询所以添加索引。同时对userId添加外键约束。

image-20220410135300231

t_user_following用于存储关注者ID、被关注者ID、关注类别ID。由于经常进行<关注者ID,被关注者ID>的查询,所以建立联合索引( userId,followingId),在查询当前用户关注的用户时使用userId字段,根据最左匹配原则可以重用联合索引( userId,followingId),在查询当前用户关注的粉丝使用followingId字段,根据最左匹配原则不能重用联合索引( userId,followingId),需要新建索引followingId。同时对userIdfollowingId添加外键约束。

索引

1,最左匹配原则

如果查询的时候查询条件精确匹配索引的左边连续一列或几列,则此列就可以被用到,同时遇到范围查询(>、<、between、like)就会停止匹配。构建一颗 B+ 树只能根据一个值来构建,因此数据库依据联合索引最左的字段来构建 B+ 树,叶子节点存储的是第一个关键字的索引 a,而叶子节点存储的是三个关键字的数据。这里可以看出 a 是有序的,而 b,c 都是无序的。但是当在 a 相同的时候,b 是有序的,a,b 相同的时候,c 又是有序的。所以查询a=0,b=1,c=2时,过滤完a=0的数据后b有序,过滤完b=1数据后c有序,可以利用到abc的索引。a=0,b>1,c=2经过a,b过滤后c无需,只能用到索引a,b。

1804577-20200521182659976-48843100

2,联合索引

3,索引优化

4,查询缓慢排查

5,B树、B+树

添加关注

检查选择分组、被关注用户是否存在。删除<关注者,被关注者>记录并插入新记录。由于涉及到多个对数据库更改的操作,需要事务保证原子性和一致性。

事务

1,ACID:原子性( Atomicity )、一致性( Consistency )、隔离性( Isolation )和持续性( Durability )。事务中的所有操作要么全部执行,要么都不执行。⑴ 原子性:全部成功/全部失败回滚⑵ 一致性一致性状态变换到另一个一致性状态(2000+3000=1000+4000)。⑶ 隔离性并发多事务间无干扰(T1/T2=>T1+T2,T2+T1)⑷ 持久性:事务提交改变永久。

2,隔离特性:

3,事务隔离:

4,声明式事务:通过声明的方式,在IoC配置中指定事务的边界和事务属性,方法、类上增加@Transactional注解,声明事务的隔离级别、传播机制。

5,引擎

6,ACID实现

获得关注分组

返回默认分组被关注者信息+各个种类下被关注者信息。返回[group0:[userInfo0,userInfo1,...],group1:[userInfo2,userInfo3,...]]

用户26获得的关注列表:

获得粉丝信息

获得当前关注当前用户的用户信息,以及粉丝与当前用户是否互关。

用户26获得的粉丝列表:

分页模糊查询

根据昵称惊醒分页模糊查询,先根据查询条件统计符合条件的用户数,若大于0才进行后续分页查询。并检查当前用户是否关注查询到的用户。

动态管理

数据表

创建动态条目数据表t_user_moments。由于userId经常被用来查询用户发表过的动态,所以需要建立索引。并对userId添加外键约束。

发布订阅模型

1,发布订阅模式:核心基于一个中心来建立整个体系。其中发布者和订阅者不直接进行通信,而是发布者将要发布的消息交由中心管理,订阅者也是根据自己的情况,按需订阅中心中的消息。发布者的发布动作和订阅者的订阅动作相互独立,无需关注对方,消息派发由发布订阅中心负责,生产者、订阅者完全解耦的。

v2-b6ed65f370a766620718ad4227d5d4e5_r

2,观察者模式:一般至少有一个可被观察的对象 Subject ,可以有多个观察者去观察这个对象。二者的关系是通过被观察者主动建立的,被观察者至少要有三个方法——添加观察者、移除观察者、通知观察者。观察者主动申请加入被观察者的列表,此后只要被观察者在某种时机触发通知观察者方法时,观察者即可接收到来自被观察者的消息。实现上:被观察者它引用一个Observer接口对象的集合,任何想要得到通知的对象,只要实现该接口,并且把自己注册到通知者即可,每当要发通知时,通知者遍历集合给被通知者发送消息,实现松耦合的关系。

567e4179118647d59f000763a3bc5046_tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0

3,区别:观察者是软件设计模式中的一种,发布订阅只是软件架构中的一种消息范式。

image-20220411164353995

 

消息队列

当不需要立即获得结果,但是并发量又需要进行控制的时候。

1,优点:

1506330094150_2541_1506330096475

2,工作模式

1506330158945_9538_1506330161280

3,RabbitMQ

4,问题

发送动态

往指定的exchange中发送消息,消息在exchange上可以根据routing key 发送到多个queue。

MessageConverter用于将Java对象转换为RabbitMQ的消息。默认情况下,Spring Boot使用SimpleMessageConverter,只能发送String和byte[]类型的消息,使用Jackson2JsonMessageConverter,就可以发送JavaBean对象,由Spring Boot自动序列化为JSON并以文本消息传递。

rabbitTemplate.convertSend()方法立即返回(异步发送)无法得到接收端反馈,如果想得到反馈可以使用convertSendAndReceive()该该方法为同步方法,只有在接收端处理完信息后才会返回,使用AsyncRabbitTemplate(rabbitTemplate)获得异步rabbitTemplateconvertSendAndReceive()该方法变成异步方法,发送后立即返回,在接收端处理后调用设定的回调方法。

此处不接受接收者反馈,异步发送,发送后立即返回。

接收动态

接收器:监听特定queue下的消息,此处broker与用户使用推送(push)的方式传递消息,当监听器收到消息时执行回调方法,在获取当前动态对应用户的粉丝集合后,构建<follwingUserId,msg>键值对存入redis,等待用户查询关注用户动态时从redis获取。

用户26关注了用于17、25,当17、25发表动态后,26将在redis中获取到动态消息。

Redis

性能优秀,数据在内存中,读写速度非常快。单进程单线程,是线程安全的,采用 IO 多路复用机制。丰富的数据类型,支持字符串 (strings)、散列(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets) 等。支持数据持久化,可以将内存中数据保存在磁盘中,重启时加载。主从复制,哨兵,高可用。

1,数据类型

2,有效期

Redis 中有个设置缓存时间过期的功能,即对存储在 redis 数据库中的值可以设置一个过期时间。

3,内存淘汰机制

如果定期删除漏掉了很多过期 key,然后也没及时去查,也就没走惰性删除,如果大量过期 key 堆积在内存里,导致 redis 内存块耗尽。

4,持久化机制

5,缓存雪崩

缓存同一时间大面积的失效,所以后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决办法:1,尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上;2,把每个 Key 的失效时间都加个随机值,保证数据不会再同一时间大面积失效。3,选择合适的内存淘汰策略,防止爆内存。对请求限流 ,避免 MySQL在redis崩溃后也崩掉,只要数据库正常工作,就可以处理用户请求,保证系统仍然可用。4,利用 redis 持久化机制保存的数据尽快恢复缓存。

14534869-cefa2f5519af3a09

6,缓存穿透

大量请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决办法:1,在接口层增加校验,比如用户鉴权,参数做校验;2,采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,用于快速判断出 Key 是否在数据库中存在,一个一定不存在的数据会被这个 bitmap 拦截掉,这个恶意请求就会被提前拦截,从而避免了对底层存储系统的查询压力。3,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。4,在访问数据据时加上互斥锁。

7,缓存击穿

缓存击穿是指一个 Key 非常热点,在不停地扛着大量的请求,大并发集中对这一个点进行访问,当这个 Key 在失效的瞬间,持续的大并发直接落到了数据库上,就在这个 Key 的点上击穿了缓存。

解决办法:1,设置热点数据永不过期。2,在访问数据据时加上互斥锁。

8,底层实现

Redis 内部使用一个 redisObject 对象来表示所有的 key 和 value。type 表示一个 value 对象具体是何种数据类型,encoding 是不同数据类型在 Redis 内部的存储方式。

9,快

Redis 单进程单线程的模型,因为 Redis 完全是基于内存的操作,CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章的采用单线程的方案了。

10,主从复制

11,redis和数据库双写一致性问题

数据库和缓存双写,就必然会存在不一致的问题。如果对数据有强一致性要求,不能放缓存。如果一致性不是太高可以采取正确更新策略,先更新数据库,再删缓存。

12,工作模式

柱塞、非柱塞、异步、同步
I/O多路复用

通过一种机制,可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作,没有就绪事件时,就会阻塞交出cpu。多路是指多个链接,复用指的是复用同一线程。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。

NIO

是一种同步非阻塞的 I/O 模型,在等待就绪阶段都是非阻塞的,真正的 I/O 操作是同步阻塞。是 I/O 多路复用的基础,成为解决高并发与大量连接、I/O 处理问题的有效方式。

服务器端同步阻塞 I/O 处理:socket.accept()、socket.read()、socket.write() 三个主要函数都是同步阻塞的,当一个连接在处理 I/O 的时候,系统是阻塞的,所以使用多线程时,就可以让 CPU 去处理更多的事情。低并发下结合线程池使得创建和回收成本相对较低,并且编程模型简单。创建和销毁都是重量级的系统函数,线程本身占用较大内存,线程的切换成本是很高的,无法应对百万级连接。

所有的系统 I/O 都分为两个阶段:等待就绪和操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。NIO 里用户最关心” 我可以读了”。NIO的读写函数可以立刻返回而不是柱塞,如果一个连接不能读写(socket.read()返回0或者socket.write()返回0),我们可以把这件事记下来,将用于传输的通道全部注册到选择器上,选择器监控通道,当某一通道就绪后连接继续进行读写,没有必要开启多线程。没有线程切换,只是拼命的读、写、选择事件。

77752ed5

Java NIO 实际读写时的核心在于:通道(Channel)和缓冲区(Buffer),选择器。通道表示打开到 IO 设备(文件流、套接字)的连接,对原 I/O 包中的流的模拟,负责传输;缓冲区用于容纳数据,负责存储,Channel的读写必须通过buffer对象,然后操作缓冲区,对数据进行处理。缓存区是双向的,既可以往缓冲区写入数据,也可以从缓冲区读取数据:缓冲区<->然后缓冲区通过通道进行传输<->从缓冲区取数据。选择器:把Channel通道注册到Selector中,通过Selecotr监听Channel中的事件状态,这样就不需要阻塞等待客户端的连接,从主动等待客户端的连接,变成了通过事件驱动,通过事件驱动实现单线程管理多个Channel的目的。

0ece5d16ec1345a5b4dc2149cb5a8b40_tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0

缓冲区根据数据类型的不同,可以进行划分ByteBuffer、CharBuffer等。根据工作方式分:直接缓冲区(磁盘->内核地址空间中->用户地址空间中->读取到应用程序)与非直接缓冲区(将缓冲区建立在物理内存之中,读写数据直接通过物理内存进行)。