day01

MySQL查询语句

查询结果去重

使用Distinct和group by都能够实现去重的效果。

  1. distinct只能放在查询字段的最前面,不能放在查询字段的中间或者后面。distinct 对后面所有的字段均起作用,即 去重是查询的所有字段完全重复的数据,而不是只对 distinct 后面连接的单个字段重复的数据。因此,Distinct查询多个字段只对一个字段去重是无法实现的。SELECT DISTINCT university from user_profile;
  2. group by一般与聚类函数使用(如count()/sum()等),也可单独使用。group by可以使查询结果按一个或者多个字段进行分组(查询的字段可以不等于group by的字段),SELECT university from user_profile GROUP BY university;

查询结果限制返回行数

  1. LIMIT n:从第0+1(m=0)条开始,取n条数据,是LIMIT 0,n的缩写
1
2
3
SELECT device_id
FROM user_profile
LIMIT 2
  1. LIMIT m,n:从第m+1条开始,取n条数据
1
2
3
SELECT device_id
FROM user_profile
LIMIT 0,2
  1. LIMIT n OFFSET m:跳过前m条数据,从第m+1条开始取n条数据
1
2
3
SELECT device_id
FROM user_profile
LIMIT 2 OFFSET 0

tips : between是闭区间,包含了两个端点。

不等于的几种写法

1
2
3
4
5
6
select device_id,gender,age,university
from user_profile
#where university != '复旦大学'
#where university <> '复旦大学'
#where university not like '复旦大学'
#where university not in ('复旦大学')

空值的过滤

过滤空值的三种方法:
(1) Where 列名 is not null(推荐使用)
(2) Where 列名 != ‘null’
(3) Where 列名 <> ‘null’

or的使用

当查询列含有索引,使用or将会使索引失效,进行全表扫描,此时推荐使用UNION进行联合查询,二者各自走索引。

1
2
3
4
5
6
7
8
-- 走索引
SELECT `device_id`,`gender`,`age`,`university`,`gpa` FROM `user_profile` WHERE `university` = '北京大学'
UNION
SELECT `device_id`,`gender`,`age`,`university`,`gpa` FROM `user_profile` WHERE `gpa`>3.7

-- 全表扫描
select device_id,gender,age,university,gpa from user_profile where gpa>3.7 or university='北京大学';

sql中的字符匹配

  1. _代表任意一个字符
1
SELECT * FROM 学生表 WHERE name LIKE '张__'//查询姓“张”且名字是3个字的学生姓名。
  1. % 代表任意数量(包括0个)的字符

  2. []匹配[]中的任意一个字符(若要比较的字符是连续的,则可以用连字符“-”表 达),

  3. [^ ] 不匹配[ ]中的任意一个字符

1
2
SELECT * FROM 学生表 WHERE 姓名 LIKE '[张李刘]%' --查找姓张、李、刘
SELECT * FROM 学生表 WHERE 姓名 LIKE '[^张李刘]%' --查找不姓张、李、刘

聚合函数

AVG()、COUNT()、SUM()、MAX()、MIN()、ROUND();
HAVING是在聚合函数之后,GROUP BY 之前的条件。主要用于过滤分组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
--聚合函数应用经典案例
select
count(gender) as male_num,
round(avg(gpa),1) as avg_gpa
from user_profile GROUP BY gender
having gender ='male'

SELECT gender,university,count(gender) as user_num,
avg(active_days_within_30) as avg_active_days,avg(question_cnt) as avg_question_cnt
from user_profile
GROUP by university,gender; --考虑多列,多列一摸一样的分在一组

--聚合函数用Orderby排序
SELECT university,
AVG(question_cnt) AS avg_question_cnt
FROM user_profile
GROUP BY university
ORDER BY AVG(question_cnt) ASC

连接查询与子查询

join

Join和Inner Join是相同的,表示在表中存在至少一个匹配时返回行,相当于返回的是两个的交集。
LeftJoin从左表(table1)返回所有的行,即使右表(table2)中没有匹配。如果右表中没有匹配,则结果为 NULL。

总结
  1. A Inner Join B:类似于取A和B的交集,只取满足条件的相交部分。
  2. A Left Join B:左连接,以左表A为基础,取A表的全部行,B满足On条件的取对应行,不满足的取NULL。
  3. A Right Join B:有连接,以右表B为基础,取B表的全部行,A满足On条件的取对应行,不满足的取NULL。
  4. A Full Outer Join B:类似于取A和B的并集。全都取,但彼此没有对应的值就取NULL。
在进行连接操作时where和on的区别
  • ON条件是在生成临时表时使用的条件,它不管ON中的条件是否满足,都会返回左表中的数据。
  • where 条件是在临时表生成好后,再对临时表进行过滤的条件。这时已经没有 left join 的含义(必须返回左边表的记录)了,条件不为真的就全部过滤掉。

tips : 子查询返回多个值只能用in,确保只返回一个值可以用=

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

-- 子查询方式
select device_id,question_id,result
from question_practice_detail
where device_id in (
select device_id
from user_profile
where university='浙江大学'
)
order by question_id;

-- Join方式
select q.device_id,q.question_id,q.result
from question_practice_detail q join user_profile u on q.device_id=u.device_id
where u.university='浙江大学'
order by q.question_id;

-- 聚合函数和连接查询与having混用
-- 查看参加了答题的山东大学的用户在不同难度下的平均答题题目数

SELECT u.university,q2.difficult_level,count(q1.question_id)/count(distinct q1.device_id)
from user_profile u
inner join question_practice_detail q1
ON u.device_id=q1.device_id
inner join question_detail q2
on q1.question_id=q2.question_id
group by u.university,q2.difficult_level
having u.university='山东大学'

Union与Union all的使用(or不走索引时,考虑Union)

  • 结果集:UNION会在结果集中自动去除重复的行并会自动排序,会带来一定的性能开销。UNION ALL不会进行排序也不会去重,结果集顺序由各个子查询顺序决定,

IF、CASE WHEN、COALESCE等函数的使用

IF语句和CASE WHEN语句通常用于根据某些条件返回不同值的函数。
if(x=n,a,b)表示如果x=n,则返回a,否则返回b。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- IF下面返回两列,一列是25岁及以上,另一列是25岁以下。
Select
if(age>=25,'25岁及以上','25岁以下' ) as age_cut,
count(device_id) as number
From user_profile
Group by age_cut

-- CASE
select device_id,gender,
case
when age<20 then '20岁以下'
when age<25 then '20-24岁'
when age>=25 then '25岁及以上'
else '其他'
end age_cut
from user_profile;

day03

Spring-WebFlux

响应式编程

tips : 受欢迎的web库Vertx完美支持协程,但在生态上并不好,不利于快速开发,协程对应响应式、Java/Kotlin的响应式生态不完整,导致响应式无法被普遍使用,导致协程无法被广泛使用。(协程和异步响应式往往是对应的)

JDK1.8是基于Observer/Observable接口而实现的观察者模式,JDK9及以后,Observer/Observable接口就被弃用了,取而代之的是Flow类(juc包下面的Flow)。(采用发布者订阅者模式并在两者之间建立订阅关系)

响应式流规范可以总结为4个接⼝:Publisher、Subscriber、Subscription和ProcessorPublisher负责⽣成数据,并将数据发送给Subscription(每个Subscriber对应⼀个Subscription)。一旦Subscriber订阅成功,就可以接收来⾃Publisher的事件。Publisher调⽤ onSubscribe() ⽅法时,会将Subscription对象传递给Subscriber。通过Subscription,Subscriber可以管理其订阅情况。Subscriber开始请求数据后,数据就会开始流经响应流,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
//下面发布者订阅者相关类都是juc包下面的flow类

public class FlowDemo2 {

public static void main(String[] args) throws Exception {
// 1. 定义发布者, 发布的数据类型是 Integer
// 直接使用jdk自带的SubmissionPublisher
SubmissionPublisher<Integer> publiser = new SubmissionPublisher<Integer>();

// 2. 定义处理器, 对数据进行过滤, 并转换为String类型
MyProcessor processor = new MyProcessor();

// 3. 发布者 和 处理器 建立订阅关系
publiser.subscribe(processor);

// 4. 定义最终订阅者, 消费 String 类型数据
Subscriber<String> subscriber = new Subscriber<String>() {

private Subscription subscription;

@Override
public void onSubscribe(Subscription subscription) {
// 保存订阅关系, 需要用它来给发布者响应
this.subscription = subscription;

// 请求一个数据
this.subscription.request(1);
}

@Override
public void onNext(String item) {
// 接受到一个数据, 处理
System.out.println("接受到数据: " + item);

// 处理完调用request再请求一个数据
this.subscription.request(1);

// 或者 已经达到了目标, 调用cancel告诉发布者不再接受数据了
// this.subscription.cancel();
}

@Override
public void onError(Throwable throwable) {
// 出现了异常(例如处理数据的时候产生了异常)
throwable.printStackTrace();


// 我们可以告诉发布者, 后面不接受数据了
this.subscription.cancel();
}

@Override
public void onComplete() {
// 全部数据处理完了(发布者关闭了)
System.out.println("处理完了!");
}

};

// 5. 处理器 和 最终订阅者 建立订阅关系
processor.subscribe(subscriber);

// 6. 生产数据, 并发布
// 这里忽略数据生产过程
publiser.submit(-111);
publiser.submit(111);

// 7. 结束后 关闭发布者
// 正式环境 应该放 finally 或者使用 try-resouce 确保关闭
publiser.close();

// 主线程延迟停止, 否则数据没有消费就退出
Thread.currentThread().join(1000);
}

}

Reactor项目的Flux和Mono

FluxMono都是数据流的发布者,使用FluxMono都可以发出三种数据信号:元素值、错误信号、完成信号,错误信号和完成信号都代表终止信号,终止信号用于告诉订阅者数据流结束了,错误信号终止数据流同时把错误信息传递给订阅者。

tips : Mono和Flux的很多操作都是相同的,二者处理的数据量不同。Flux返回N个元素,Mono返回0或1个元素。Flux适用于处理多个元素,Mono适用于处理只有一个或没有元素的情况。

alt text
Flux和Mono的相关操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Flux<String> characterFlux = Flux
.just("Garfield", "Kojak", "Barbossa")
.delayElements(Duration.ofMillis(500)); // 每500毫秒发布⼀个数据

Flux<String> foodFlux = Flux
.just("Lasagna", "Lollipops", "Apples")
.delaySubscription(Duration.ofMillis(250)) // 订阅后250毫秒后开始发布数据
.delayElements(Duration.ofMillis(500)); // 每500毫秒发布⼀个数据

// 使⽤mergeWith()⽅法,将两个Flux合并,合并过后的Flux数据项发布顺序与源Flux的发布时间⼀致
// Garfield Lasagna Kojak Lollipops Barbossa Apples
Flux<String> mergedFlux = characterFlux.mergeWith(foodFlux);

mergedFlux.subscribe(System.out::println);

// 阻塞,等待结果
Thread.sleep(100000);

异步机制

事件轮询

事件轮询机制在多个事件源或输入源之间检测和处理事件。轮询机制不断检查是否有新的事件发生,并根据检测到的事件作出相应处理。与事件驱动机制不同,事件轮询通过主动查询的方式来获取事件的状态,而不是等待事件自动触发。

工作流程:

  • 创建一个轮询器,并为它指定一个或多个事件源(如文件、网络套接字、定时器等)。
  • 在循环中不断查询事件源的状态,检查是否有事件发生。
  • 一旦检测到事件,立即调用相应的处理程序来处理该事件。

例如:程序启动后,初始化一个轮询器,用来检测多个客户端的连接。轮询器不断轮询客户端的连接状态,检测是否有新的请求。如果某个客户端有请求数据到达,轮询器立即调用对应的函数处理该请求。处理完之后继续轮询。

NIO
1.NIO的非阻塞I/O模型
  • 在传统的阻塞I/O中,程序通常需要为每一个客户端连接创建一个线程,每个线程阻塞等待I/O操作完成(例如读或写操作)。这种模型在高并发场景下效率较低,因为线程数量的增加会导致大量的上下文切换和资源消耗。
  • NIO 提供了一种非阻塞模式,程序不必阻塞等待 I/O 操作完成。它允许在单个线程中管理多个通道(Channel),从而极大地提升了性能。
2.Selector与事件轮询
  • NIO的核心组件之一是Selector,它就是事件轮询机制的一个实现。
    Selector允许程序同时监控多个通道(如网络套接字)的状态。程序通过 Selector注册多个通道,然后可以在事件发生时轮询这些通道,处理其中的I/O事件(如可读、可写等)。
  • Selector 内部使用类似于操作系统提供的select()、poll()或epoll()等系统调用实现事件轮询机制,监听通道的状态变化。
3.具体工作流程
  • 事件注册:Selector可以注册多个Channel,并指定关注的事件类型(如READ、WRITE等)。
  • 事件轮询:Selector进入轮询循环,通过操作系统底层的事件轮询机制不断检查通道上是否有事件发生。
  • 事件处理:当检测到有通道上有事件发生时,Selector将返回相应的事件,程序会调用相应的事件处理逻辑来处理这些I/O事件。
4.总结

NIO的Selector是Java中事件轮询机制的核心实现,依赖底层操作系统提供的 select()、poll() 或 epoll() 等机制来高效地管理大量 I/O 通道的状态。通过事件轮询机制,NIO 能够在单个线程中处理多个I/O事件,从而显著提升高并发场景下的性能和资源利用率。

select()、poll() 和 epoll()
1.select()
  • select() 通过将需要监控的文件描述符集合传递给内核,内核遍历这些文件描述符,检查它们是否可读、可写或者有异常发生。
  • 程序调用 select() 函数并传入一组文件描述符,内核会阻塞该调用,直到其中至少有一个文件描述符变为可用(如可读或可写),或超时发生。
  • 文件描述符通过三个fd_set集合来表示,分别用于读、写和异常事件的检测。

底层实现:
select() 依赖于位图(bitmap)来表示每个文件描述符的状态。其实现方式是在每次调用时,遍历所有文件描述符,检查它们的状态变化。当事件发生时,内核更新位图并返回给用户空间程序。

不足:

  • 文件描述符的数量有上限:通常为1024。
  • 每次调用select() 时,用户空间和内核空间之间需要复制文件描述符集合,这会导致性能瓶颈。
  • select()会遍历整个文件描述符集合,即使只有少数几个文件描述符有事件发生,这种线性扫描会导致效率低下。
2.poll()
  • 不同于select()使用位图表示文件描述符,poll()使用一个结构体数组**pollfd[]**来保存每个文件描述符及其感兴趣的事件。
  • select()一样,poll()的检查过程也是每次都会遍历整个文件描述符集合。
  • poll()会在用户空间和内核空间之间复制一个pollfd结构数组(所以每次调用poll时也会在用户态和内核态之间复制),然后内核通过线性扫描检查每个文件描述符是否有事件发生。
  • poll()是水平触发的,即当某个文件描述符的状态改变后,如果不处理它,则每次调用poll()都会通知该文件描述符的状态变化。
3.epoll()

工作原理:

  • epoll()是Linux专有的I/O多路复用机制,是 select() 和 poll() 的改进版本。它在处理大量并发连接时效率更高,特别适合高并发场景。
  • epoll()除水平触发机制外,还有边缘触发:只在文件描述符的状态从不可用变为可用时才会通知程序。

底层实现:

  • 事件注册机制: epoll()首次调用时,用户通过epoll_create()创建一个epoll实例,然后通过epoll_ctl()将要监控的文件描述符注册到这个实例中。与select()poll()每次调用时都需要传递文件描述符不同,epoll()将文件描述符注册一次,后续就可以直接通过事件通知机制来获取I/O事件。
  • 内核事件队列: epoll通过一个内核级事件队列来监控文件描述符的状态。当某个文件描述符有事件发生时,内核将该事件加入队列中,然后用户空间通过epoll_wait()来获取已发生的事件。
  • epoll使用红黑树来管理文件描述符,以实现高效的增删操作,事件发生时,内核将事件加入双向链表中,避免了遍历整个文件描述符集合。
区别总结:

alt text

Spring WebFlux对响应式编程(异步)的支持

tips: WebFlux的默认嵌⼊式服务器是Netty⽽不是Tomcat。Netty是⼀个异步、事件驱动的服务器,⾮常适合Spring WebFlux这样的反应式Web框架。

传统的基于Servlet的Web框架,如Spring MVC,在本质上都是阻塞和多线程的,每个连接都会使用一个线程。在请求处理的时候,会在线程池中拉取一个工作者( worker )线程来对请求进行处理。同时,请求线程是阻塞的,直到工作者线程提示它已经完成为止。

在Spring5中,引入了一个新的异步、非阻塞的WEB模块,就是Spring-WebFlux。该框架在很大程度上是基于Reactor项目的,能够解决Web应用和API中对更好的可扩展性的需求。

异步的Web框架能够以更少的线程获得更⾼的可扩展性,通常它们只需要与CPU核⼼数量相同的线程。通过使⽤所谓的事件轮询(event looping)机制,这些框架能够⽤⼀个线程处理很多请求,这样每次连接的成本会更低。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

import java.util.HashMap;
import java.util.Map;

@Service
public class UserService {

// 模拟数据库存储
private Map<Integer, User> map = new HashMap<>();

public UserService() {
map.put(1, new User("zhangsan"));
map.put(2, new User("lisi"));
map.put(3, new User("wangwu"));
}

// 根据id查询
public Mono<User> getById(Integer id){
// 返回数据或空值
return Mono.justOrEmpty(map.get(id));
}

// 查询多个
public Flux<User> getAll(){
return Flux.fromIterable(map.values());
}

// 保存
public Mono<Void> save(Mono<User> userMono){
return userMono.doOnNext(user -> {
int id = map.size() + 1;
map.put(id, user);
}).thenEmpty(Mono.empty()); // 最后置空
}
}

SpirngWebFlux一般用于SpringCloud-Gateway网关,用于处理请求、路由转发等功能。

try-with-resources

在try-with-resources内的资源,需要释放的资源(比如 BufferedReader)实现了 AutoCloseable接口。

day06

使用枚举类的方法

直接枚举类名.加上枚举字段名加上get方法。