开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

《蹲坑也能进大厂》多线程系列 - 悲观锁、乐观锁、可重入/不

发表于 2021-06-26

这是我参与更文挑战的第 15 天,活动详情查看:更文挑战

作者:花Gie

微信公众号:Java开发零到壹

前言

多线程系列我们前面已经更新过很多章节,强烈建议小伙伴按照顺序学习:

《蹲坑也能进大厂》多线程系列文章目录

前面我们已经学习过synchronized锁,也知道他的用法和原理,但是它属于锁中的哪一类呢,它属于悲观锁、可重入锁、不可中断锁等,那为什么会有这么多锁类型呢,看完本文你就能够掌握锁究竟是什么。

正文

锁的总览

锁可以从不同的角度进行分类,这些分类并不是互斥的,比如一个人既可以是医生,又可以是父亲。分类总览如图:

Java锁分类

乐观锁与悲观锁

  • 概念

这其实和不同人的性格一样,乐观锁就对应乐观开朗性格的小伙伴,他们总会看到事物美好的一面;而悲观锁则对应生活中凡是都将事情考虑到最坏的状态。

悲观锁又叫互斥同步锁,它为了确保结果的正确性,会在每次获取到数据后,都会将其锁住,因此当其他线程也来访问时,就会进入阻塞状态,这样就可以防止其他线程访问该数据,从而保证数据的安全性。Java中我们常用的Synchronized及RenntrantLock都是悲观锁,在数据库很多地方就用到了这种锁机制,比如行锁,表锁,读锁,写锁等。

乐观锁又叫非互斥同步锁,它认为自己在处理数据的时候,不会有其他线程来进行干扰,因此它不会把数据锁住;如果一个线程在修改数据时,没有其他线程干扰,那就会正常执行修改;而如果该数据已经被其他线程修改过,那当前线程为了保证数据正确性,就会执行放弃或报错等策略。常用的悲观锁例子有原子类、并发集合等。

我们对原子类也有过分析,有兴趣小伙伴建议看一下AtomicInteger使用方式及源码介绍。

  • 乐观锁实现方式
1. `版本号机制`


在数据表中添加一个version字段,用于表示数据被更新的次数,当读取出数据时,将此版本号一同读出,此后每更新完一次数据后,对版本号version加一,然后保存到表记录中。假如进行更新操作时,会将当前version版本号和数据库中的版本号进行对比,如果提交的数据版本号较大,就进行更新操作,否则认为是过期数据,就会重试更新操作,直到更新成功。
2. `CAS`


全称为compare and swap,从字面上理解就是**比较与转换**。CAS包含三个操作数 `目标数据内存位置(V)`、 `进行比较的预期值(A)`、 `拟写入的新值(B)`,在修改数据时,会将内存位置V与预期值A进行比较,如果两者相等,会将内存位置V修改为新值B,否则就不执行任何操作。



> **拓展:**
> 
> 
> 
>     1. CAS包含比较和转换两个操作,但它属于原子操作,由CPU在硬件层面保证其原子性,
>     2. 许多CAS的操作是自旋的,即操作不成功时不断重试,直到操作成功为止。
  • 优缺点对比

我们前面已经用过Synchronized这种悲观锁,感觉也是非常简单方便,那为什么还要搞一个乐观锁呢,这就要对两种锁的不足之处进行对比:

1. `悲观锁缺陷`
    + 当一个线程进入悲观锁时,如果该线程永久阻塞(如死锁),那其他等待该锁线程只能进行等待,并且永远不会得到执行。
    + 限制并发数:
2. `乐观锁缺陷`
    + **ABA问题**:如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。
    + **自旋时间长开销大**:如果数据修改不成功,就会一直循环执行直到成功,会给CPU带来非常大的执行开销。
    + **功能限制**:CAS只能保证单个变量的原子性,如果我们需要保证一段代码的原子性,乐观锁就不再适合。
  • 使用场景选择

悲观锁或乐观锁并不是想要替代对方,也没有优劣之分,只是他们各自适用在不同的场景。

  1. 对于资源竞争较少(线程冲突较轻)的情况,使用乐观锁更佳。使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,因此可以获得更高的性能。
  2. 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的几率会比较大,这样会造成CPU资源浪费,效率低于悲观锁。

可重入、不可重入锁

  • 概念

不可重入锁是指当前线程执行中已经获取了锁,如果再次获取该锁时,就会被阻塞。

下面我们以wait/notify来设计一个不可重入锁(此外还可以通过CAS + 自旋来实现):

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
java复制代码//wait、notify实现不可重入锁
public class NonReentrantLockDemo1 {
//记录是否被锁
private volatile boolean locked = false;

public synchronized void lock() {
//当某个线程获取锁成功,其他线程进入等待状态
while (locked) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//加锁成功,locked 设置为 true
locked = true;
}
//释放锁
public synchronized void unlock() {
locked = false;
notify();
}
}


//测试类
class ThreadDemo implements Runnable{
NonReentrantLockDemo lock = new NonReentrantLockDemo();

public static void main(String[] args) {
new Thread(new ThreadDemo()).start();
}
/**
* 方法1:调用方法2
*/
public void method1() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " method1()");
method2();
} finally {
lock.unlock();
}
}
/**
* 方法2:打印前获取 obj 锁
*
*/
public void method2() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " method2()");
} finally {
lock.unlock();
}
}

@Override
public void run() {
method1();
}
}

测试结果:

image-20210627115029075

结果显示:当线程执行到method1时,已经拿到lock锁,只要该锁没有被释放,其他代码块无法使用此锁来加锁。当该线程再去调用method2时,而method2也需要获取lock才能执行,这样都导致了死锁,这种会出现问题的重入一把锁的情况,叫不可重入锁。

可重入锁又叫递归锁,指在同一个线程在外层方法获取锁的时候,进入内层方法会自动获取锁。因为《蹲坑也能进大厂》多线程系列-synchronized深入原理终结篇已经对该锁进行详细的讲解,所以不再赘述。

  • 代码示例

这里使用一段代码简单演示ReentrantLock可重入:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码private static ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) {
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
}

打印结果:

1
2
3
4
5
java复制代码0
1
2
1
0

结果显示:同一个线程获取到ReentrantLock锁后,在不释放该锁的情况下可以再次获取。

  • 可重入性优点
  1. 避免死锁
  2. 提升封装性

总结

以上就是对锁的部分介绍,理论性比较强,但是这部分内容非常重要,不然后续别人问你悲观锁有哪些,即使你用过,但你可能都不理解这个悲观锁是什么,造成场面一度尴尬。

关于锁的剩余部分会在下一章继续输出。

点关注,防走丢

以上就是本期全部内容,如有纰漏之处,请留言指教,非常感谢。我是花GieGie ,有问题大家随时留言讨论 ,我们下期见🦮。

文章持续更新,可以微信搜一搜 Java开发零到壹 第一时间阅读,并且可以获取面试资料学习视频等,有兴趣的小伙伴欢迎关注,一起学习,一起哈🐮🥃。

原创不易,你怎忍心白嫖,如果你觉得这篇文章对你有点用的话,感谢老铁为本文点个赞、评论或转发一下,因为这将是我输出更多优质文章的动力,感谢!

参考链接:

ifeve.com/java-art-re…

www.cnblogs.com/ConstXiong/…

blog.csdn.net/qq_20499001…

www.cnblogs.com/yeya/p/1358…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

震惊!你还要问别人?在这里大佬都给你整理好了!!JdbcTe

发表于 2021-06-26

在这里插入图片描述

JdbcTemplate简介

  JdbcTemplate是Spring JDBC的核心类,借助该类提供的方法可以很方便的实现数据的增删改查。

  Spring对数据库的操作在jdbc上面做了深层次的封装,使用spring的注入功能,可以把DataSource注册到JdbcTemplate之中。

  JdbcTemplate位于中。其全限定命名为org.springframework.jdbc.core.JdbcTemplate。要使用JdbcTemlate还需一个这个包包含了事务和异常控制

  

JdbcTemplate主要提供以下五类方法:

  1. execute方法:可以用于执行任何SQL语句,一般用于执行DDL语句;
  2. update方法及batchUpdate方法:update方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句;
  3. query方法及queryForXXX方法:用于执行查询相关语句;
  4. call方法:用于执行存储过程、函数相关语句。

xml中的配置:

1
2
3
4
5
6
7
8
9
10
11
12
c复制代码<!-- 扫描 -->
<context:component-scan base-package="com.zzj.*"></context:component-scan>

<!-- 不属于自己工程的对象用bean来配置 -->
<!-- 配置数据库连接池 -->
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close" p:username="root" p:password="qw13579wq">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://127.0.0.1:3306/test"></property>
</bean>

<!-- 配置jdbcTemplate -->
<bean class="org.springframework.jdbc.core.JdbcTemplate" p:dataSource-ref="dataSource"></bean>

常用方法:

数据库user_info表:
在这里插入图片描述

我们先创建一个实体对象,对应数据库表中的信息,方便之后的查询操作

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
c复制代码package com.zzj.vo;

public class UserInfo {

private int id;
private String userName;
private String password;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getUserName() {
return userName;
}

public void setUserName(String userName) {
this.userName = userName;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

@Override
public String toString() {
return "UserInfo [id=" + id + ", userName=" + userName + ", password=" + password + "]";
}

}

修改(包含增、删、改):update()方法,另有批量插入方法batchUpdate()

update()方法增删改

:

UserInfoDao.java代码:

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
c复制代码@Repository
public class UserInfoDao {

@Autowired
//从容器中自动扫描获取jdbcTemplate
private JdbcTemplate jdbcTemplate;

//update()实现增加数据
public boolean insert(int id,String userName,String password){
String sql = "insert into user_info values (?,?,?)";
return jdbcTemplate.update(sql,id,userName,password)>0;
}

//update()实现修改
public boolean update(int id,String userName,String password){
String sql = "update user_info set user_name=?,password=? where id=?";
return jdbcTemplate.update(sql,userName,password,id)>0;
}

//update()实现删除
public boolean delete(int id){
String sql = "delete from user_info where id=?";
return jdbcTemplate.update(sql,id)>0;
}

}

测试类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c复制代码public class Test {

public static void main(String[] args) {

ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("application.xml");
UserInfoDao userDao = applicationContext.getBean(UserInfoDao.class);

boolean insert = userDao.insert(1,"Jim", "123");
boolean update = userDao.update(1,"Tom","123456");
boolean delete = userDao.delete(1);
     System.out.println("插入:"+insert+",修改:"+update+",删除:"+delete);


}

}

测试结果:

在这里插入图片描述

查询:查询单个值、查询一个对象、查询多个对象

查询单个值

1
2
3
4
5
6
7
8
9
10
c复制代码//查询单个值
public boolean login(String userName,String password){
try {
String sql = "select id from user_info where user_name=? and password=?";
jdbcTemplate.queryForObject(sql,String.class,userName,password);
return true;
} catch (DataAccessException e) {
return false;
}
}

查询一个对象:RowMapper方法和ResultSetExtractor方法

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
c复制代码//查询单个对象
public UserInfo getById(int id){
String sql = "select id,user_name,password from user_info where id=?";

//RowMapper方法
class UserInfoRowMapper implements RowMapper<UserInfo>{

@Override
public UserInfo mapRow(ResultSet rs, int rowNum) throws SQLException {
UserInfo userInfo = new UserInfo();
userInfo.setId(rs.getInt("id"));
userInfo.setUserName(rs.getString("user_name"));
userInfo.setPassword(rs.getString("password"));
return userInfo;
}

}

return jdbcTemplate.queryForObject(sql,new UserInfoRowMapper(),id);

//RowMapper方法的Lambda表达式
return jdbcTemplate.queryForObject(sql,(ResultSet rs,int rowNum)->{
UserInfo userInfo = new UserInfo();
userInfo.setId(rs.getInt("id"));
userInfo.setUserName(rs.getString("user_name"));
userInfo.setPassword(rs.getString("password"));
return userInfo;
},id);

//ResultSetExtractor方法
class UserInfoResultSet implements ResultSetExtractor<UserInfo>{

@Override
public UserInfo extractData(ResultSet rs) throws SQLException, DataAccessException {
UserInfo userInfo = null;
if(rs.next()){
userInfo = new UserInfo();
userInfo.setId(rs.getInt("id"));
userInfo.setUserName(rs.getString("user_name"));
userInfo.setPassword(rs.getString("password"));
}
return userInfo;
}
}
return jdbcTemplate.query(sql,new UserInfoResultSet(),id);

//ResultSetExtractor方法的lambda表达式
return jdbcTemplate.query(sql,(ResultSet rs)->{
UserInfo userInfo = null;
if(rs.next()){
userInfo = new UserInfo();
userInfo.setId(rs.getInt("id"));
userInfo.setUserName(rs.getString("user_name"));
userInfo.setPassword(rs.getString("password"));
}
return userInfo;
},id);

}

查询多个对象:RowMapper方法和ResultSetExtractor方法

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
c复制代码//查询多个对象
public List<UserInfo> selectAll(){
String sql = "select id,user_name,password from user_info";

//RowMapper方法
class UserInfoRowMapper implements RowMapper<UserInfo>{

@Override
public UserInfo mapRow(ResultSet rs, int rowNum) throws SQLException {
UserInfo userInfo = new UserInfo();
userInfo.setId(rs.getInt("id"));
userInfo.setUserName(rs.getString("user_name"));
userInfo.setPassword(rs.getString("password"));
return userInfo;
}

}
return jdbcTemplate.query(sql,new UserInfoRowMapper());


//RowMapper方法的Lambda表达式
return jdbcTemplate.query(sql,(ResultSet rs,int rowNum)->{
UserInfo userInfo = new UserInfo();
userInfo.setId(rs.getInt("id"));
userInfo.setUserName(rs.getString("user_name"));
userInfo.setPassword(rs.getString("password"));
return userInfo;
});

//ResultSetExtractor方法
class UserInfoResultSet implements ResultSetExtractor<List<UserInfo>>{

@Override
public List<UserInfo> extractData(ResultSet rs) throws SQLException, DataAccessException {
List<UserInfo> list = new ArrayList<>();
while(rs.next()){
UserInfo userInfo = new UserInfo();
userInfo.setId(rs.getInt("id"));
userInfo.setUserName(rs.getString("user_name"));
userInfo.setPassword(rs.getString("password"));
list.add(userInfo);
}
return list;
}

}
return jdbcTemplate.query(sql, new UserInfoResultSet());

//ResultSetExtractor方法的lambda表达式
return jdbcTemplate.query(sql,(ResultSet rs)->{
List<UserInfo> list = new ArrayList<>();
while(rs.next()){
UserInfo userInfo = new UserInfo();
userInfo.setId(rs.getInt("id"));
userInfo.setUserName(rs.getString("user_name"));
userInfo.setPassword(rs.getString("password"));
list.add(userInfo);
}
return list;
});

}

【参考文案】

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何设计一个C++网络库

发表于 2021-06-26

C++网络库的实现涉及了很多网络编程相关的内容,例如非阻塞IO和IO多路复用的应用,Reactor模式的思想,应用层Buffer的作用,线程池实现One-Loop-One-Thread模型等。muduo是陈硕大神开发的多线程TCP网络库,本文记录了学习muduo源码后的思考,捋一捋C++网络库设计过程中的一些逻辑,以作参考借鉴。

网络库基础原理

非阻塞IO和IO复用

阻塞IO和非阻塞IO的区别在于IO未就绪时程序是否阻塞等待。IO复用是指通过某种机制(通常是epoll)监听多个描述符,一旦某个描述符就绪,就能够通知程序进行相应的读写操作。

epoll原理

epoll底层使用红黑树维护监控描述符,使用链表维护就绪描述符,epoll_wait调用只需要观察就绪链表。epoll高效的实现机制在于通过在内核注册中断回调,异步地把就绪描述符放进就绪链表里。

Reactor模式

IO复用机制依赖于一个事件多路分发器,分发器对象负责将请求事件分发到对应的事件处理器,事件处理器需要预先注册回调函数,事件分发器捕获IO就绪事件,然后将就绪事件分发到对应的事件处理器,由处理器完成实际的IO操作。

面向过程实现思路(单线程)

服务器创建listenfd,设置为非阻塞并开始监听;然后服务器创建epollfd,向epollfd注册listenfd及关注事件,并调用epoll_wait阻塞等待;当listenfd有可读事件发生时,epoll_wait被唤醒,程序调用accept获得connfd,设置connfd为非阻塞,并向epollfd注册connfd及关注事件,完成后再次循环调用epoll_wait阻塞等待;当connfd有可读可写事件发生时,epoll_wait被唤醒,程序调用相应的读写处理逻辑。

面向对象抽象设计(多线程)

基本概念

网络库一般监听三种类型的事件:网络IO事件、定时器事件、自身线程唤醒事件。定时器事件用于处理网络库中某些控制逻辑,例如超时断开连接;自身线程唤醒事件是网络库必要的一种通知机制,方便唤醒阻塞等待的IO复用。

网络库线程一般分为几类:IO线程用于处理连接请求;计算线程用于进行请求需要的复杂运算,其它线程包括日志线程异步记录日志和某些业务线程等。

核心设计

一个基于Reactor模式的C++多线程TCP网络库服务端代码主要包含了以下核心部分:

  • EventLoop
  • Channel
  • Poller
  • TcpServer
  • Acceptor
  • TcpConnection
  • EventLoopThreadPool

如何设计一个C++网络库.jpg

  1. EventLoop是对IO复用等待唤醒处理过程的抽象。每个线程会绑定一个EventLoop实例并运行loop函数,loop函数实现IO复用等待获取就绪事件,并通过回调函数进行处理的循环逻辑。EventLoop实例在网络库里有两类:main-loop用于监听listenfd并accept连接,IO-loop处理分配到该线程上的连接的数据请求。
  2. Channel对象负责管理fd的IO事件。一个Channel对象关联一个fd,fd可以是listenfd,eventfd,timefd等,并和某个EventLoop实例进行绑定。Channel实现了IO事件处理函数,处理函数会在绑定EventLoop实例对应的线程上执行。
  3. Poller是IO复用的抽象,提供了更新事件和等待就绪事件的接口。EpollPoller继承自Poller,封装了和epoll相关的系统调用并实现了Poller的接口。
  4. TcpServer是一个TCP功能服务的抽象。它包含了Acceptor,EventLoop线程池,并管理所有与之建立的连接。
  5. Acceptor创建listenfd并进行监听,Acceptor包含一个Channel对象,该channel关联了listenfd并绑定到main-loop。
  6. TcpConnection是客户端和TCP服务器连接的抽象。
  7. EventLoopThreadPool连接了EventLoop和ThreadPool。EventLoopThreadPool在创建TcpServer实例时被初始化,它存放的是IO线程。TcpConnection对象创建时会绑定到某个IO线程,由该IO线程处理该连接的所有请求。

逻辑思路

TcpSever如何跟EventLoop关联

demo的实现逻辑是先创建EventLoop实例,并作为参数创建TcpServer实例,TcpServer实例调用start函数,最后EventLoop实例调用loop函数。

Acceptor对象初始化时会向epoll注册listenfd及关注事件,TcpServer实例调用start函数后启动监听。客户端发起连接后,main-loop监听的listenfd有可读事件,程序调用listenfd对应Channel的读处理函数,读处理函数实际上嵌入了Acceptor对象的accept逻辑。得到新的connfd后,TcpConnection对象被创建,TcpConnection对象包含一个Channel对象,该channel关联connfd,并绑定到某个IO线程,IO线程的选择使用Round-Robin轮询算法,IO线程从EventLoopThreadPool获取。connfd及关注事件会被注册到IO线程对应的poller里。

数据如何被读写

connfd有IO事件就绪时,相应的IO-loop退出等待并收集该线程上所有需要进行事件处理的channels,然后调用Channel相应的回调函数进行读写。

数据读写需要应用层Buffer,read一次可能不能把内核缓冲区的数据全部读完,应该把已经读到的数据保存到应用层接收Buffer,由应用层接收Buffer解决粘包问题,write一次可能不能把所有数据全部写入内核缓冲区,应该有一个应用层发送Buffer,当数据未全部写入内核时会先被填充到应用层发送Buffer,然后在epoll的LT模式下关注POLLOUT事件。POLLOUT事件触发会从应用层发送Buffer取出数据写入内核缓冲区,直到应用层发送Buffer数据全部写完,最后取消关注POLLOUT事件。

业务层需要关心的事件

  • 连接建立:OnConnection
  • 连接断开:OnConnection
  • 消息到达:OnMessage
  • 消息发送完毕(半个):OnWriteComplete,低流量的服务不必关心

对于消息到达事件:

connfd有数据到来时,先被内核接收存放在内核缓冲区中,然后网络库事件循环的可读事件被触发,将数据从内核缓冲区读到应用层Buffer中,并且网络库回调OnMessage函数,执行消息到达事件在业务层面的处理。

由于接收的TCP数据包可能是半包(数据不完整),应该在OnMessage里判断数据包是否完整,若接收的TCP数据包完整,则直接把数据包取出来进行处理,若接收的数据包不完整,则OnMesssage立即返回,这样内核下次接收到数据时,会继续触发网络库事件循环的可读事件,直到OnMessage判断数据包已经完整后才取出来处理。

对于消息发送完毕事件:

应用层要发送数据,如果内核发送缓冲区足够大,则把要发送的数据全部填入内核缓冲区中,并触发一个发送完成事件,网络库会回调OnWriteComplete函数表示消息发送完毕。

如果内核发送缓冲区不够,则将一部分数据填入内核缓冲区,剩余部分追加到应用层发送Buffer,等内核发送缓冲区将数据发送出去后会触发一个可写事件,在这个事件中就将应用层发送Buffer的数据继续填充到内核发送缓冲区(若内核发送缓冲区还是不够大,则再填充一部分),直到应用层发送缓冲区的数据全部填充完为止,网络库事件循环中的发送完成事件被触发,回调OnWriteComplete表示消息发送完毕。

业务层的处理逻辑如何向网络库传递

三个半事件的回调函数在业务层实现后,传递给TcpServer实例(TcpServer实例可以有默认三个半事件的回调函数),然后在TcpConnection对象创建时传递给TcpConnection对象,onConnection回调函数会被嵌入到TCP连接建立和释放处理代码里,onMessage回调函数会被嵌入到TCP连接读处理代码里,并在相应的场景下被调用。

Connection对象会把连接释放、读写处理和错误处理的函数传递给Connection对象里的Channel,这样IO-loop监听的channel有事件就绪时就能调用Channel相关的回调函数。

因为有了抽象和分层,所以需要回调函数作为中间媒介,优秀的回调函数设计需要依赖较高程度的抽象。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

RabbitMQ 可靠性、重复消费、顺序性、消息积压解决方案

发表于 2021-06-26

前言

上篇文章介绍了 为什么引入消息队列? 引入 MQ 给我们解决了一些问题,但同时又引入了一些复杂的问题,这些问题是大型项目中必须解决的重点,更重要的是,面试也经常问。实际上消息队列可以说是没法百分之百保证可靠性的!RabbitMQ 提供的相关机制也只是在于缩小消息丢失的概率,或者说提供了消息丢失后的我们可以记录日志的功能。 在解决这些问题时有必要明白一点,其实小公司业务量不大,并发量不高的情况下这些问题是几乎不会发生的……即使偶尔出现,开发人员手动修复数据处理就好。所以可结合公司实际业务场景看有没有必要解决这些问题

消息可靠性

以创建订单为例,可能会出现这样的业务场景

image.png

  • MQ 挂了,消息没发出去。创建订单后面几个优惠券、积分的下游系统全都没有执行业务结算怎么办?
  • MQ 是高可用的,消息发出去了,但是优惠券结算业务报错了怎么办?因为这个是异步的,也不好去回滚
  • 消息正常发出去,消费者也接收到了,商户系统、优惠券系统都正常执行完了,积分业务报错了导致积分没结算,那这个订单的数据就不一致了

要解决上述问题,就是要保证消息一定要可靠的被消费,那么我们可以来分析下消息有哪些步骤会出问题。RabbitMQ 发送消息是这样的:

image.png

消息被生产者发到指定的交换机根据路由规则路由到绑定的队列,然后推送给消费者。在这个过程中有可能会发生消息出问题的场景:

  • 生产者消息没到交换机,相当于生产者弄丢消息
  • 交换机没有把消息路由到队列,相当于生产者弄丢消息
  • RabbitMQ 宕机导致队列、队列中的消息丢失,相当于 RabbitMQ 弄丢消息
  • 消费者消费出现异常,业务没执行,相当于消费者弄丢消息

生产者弄丢消息

RabbitMQ 提供了确认和回退机制,有一个异步监听机制,每次发送消息,如果成功/未成功发送到交换机都可以触发一个监听,从交换机路由到队列失败也会有一个监听。只需要开启这两个监听机制即可,以 SpringBoot 整合 RabbitMQ 为例

引入依赖 starter

1
2
3
4
xml复制代码    <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

配置文件

1
2
3
yaml复制代码rabbitmq:
publisher-returns: true
publisher-confirm-type: correlated #新版本 publisher-confirms: true 已过时

然后编写监听回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
less复制代码@Configuration
@Slf4j
public class RabbitMQConfig {

@Autowired
private RabbitTemplate rabbitTemplate;

@PostConstruct
public void enableConfirmCallback() {
//confirm 监听,当消息成功发到交换机 ack = true,没有发送到交换机 ack = false
//correlationData 可在发送时指定消息唯一 id
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if(!ack){
//记录日志、发送邮件通知、落库定时任务扫描重发
}
});

//当消息成功发送到交换机没有路由到队列触发此监听
rabbitTemplate.setReturnsCallback(returned -> {
//记录日志、发送邮件通知、落库定时任务扫描重发
});
}
}

测试的时候可以在发送消息时故意写错交换机、路由键的名称,然后就会回调到我们刚刚写的监听方法, cause 会给我们展示具体没有发到交换机的原因;returned 对象中包含了消息相关信息。

实际上据我了解一些企业并不会在这两个监听里面去做重发,为什么呢?成本太高了……首先 RabbitMQ 本身丢失的可能性就非常低,其次如果这里需要落库再用定时任务扫描重发还要开发一堆代码,分布式定时任务……再其次定时任务扫描肯定会增加消息延迟,不是很有必要。真实业务场景是记录一下日志就行了,方便问题回溯,顺便发个邮件给相关人员,如果真的极其罕见的是生产者弄丢消息,那么开发往数据库补数据就行了。

RabbitMQ 弄丢消息

不开启持久化的情况下 RabbitMQ 重启之后所有队列和消息都会消失,所以我们创建队列时设置持久化,发送消息时再设置消息的持久化即可(设置 deliveryMode 为 2 就行了)。一般来说在实际业务中持久化是必须开的。

消费者弄丢消息

所谓消费端弄丢消息就是消费端执行业务代码报错了,那么该做的业务其实没有做。比如创建订单成功了,优惠券结算报错了,默认情况下 RabbitMQ 只要把消息推送到消费者就会认为消息已经被消费,就从队列中删除了,但是优惠券还没有结算,这样就相当于消息变相丢失了。这种情况还是很常见的,毕竟我们开发人员不能保证自己的代码不报错,这种问题一定得解决。 否则用户下了订单,优惠券没有扣减,你这个月的绩效估计是没了……

RabbitMQ 给我们提供了消费者应答(ack)机制,默认情况下这个机制是自动应答,只要消息推送到消费者就会自动 ack ,然后 RabbitMQ 删除队列中的消息。启用手动应答之后我们在消费端调用 API 手动 ack 确认之后,RabbitMQ 才会从队列删除这条消息。

首先在配置文件中开启手动 ack

1
2
3
4
5
yaml复制代码spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual #手动应答

然后在消费端代码中手动应答签收消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typescript复制代码    @RabbitListener(queues = "queue")
public void listen(String object, Message message, Channel channel) {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
log.info("消费成功:{},消息内容:{}", deliveryTag, object);
try {
/**
* 执行业务代码...
* */
channel.basicAck(deliveryTag, false);
} catch (IOException e) {
log.error("签收失败", e);
try {
channel.basicNack(deliveryTag, false, true);
} catch (IOException exception) {
log.error("拒签失败", exception);
}
}
}

踩坑经验

如果生产环境你用上述方案的代码,一旦发生一次消费报错你就会崩溃。因为 basicNack 方法的第三个参数代表是否重回队列,如果你填 false 那么消息就直接丢弃了,相当于没有保障消息可靠。如果你填 true ,当发生消费报错之后,这个消息会被重回消息队列顶端,继续推送到消费端,继续消费这条消息,通常代码的报错并不会因为重试就能解决,所以这个消息将会出现这种情况:继续被消费,继续报错,重回队列,继续被消费……死循环

所以真实的场景一般是三种选择

  • 当消费失败后将此消息存到 Redis,记录消费次数,如果消费了三次还是失败,就丢弃掉消息,记录日志落库保存
  • 直接填 false ,不重回队列,记录日志、发送邮件等待开发手动处理
  • 不启用手动 ack ,使用 SpringBoot 提供的消息重试

SpringBoot 提供的消息重试

其实很多场景并不是一定要启用消费者应答模式,因为 SpringBoot 给我们提供了一种重试机制,当消费者执行的业务方法报错时会重试执行消费者业务方法。

启用 SpringBoot 提供的重试机制

1
2
3
4
5
6
7
yaml复制代码spring:
rabbitmq:
listener:
simple:
retry:
enabled: true
max-attempts: 3 #重试次数

消费者代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
php复制代码    @RabbitListener(queues = "queue")
public void listen(String object, Message message, Channel channel) throws IOException {
try {
/**
* 执行业务代码...
* */
int i = 1 / 0; //故意报错测试
} catch (Exception e) {
log.error("签收失败", e);
/**
* 记录日志、发送邮件、保存消息到数据库,落库之前判断如果消息已经落库就不保存
* */
throw new RuntimeException("消息消费失败");
}
}

注意一定要手动 throw 一个异常,因为 SpringBoot 触发重试是根据方法中发生未捕捉的异常来决定的。值得注意的是这个重试是 SpringBoot 提供的,重新执行消费者方法,而不是让 RabbitMQ 重新推送消息。

重试无法解决

值得注意的是很多时候消息消费时候并不是由于网络抖动的原因,而是由于代码bug。这种情况下,我们重试多少次也都还是不会消费成功,必须得修改代码,发布之后重新消费消息。这其实是一个很常见的问题,同时也是一个比较棘手的问题。

因为我们要重新消费消息就要知道这条消息的消息体、对应的用户等信息,那么我们只能去捞日志。在数据量大或者日志多的情况下这无疑对消息的追溯很不友好。

对此我们可以采取的方案是发消息之前落库,使用 Spring 事件机制,我们先把消息体、用户id等信息落库,待事务提交之后发送消息,发送成功之后再更新消息的发送状态。

消息落库的优劣

消息落库的优势一目了然,对于消息的追溯很友好,如果发现哪个消息发送失败,或者消费失败,直接从消息表获取这条消息的消息体,重新执行一次消费方法即可。

同时劣势也是一目了然,由于消息要落库,就存在数据库磁盘IO,在极大的并发下,接口吞吐量会被降低,数据库也会抵挡不住压力。由于我们使用消息队列就是用来进行削峰、异步、解耦的,这样一来似乎让我们并没有真正的削峰,对于这个问题我们也可以考虑将消息库表水平拆分,这样可以将压力均分下去。

消息可靠性总结

其实认真研究下来你会发现所谓的消息可靠性本身就是无法保证的……所谓的各种可靠性机制只是为了以后消息丢失提供可查询的日志而已,不过通过这些机制耗费一些(巨大)成本的确是能够缩小消息丢失的可能性

消息顺序性

有些业务场景会需要让消息顺序消费,比如使用 canal 订阅 MySQL 的 binary 日志来更新 Redis,通常我们会把 canal 订阅到的数据变化发送到消息队列。

image.png

如果不保证 RabbitMQ 的顺序消费, Redis 中就有可能会出现脏数据。

单个消费者实例

其实队列本身是有顺序的,但是生产环境服务实例一般都是集群,当消费者是多个实例时,队列中的消息会分发到所有实例进行消费(同一个消息只能发给一个消费者实例),这样就不能保证消息顺序的消费,因为你不能确保哪台机器执行消费端业务代码的速度快

image.png

所以对于需要保证顺序消费的业务,我们可以只部署一个消费者实例,然后设置 RabbitMQ 每次只推送一个消息,再开启手动 ack 即可,配置如下

1
2
3
4
5
6
yaml复制代码spring:
rabbitmq:
listener:
simple:
prefetch: 1 #每次只推送一个消息
acknowledge-mode: manual

这样 RabbitMQ 每次只会从队列推送一个消息过来,处理完成之后我们 ack 回应,再消费下一个,就能确保消息顺序性。

多个消费者实例

RabbitMQ 多消费实例情况下要想保证消息的顺序性,非常困难,细节非常多,一句话:我不会……

消息重复消费(幂等性)

这个也是生产环境业务中经常出现的场景,我的博客使用了 RabbitMQ ,就很奇怪经常日志上会显示消息被消费了两次。

我们解决消息重复消费有两种角度,第一种就是不让消费端执行两次,第二种是让它重复消费了,但是不会对我的业务数据造成影响就行了。

确保消费端只执行一次

一般来说消息重复消费都是在短暂的一瞬间消费多次,我们可以使用 redis 将消费过的消息唯一标识存储起来,然后在消费端业务执行之前判断 redis 中是否已经存在这个标识。举个例子,订单使用优惠券后,要通知优惠券系统,增加使用流水。这里可以用订单号 + 优惠券 id 做唯一标识。业务开始先判断 redis 是否已经存在这个标识,如果已经存在代表处理过了。不存在就放进 redis 设置过期时间,执行业务。

1
2
3
4
5
6
7
8
kotlin复制代码    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("orderNo+couponId");
//先检查这条消息是不是已经消费过了
if (!Boolean.TRUE.equals(flag)) {
return;
}
//执行业务...
//消费过的标识存储到 Redis,10 秒过期
stringRedisTemplate.opsForValue().set("orderNo+couponId","1", Duration.ofSeconds(10L));

允许消费端执行多次,保证数据不受影响

  • 数据库唯一键约束

如果消费端业务是新增操作,我们可以利用数据库的唯一键约束,比如优惠券流水表的优惠券编号,如果重复消费将会插入两条相同的优惠券编号记录,数据库会给我们报错,可以保证数据库数据不会插入两条。

  • 数据库乐观锁思想

如果消费端业务是更新操作,可以给业务表加一个 version 字段,每次更新把 version 作为条件,更新之后 version + 1。由于 MySQL 的 innoDB 是行锁,当其中一个请求成功更新之后,另一个请求才能进来,由于版本号 version 已经变成 2,必定更新的 SQL 语句影响行数为 0,不会影响数据库数据。

消息(堆积)积压

所谓消息积压一般是由于消费端消费的速度远小于生产者发消息的速度,导致大量消息在 RabbitMQ 的队列中无法消费。

其实这玩意我也不知道为什么面试这么喜欢问…..既然消费者速度跟不上生产者,那么提高消费者的速度就行了呀!个人认为有以下几种思路

  • 对生产者发消息接口进行适当限流(不太推荐,影响用户体验)
  • 多部署几台消费者实例(推荐)
  • 适当增加 prefetch 的数量,让消费端一次多接受一些消息(推荐,可以和第二种方案一起用)

结语

如果这篇文章对你有帮助,记得点赞加关注。你的支持就是我继续创作的动力!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Golang与Java各方面使用对比(下)

发表于 2021-06-26

一、面向对象

1.1、与Java面向对象的区别

Golang是一门具备面向对象编程风格的语言,但是却不具备Java等传统面向对象语言中“继承(extends)、实现(implements)”的关键字。

在Golang中,通过接口或结构体的组合来实现非严格的“继承”,通过非侵入式的接口来实现非严格的“多态”,通过结构体及包和函数实现了代码细节的“封装”,有了封装、继承与多态,就可以很好地通过OO思维实现与现实需求所对应的程序了。

1.2、结构体组合

假设有这么一个场景:动物(Animal)具备名字(Name)、年龄(Age)的基本特性,现在需要实现一个Dog类型,且Dog类型需要具备Animal所需的所有特性,并且自身具备犬吠(bark())的方法,使用Java和Golang来实现该场景会有什么区别呢?

首先来看看最熟悉的Java要如何写,很简单,使用抽象类描述Animal作为所有动物的超类,Dog extends Animal:

  • Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public abstract class Animal {
protected String name;
protected int age;
}

public class Dog extends Animal {
public void bark() {
System.out.println(age + "岁的" + name + "在汪汪汪...");
}
}

public class Test {
public static void main(String[] args) {
Dog dog = new Dog();
dog.name = "tom";
dog.age = 2;
dog.bark(); // 2岁的tom在汪汪汪...
}
}

在Golang中,可以这样通过结构体的组合来实现继承:

  • Golang
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
go复制代码package oom

type Animal struct {
Name string
Age int
}

type Dog struct {
*Animal
}

func (d *Dog) Bark() {
fmt.Printf("%d岁的%s在汪汪汪...", d.Age, d.Name)
}

// ----------
package main

func main() {
dog := &oom.Dog{&oom.Animal{
Name: "tom",
Age: 2,
}}
dog.Bark() // 2岁的tom在汪汪汪...
}

Golang使用了非侵入式接口来实现“多态”。

1.3、非侵入式接口

Go语言的接口并不是其他语言(C++、Java、C#等)中所提供的接口概念。
在Go语言出现之前,接口主要作为不同组件之间的契约存在。对契约的实现是强制的,你必须声明你的确实现了该接口。为了实现一个接口,你需要从该接口继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码interface IFoo {
void Bar();
}

class Foo implements IFoo { // Java文法
// ...
}

class Foo : public IFoo { // C++文法
// ...
}

IFoo foo = new Foo;

这类接口我们称为侵入式接口。“侵入式”的主要表现在于实现类需要明确声明自己实现了某个接口。这种强制性的接口继承是面向对象编程思想发展过程中一个遭受相当多置疑的特性。

Golang的非侵入式接口不需要通过任何关键字声明类型与接口之间的实现关系,只要一个类型实现了接口的所有方法,那么这个类型就是这个接口的实现类型。

假设现在有一个Factory接口,该接口中定义了Produce()方法及Consume()方法,CafeFactory结构体作为其实现类型,那么可以通过以下代码实现:

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
go复制代码package oom

type Factory interface {
Produce() bool
Consume() bool
}

type CafeFactory struct {
ProductName string
}

func (c *CafeFactory) Produce() bool {
fmt.Printf("CafeFactory生产%s成功", c.ProductName)
return true
}

func (c *CafeFactory) Consume() bool {
fmt.Printf("CafeFactory消费%s成功", c.ProductName)
return true
}

// --------------
package main

func main() {
factory := &oom.CafeFactory{"Cafe"}
doProduce(factory)
doConsume(factory)
}


func doProduce(factory oom.Factory) bool {
return factory.Produce()
}

func doConsume(factory oom.Factory) bool {
return factory.Consume()
}

可以看到,只要CafeFactory实现了所有的Factory方法,那么它就是一个Factory了,而不需要使用implements关键字去显式声明它们之间的实现关系。

Golang的非侵入式接口有许多好处:

1.在Go中,类型的继承树并无意义,我们只需要知道这个类型实现了哪些方法,每个方法是啥含义就足够了

2.实现类型的时候,只需要关心自己应该提供哪些方法,不用再纠结接口需要拆得多细才合理。接口由使用方按需定义,而不用事前规划

3.不用为了实现一个接口而导入一个包,因为多引用一个外部的包,就意味着更多的耦合。接口由使用方按自身需求来定义,使用方无需关心是否有其他模块定义过类似的接口

一句话总结非侵入式接口的好处就是简单、高效、按需实现。

1.4、interface{}空接口

interface{} 空接口是任意类型的接口,所有的类型都是空接口的实现类型。因为Golang对于实现类型的要求是实现了接口的所有方法,而空接口不存在方法,所以任意类型都可以充当空接口。

以下是一个使用空接口充当参数的类型判断例子:

1
2
3
4
5
6
7
8
9
10
go复制代码func getType(key interface{}) string {
switch key.(type) {
case int:
return "this is a integer"
case string:
return "this is a string"
default:
return "unknown"
}
}

二、异常处理

2.1、与Java异常处理的区别

在Java中通过try..catch..finally的方式进行异常处理,有可能出现异常的代码会被try块给包裹起来,在catch中捕获相关的异常并进行处理,最后通过finally块来统一执行最后的结束操作(释放资源、释放锁)。

而Golang中的异常处理(更贴切地说是错误处理)方式比Java的简单太多,所有可能出现异常的方法或者代码直接把错误当作第二个响应值进行返回,程序中对返回值进行判断,非空则进行处理并且立即中断程序的执行,避免错误的传播。

1
2
3
4
5
6
7
8
9
10
go复制代码value, err := func(param)

if err != nil {
// 返回了异常,进行处理
fmt.Printf("Error %s in pack1.Func1 with parameter %v", err.Error(), param1)
return err
}

// func执行正确,继续执行后续代码
Process(value)

Golang引入了一个关于错误处理的标准模式,即error接口,该接口的定义如下:

1
2
3
go复制代码type error interface {
Error() string
}

对于大多数函数,如果要返回错误,大致上都可以定义为如下模式,将 error 作为多种返回值中的最后一个,但这并非是强制要求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码unc main() {
if res, err := compute(1, 2, "x"); err != nil {
panic(err)
} else {
fmt.Println(res)
}
}

func compute(a, b int, c string)(res int, err error) {
switch c {
case "+" :
return a + b, nil
case "-":
return a - b, nil
case "*":
return a * b, nil
case "/":
return a / b, nil
default:
return -1, fmt.Errorf("操作符不合法")
}
}

当然了,Golang中也可以像Java一样灵活地自定义错误类型,定义PathError结构体,并且实现Error接口后,该结构体就是一个错误类型了:

  • PathError
1
2
3
4
5
6
7
8
9
go复制代码type PathError struct {
Op string
Path string
Err error
}

func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
  • main
1
2
3
4
5
6
7
8
9
10
go复制代码    func GetStat(name string) (fi FileInfo, err error) {
var stat syscall.Stat_t
err = syscall.Stat(name, &stat)
if err != nil {
// 返回PathError错误类型
return nil, &PathError {"stat", name, err}
}
// 程序正常,返回nil
return fileInfoFromStat(&stat, name), nil
}

这种异常处理方式是Golang的一大特色,外界对这种异常处理方式有褒有贬:

  1. 优点:代码清晰,所有的异常都需要被考虑到,出现异常后马上就需要处理
  2. 缺点:代码冗余,所有的异常都需要通过if err != nil {}去做判断和处理,不能够做到统一捕捉和处理

2.2、逗号 ok 模式

在使用Golang编写代码的过程中,许多方法经常在一个表达式返回2个参数时使用这种模式:,ok,第一个参数是一个值或者nil,第二个参数是true/false或者一个错误error。在一个需要赋值的if条件语句中,使用这种模式去检测第二个参数值会让代码显得优雅简洁。这种模式在Golang编码规范中非常重要。这也是Golang自身的函数多返回值特性的体现。

2.3、defer、panic及recover

defer、pannic及recover是Golang错误处理中常用的关键字,它们各自的用途为:

2.3.1、defer

defer的作用是延迟执行某段代码,一般用于关闭资源或者执行必须执行的收尾操作,无论是否出现错误defer代码段都会执行,类似于Java中的finally代码块的作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码     func CopyFile(dst, src string) (w int64, err error) {
srcFile, err := os.Open(src)
if err != nil {
return
}
// 延迟关闭srcFile
defer srcFile.Close()
dstFile, err := os.Create(dstName)
if err != nil {
return
}
// 延迟关闭dstFile
defer dstFile.Close()
return io.Copy(dstFile, srcFile)
}

defer也可以执行函数或者是匿名函数:

1
2
3
4
5
6
7
8
9
go复制代码defer func() {
// 清理工作
} ()

// 这是传递参数给匿名函数时的写法
var i := 1
defer func(i int) {
// 做你复杂的清理工作
} (i)

需要注意的是,defer使用一个栈来维护需要执行的代码,所以defer函数所执行的顺序是和defer声明的顺序相反的。

1
2
3
go复制代码defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)

上述执行结果为

1
2
3
go复制代码3
2
1
2.3.2、panic

panic的作用是抛出错误,制造系统运行时恐慌,当在一个函数执行过程中调用panic()函数时,正常的函数执行流程将立即终止,但函数中之前使用defer关键字延迟执行的语句将正常展开执行,之后该函数将返回到调用函数,并导致逐层向上执行 panic流程,直至所属的goroutine中所有正在执行的函数被终止。

panic和Java中的throw关键字类似,用于抛出错误,阻止程序执行。

以下是基本使用方法:

1
2
3
go复制代码panic(404)
panic("network broken")
panic(Error("file not exists"))
2.3.3、recover

recover的作用是捕捉panic抛出的错误并进行处理,需要联合defer来使用,类似于Java中的catch代码块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码func main() {
fmt.Println("main begin")
// 必须要先声明defer,否则不能捕获到panic异常
defer func() {
fmt.Println("defer begin")
if err := recover(); err != nil {
// 这里的err其实就是panic传入的内容
fmt.Println(err)
}
fmt.Println("defer end")
}()
f()
// f中出现错误,这里开始下面代码不会再执行
fmt.Println("main end")
}

func f() {
fmt.Println("f begin")
panic("error")
//这里开始下面代码不会再执行
fmt.Println("f end")
}

最后的执行结果为:

1
2
3
4
5
go复制代码main begin
f begin
defer begin
error
defer end

利用recover处理panic指令,defer必须在panic之前声明,否则当panic时,recover无法捕获到panic。

三、并发编程

3.1、CSP(MPG)并发模型介绍及对比

在Java中,通常借助于共享内存(全局变量)作为线程间通信的媒介,但在Golang中使用的是通道(channel)作为协程间通信的媒介,这也是Golang中强调的:

不要通过共享内存通信,而通过通信来共享内存

在Java中,使用共享内存来进行通信常会遇到线程不安全问题,所以我们经常需要进行大量的额外处理,方式包括加锁(同步化)、使用原子类、使用volatile提升可见性等等。

CSP的指的是是Communicating Sequential Processes (CSP)的缩写,中文为顺序通信进程。CSP的核心思想是多个线程之间通过Channel来通信(对应到golang中的chan结构),这里的Channel可以理解为操作系统中的管道或者是消息中间件(不同之处在于这个MQ是为不同协程间服务的,而不是进程)

说到了CSP就得提一下Golang自身的并发模型MPG,MPG中M指的是内核线程、P指的是上下文环境、G指的是协程,其中M与P一起构成了G可运行的环境,M和P是一一对应关系,通过P来动态地对不同的G做映射和控制,所以Golang中的协程是建立在某个线程之上的用户态线程。

在这里插入图片描述

具体的细节譬如MPG如何映射、G的状态有哪些、调度器如何工作等不在此文展开。

3.2、Goroutine及Channel的使用

在Java中开启一个线程需要创建Thread实现类或Runnable实现类、重写run方法、通过t.start()开启线程执行特定任务,但在Golang中要开启一个Goroutine十分简单,只需使用go这个关键字即可。

1
2
3
4
5
6
7
8
9
10
go复制代码// 开启协程执行一段代码
go fmt.Println("go")

// 开启协程执行函数
go SomeMethod(1, 1)

// 开启协程执行匿名函数
go func() {
go fmt.Println("go")
}()

关于协程,有一些注意点:

  1. main函数运行的协程为主协程,其他协程为主协程的守护协程,当主协程死亡其它协程也会死亡
  2. 协程在执行完所需执行的方法及代码后会死亡,遇到panic导致程序结束时也会死亡

channel是Golang在语言级别提供的goroutine间的通信方式。我们可以使用channel在两个或多个goroutine之间传递消息,因此通过channel传递对象的过程和调用函数时的参数传递行为比较一致,比如也可以传递指针等。

channel是类型相关的。也就是说,一个channel只能传递一种类型的值,这个类型需要在声明channel时指定。

一般channel的声明形式为:

var chanName chan ElementType

与一般的变量声明不同的地方仅仅是在类型之前加了chan关键字。 ElementType 指定这个channel所能传递的元素类型。举个例子,我们声明一个传递类型为 int channel:

var ch chan int

或者,我们声明一个 map ,元素是 bool 型的channel:

var m map[string] chan bool

初始化一个channel也很简单,直接使用内置的函数 make() 即可:

ch := make(chan int)

在channel的用法中,最常见的包括写入和读出。将一个数据写入(发送)至channel的语法很直观,如下:

ch <- value

向channel写入数据通常会导致程序阻塞,直到有其他goroutine从这个channel中读取数据。从channel中读取数据的语法是

value := <-ch

如果channel之前没有写入数据,那么从channel中读取数据也会导致程序阻塞,直到channel中被写入数据为止。我们之后还会提到如何控制channel只接受写或者只允许读取,即单向channel。

channel有如下特性:

  1. 读取、写入操作为原子操作,无需担心并发时的数据安全问题,channel内数据的写入对所有协程可见
  2. channel中阻塞的协程是FIFO的,严格按照入队顺序读写数据
  3. 对于非缓冲channel的读取和写入是同步发生的,写入会阻塞直到有读者,读取会阻塞直到有写者,类似于Java中的synchronousqueue;对于缓冲channel的读取和写入是异步的,写入时若队列已满则阻塞,直到有读者、读取时若队列为空则阻塞,直到有写者,类似于Java中的linkedblockingqueue
  4. 对于为nil的channel的写入和读取都会永久阻塞

四、垃圾回收

4.1、Java的垃圾回收体系

Java基于JVM完成了垃圾收集的功能,其体系很庞大,包括了垃圾回收器(G1、CMS、Serial、ParNew等)、垃圾回收算法(标记-清除、标记-整理、复制、分代收集)、可达性算法(可达性分析、引用计数法)、引用类型、JVM内存模型等内容,目前Java在JDK 1.7开始使用G1垃圾收集器来进行垃圾回收,其特性及回收过程大致如下:

在这里插入图片描述

4.2、Golang三色标记法

三色标记法,主要流程如下:

  1. 所有对象最开始都是白色
  2. 从root开始找到所有可达对象,标记为灰色,放入待处理队列
  3. 遍历灰色对象队列,将其引用对象标记为灰色放入待处理队列,自身标记为黑色
  4. 处理完灰色对象队列,执行清扫工作

要进一步学习可以参考这篇文章:legendtkl.com/2017/04/28/…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Golang与Java各方面使用对比(上)

发表于 2021-06-26

一、基本情况

1.1、Golang基本介绍

在这里插入图片描述

Go语言(或 Golang)起源于 2007 年,并在 2009 年正式对外发布。Go 是非常年轻的一门语言,它的主要目标是“兼具 Python 等动态语言的开发速度和 C/C++ 等编译型语言的性能与安全性”。

Go语言的推出,旨在不损失应用程序性能的情况下降低代码的复杂性,具有“部署简单、并发性好、语言设计良好、执行性能好”等优势,目前国内诸多 IT 公司均已采用Go语言开发项目。

Go语言有时候被描述为“C 类似语言”,或者是“21 世纪的C语言”。Go 从C语言继承了相似的表达式语法、控制流结构、基础数据类型、调用参数传值、指针等很多思想,还有C语言一直所看中的编译后机器码的运行效率以及和现有操作系统的无缝适配。

因为Go语言没有类和继承的概念,所以它和 Java 或 C++ 看起来并不相同。但是它通过接口(interface)的概念来实现多态性。Go语言有一个清晰易懂的轻量级类型系统,在类型之间也没有层级之说。因此可以说Go语言是一门混合型的语言。

此外,很多重要的开源项目都是使用Go语言开发的,其中包括 Docker、Go-Ethereum、Thrraform 和 Kubernetes。

据最新的2020年9月Tiobe编程语言排行榜,目前Golang的使用率排名为第11:

在这里插入图片描述

1.2、Golang使用场景

  1. 服务端开发(配合gin、gorm等库就能够完成高性能的后端服务)
  2. 容器开发(譬如Docker、K8s都是基于Golang开发的)
  3. 脚本开发(由于Golang自身部署简单,并且与操作系统API交互方便,所以还可替代Python作为脚本开发)
  4. 底层工具的开发(可代替C或者C++开发操作系统底层工具)

二、基本语法

2.1、编码规约

Golang是一门严格的工程语言,主要体现在编码风格及可见域规则上。在Java中,允许多种编码风格共存,譬如以下两种方法声明,对于Java来说都是允许的:

1
2
3
4
5
6
7
8
java复制代码public String getString(Integer num) {
return num.toString();
}

public String getString(Integer num)
{
return num.toString();
}

1.左右花括号需要符合上下换行风格

在Golang中,只允许出现一种换行风格:

1
2
3
go复制代码func getString(num int) string {
return strconv.Itoa(num)
}

如果出现以下换行风格则会报错,无法通过编译:

在这里插入图片描述

2.变量声明后必须使用,不使用需要使用_来代替

在Java中,变量可以声明了却不使用,而Golang中声明的变量必须被使用,否则需要使用_来替代掉变量名,表明该变量不会比使用到:

1
2
3
4
5
go复制代码func getString(num int) string {
temp := num // 没有使用者,无法编译
_ := num // 正常编译
return strconv.Itoa(num)
}

3.可见域规则

Java对方法、变量及类的可见域规则是通过private、protected、public关键字来控制的,而Golang中控制可见域的方式只有一个,当字段首字母开头是大写时说明其是对外可见的、小写时只对包内成员可见。

  • entity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
go复制代码package entity

type Person struct {
Name string
Age int
id string
}

type student struct {
detail Person
}

func test() {
// 本包内可见
person := &student{detail: Person{
Name: "ARong",
Age: 21,
id: "211",
}}
fmt.Println(person)
}
  • main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码package main

import (
"fmt"
entity "others/scope"
)

func main() {
// id字段不可见
person := &entity.Person{
Name: "ARong",
Age: 21,
}
fmt.Println(person)
}

2.2、变量声明及初始化

1.变量声明及初始化的文法
在Java中,通常声明变量及初始化的文法为:

1
2
java复制代码    // Object:要声明的类型、v:变量名称、new Object()变量初始化
Object v = new Object();

而Golang使用var关键字来声明变量:

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码    // var:变量定义、v1:变量名称、int:变量类型
var v1 int
var v2 string
var v3 [10]int // 数组
var v4 []int // 数组切片
var v5 struct {
f int
}
var v6 *int // 指针
var v7 map[string]int // map,key为string类型,value为int类型
var v8 func(a int) int
var v9,v10 int //v9和v10都声明为int型

也可以采用:=自动推测变量类型:

1
2
3
go复制代码	var v1 int = 10 // 正确的使用方式1
var v2 = 10 // 正确的使用方式2,编译器可以自动推导出v2的类型
v3 := 10 // 正确的使用方式3,编译器可以自动推导出v3的类型

2.对于基本类型,声明即初始化;对于引用类型,声明则初始化为nil

在Java中,如果在方法内部声明一个变量但不初始化,在使用时会出现编译错误:

1
2
3
4
5
6
java复制代码public void solve() {
int num;
Object object;
System.out.println(num); // 编译错误
System.out.println(object); // 编译错误
}

而在Golang中,对于基本类型来讲,声明即初始化;对于引用类型,声明则初始化为nil。这样可以极大地避免NPE的发生。

1
2
3
4
5
6
go复制代码func main() {
var num int
var hashMap *map[string]int
fmt.Println(num) // num = 0
fmt.Println(hashMap) // &hashMap== nil
}

2.3、值类型及引用类型

Golang的类型系统与Java相差不大,但是需要注意的是Java中的数组是属于引用类型,而Golang中的数组属于值类型,当向方法中传递数组时,Java可以直接通过该传入的数组修改原数组内部值(浅拷贝),但Golang则会完全复制出一份副本来进行修改(深拷贝):

  • Java
1
2
3
4
5
6
7
8
9
java复制代码    public static void main(String[] args) {
int[] array = {1, 2, 3};
change(array);
System.out.println(Arrays.toString(array)); // -1,2,3
}

private static void change(int[] array) {
array[0] = -1;
}
  • Golang
1
2
3
4
5
6
7
8
9
go复制代码func main() {
array := [...]int{1, 2, 3}
change(array)
fmt.Println(array) // 1,2,3
}

func change(array [3]int) {
array[0] = -1
}

并且值得注意的是,在Golang中,只有同长度、同类型的数组才可视为“同一类型”,譬如[2]int和[3]int则会被视为不同的类型,这在参数传递的时候会造成编译错误。

所以在Golang中数组很少被直接使用,更多的是使用切片(基于数组指针)来代替数组。

在Golang中,只有切片、指针、channel、map及func属于引用类型,也就是在传递参数的时候,实质上复制的都是他们的指针,内部的修改会直接影响到外部:

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
go复制代码func main() {
slice := []int{1, 2, 3}
changeSlice(slice)
fmt.Println(slice) // -1,2,3

mapper := map[string]int {
"num": 0,
}
changeMap(mapper)
fmt.Println(mapper) // num = -1

array := [...]int{1, 2, 3}
changePointer(&array)
fmt.Println(array) // -1,2,3

intChan := make(chan int, 1)
intChan <- 1
changeChannel(intChan)
fmt.Println(<- intChan) // -1
}

func changeChannel(intChan chan int) {
<- intChan
intChan <- -1
}

func changePointer(array *[3]int) {
array[0] = -1
}

func changeMap(mapper map[string]int) {
mapper["num"] = -1
}

func changeSlice(array []int) {
array[0] = -1
}

三、结构体、函数及指针

3.1、结构体声明及使用

在Golang中区别与Java最显著的一点是,Golang不存在“类”这个概念,组织数据实体的结构在Golang中被称为结构体。函数可以脱离“类”而存在,函数可以依赖于结构体来调用或者依赖于包名调用。

Golang中的结构体放弃了继承、实现等多态概念,结构体之间可使用组合来达到复用方法或者字段的效果。

要声明一个结构体只需使用type + struct关键字即可:

1
2
3
4
5
go复制代码type Person struct {
Name string
Age int
id string
}

要使用一个结构体也很简单,一般有以下几种方式去创建结构体:

1
2
3
4
5
6
7
8
9
go复制代码    personPoint := new(entity.Person) // 通过new方法创建结构体指针
person1 := entity.Person{} // 通过Person{}创建默认字段的结构体
person2 := entity.Person{ // 通过Person{Name:x,Age:x}创建结构体并初始化特定字段
Name: "ARong",
Age: 21,
}
fmt.Println(personPoint) // &{ 0 }
fmt.Println(person1) // { 0 }
fmt.Println(person2) // {ARong 21 }

3.2、函数和方法的区别

使用Java的朋友应该很少使用“函数”这个词,因为对于Java来说,所有的“函数”都是基于“类”这个概念构建的,也就是只有在“类”中才会包含所谓的“函数”,这里的“函数”被称为“方法”。

而“函数”这个词源于面向过程的语言,所以在Golang中,“函数”和“方法”的最基本区别是:

函数不基于结构体而是基于包名调用,方法基于结构体调用。

下面是一个例子,可以直观地看出方法和函数的区别:

  • entity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码package entity

import "fmt"

type Person struct {
Name string
Age int
id string
}

// Person结构体/指针可调用的"方法",属于Person结构体
func (p *Person) Solve() {
fmt.Println(p)
}

// 任何地方都可调用的"函数",不属于任何结构体,可通过entity.Solve调用
func Solve(p *Person) {
fmt.Println(p)
}
  • main
1
2
3
4
5
6
7
go复制代码func main() {
personPoint := new(entity.Person) // 通过new方法创建结构体指针

entity.Solve(personPoint) // 函数调用

personPoint.Solve() // 方法调用
}

3.3、指针的使用

在Java中不存在显式的指针操作,而Golang中存在显式的指针操作,但是Golang的指针不像C那么复杂,不能进行指针运算。

下面从一个例子来看Java的隐式指针转化和Golang的显式指针转换:Java和Golang方法传参时传递的都是值类型,在Java中如果传递了引用类型(对象、数组等)会复制其指针进行传递, 而在Golang中必须要显式传递Person的指针,不然只是传递了该对象的一个副本。

Golang使用 * 来定义和声明指针,通过&来取得对象的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码func main() {
p1 := entity.Person{
Name: "ARong1",
Age: 21,
}
changePerson(p1)
fmt.Println(p1.Name) // ARong1
changePersonByPointer(&p1)
fmt.Println(p1.Name) // ARong2
}

func changePersonByPointer(person *entity.Person) {
person.Name = "ARong2"
}

func changePerson(person entity.Person) {
person.Name = "ARong2"
}

注意,如果结构体中需要组合其他结构体,那么建议采用指针的方式去声明,否则会出现更新丢失问题。

以下是Golang方法的一个隐式指针转换,结构体调用方法时,如果传递的是对象,那么会被自动转化为指针调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码
type Person struct {
Name string
Age int
}

// Person结构体/指针可调用的"方法",属于Person结构体
func (p *Person) Solve() {
fmt.Println(p)
}


func main() {
p := entity.Person{
Name: "ARong",
Age: 21,
}

pp := &p
pp.Solve() // 显式

p.Solve // 隐式,自动将p转化为&p
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Django REST framework 完结

发表于 2021-06-26

写在前面

关于 DRF 的文章写了也有一段时间了,功能上也都有涉及到。今天就对这些天写的文章做一个总结,总结技术的同时也总结一下工作。

项目目录

项目目录按照入门顺序已排序,希望这十篇文章,每一篇都能带给你灵感与帮助。

  • Django REST framework 入门
  • Django REST framework 登录之JWT
  • Django REST framework 登录之实战
  • Django REST framework 全文搜索实战
  • Django REST framework 代码风格实战
  • Django REST framework 文档、日志
  • Django REST framework 异常处理
  • Django REST framework 限流
  • Django REST framework 部署实战
  • Django REST framework Docker 部署

实战分享

代码仓库详情见: Github 。

1. 关于技术选型

关于 Python 的 Web 框架有许多,比较流行的框架有 Django、Flask、Tornado,随着 python社区日益壮大发展,近两年也涌现出很多优秀的 web 框架,具体可以参考我 Python异步框架 。

Django 的优势我在入门的文章中有提到,Django 有着丰富的周边生态以及较为全面的文档支持。这一点优势其实 Flask 也有,不过 Flask 相对于 Django 来说各有千秋。总结来说就是 Django 大而全,定制化的能力较弱,功能只需要按照官方文档配置;Flask 小而精,有着丰富的第三方插件支持,第三方功能需要自行安装配置。

这里选择 Django 的原因就是因为,懒。没有太多的时间做过多的配置,本着拿来主义的原则。上手就是一通开发、测试、迭代,就上线了。一天一个小版本,一周一个迭代,一个月一次重构。

2. 关于数据库

Django 支持多种关系型数据库 PostgreSQL、SQLite、MySQL、Oracl等。一般建议自己学习就 SQLite,不需要在数据库上花太多的时间。其他情况根据公司技术做选择就行。

这里需要注意的是默认的 migrate 命令,只能在开发环境使用。生产环境应该使用 SQL 操作数据库,一来避免不可预见的风险,二来更符合开发规范。

默认的 migrate 命令,只能在开发环境使用。

默认的 migrate 命令,只能在开发环境使用。

默认的 migrate 命令,只能在开发环境使用。

3. 关于 Django Admin

建议一定要使用 Admin 的页面,因为它的使用会大大减少线上上线之后 SQL 提交的次数。对开放人员极其友好,但是超级管理员的权限只能赋予少部分人员(这玩意太强大)。

建议可以定制一些命令行工具,比如一些在开发环境用到的脚本,初始化数据库、导出数据的脚本、数据库差异等工具,这样可以使你的开发效率成倍提升

  • Django 管理站点
  • django-extensions

4. 关于代码仓库

关于整个 DRF 专栏的项目代码已经整理好放在我的 Github 。欢迎有需要的小伙伴 STAR & FORK。
当然如果有哪里写的不好写的不正确的,也欢迎小伙伴指证,我会及时修正。

5. 关于 DRF 其他内容

这里关于 DRF 的知识点,以及作为一个开框架来说,基本上是够用的。有关于 ViewSet Serializer Filter 的功能因为会根据具体业务有着不同的使用方式,所以这里并没有深入讲解,需要有需求的小伙伴,在使用的时候结合 官方文档使用。

当然如果你有好的功能点想让我继续更新,也欢迎沟通交流。

6. 其他你可能会用到的技术

  • Celery Python开发的分布式任务调度模块。

工作场景有定时任务、异步任务调度等。在 Django 中可以直接使用 django-celery ,它为我们封装了所有关于 Celery 的基本功能与操作。

  • Redis 基于内存实现的高性能非关系型数据库。

一般有以下的业务场景:

+ 缓存 对热数据进行缓存,减轻数据压力
+ 队列 配合 Celery 实现消息的消费监控等功能
+ 分布式锁 基于 SETNX 命令的原子性操作实现解决资源竞争的问题

Redis 是完全开源免费的,遵守 BSD 协议,是一个灵活的高性能 key-value 数据结构存储,可以用来作为数据库、缓存和消息队列。

项目推荐

推荐几款热门开源的 Django 项目,供大家学习研究

  • 使用Django快速搭建博客
  • Django-CMS
  • Jumpserver 堡垒机

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

测试平台系列(2) 给Pity添加配置

发表于 2021-06-26

给Pity添加配置

回顾

还记得上篇文章创立的**「Flask」**实例吗?我们通过这个实例,给根路由 「/」 绑定了一个方法,从而使得用户访问不同路由的时候可以执行不同的方法。

配置

要知道,在一个**「Web」**项目中,有很多东西是可能会产生变化从而需要抽出来作为配置项的。

所以我们接着来讲讲怎么在**「Flask」**安排咱们自己的配置。

种类

「Flask」支持的配置种类挺多,大概有「py文件」, 「Config对象」, **「JSON」**等。

图片

我们这里采用**「from_object」**的方式。

编写pity/config.py文件

1
2
3
4
5
6
7
lua复制代码# 基础配置类
import os


class Config(object):
    ROOT = os.path.dirname(os.path.abspath(__file__))
    LOG_NAME = os.path.join(ROOT, 'logs', 'pity.log')

目前加了根目录配置和log文件路径。

修改pity/app/init_.py文件,引入配置文件

1
2
3
4
5
arduino复制代码from flask import Flask
from config import Config

pity = Flask(__name__)
pity.config.from_object(Config)

图片

改动差异

这一节就这么结束了,如果嫌学的不够的可以看下一节。至于有的人问日志为什么不用JSON文件,其实是可以的,不用太过于纠结。

全部代码地址: github.com/wuranxu/pit…

「觉得有用的话可以帮忙点个Star哦QAQ」

本文使用 文章同步助手 同步

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

测试平台系列(1) 搭建Flask服务

发表于 2021-06-26

搭建Flask服务

项目地址

项目地址

代码都会在这里有所展示,喜欢的话可以帮点个star,谢谢大家了!如果你喜欢该教程,也可以分享给其他人。

关于选型

想了很久,本来打算用**「Gin」做为后端服务,或者作为网关层,后面想了一下好像没这个必要。这个平台的用户量会比较有限,而且也会做一定的「服务拆分」。于是还是采用了更大众一点的口味: **「Flask」,有的同学可能会说,那怎么不用*「Django」\*?

哈哈,问到点了,「Django」笔者是真不会,基本上没有接触过,从接触「Python Web开发」的时候,我就用的是「Flask」。好在**「Flask」**比较精简,django用户也能比较快的上手。

环境准备

笔者其实比较好奇,不知道大家是要看一个很完整的过程,还是一个大概的,所以可能比较随性哈,复杂的地方尽量完整,毕竟又写文章又写代码的话,还是比较费事的。前期可能讲的比较仔细,后期可能以代码为主。所以有的地方如果有疑问的话,可以在文章下面评论或者联系本人。

预备知识

  1. 熟悉pip的使用方法
  2. 熟悉Python语法
  3. 熟悉Pycharm用法

工具/软件准备

  • IDE: Pycharm
  • Python3.4以上

最好是3.4以上,笔者这里比较随意,用的是3.7版本,没有太大的区别。

最简单的例子

笔者目前的目录在: J:\projects\github.com\wuranxu

以后的代码都会以这个目录为准,仅供参考。

创建项目并通过Pycharm打开

图片

安装Flask包

在当前目录(pity)下打开终端并输入:

1
复制代码pip3 install flask

如果安装过程很缓慢,可以加上豆瓣源:

1
arduino复制代码pip3 install flask -i https://pypi.douban.com/simple

图片

由于笔者已经安装好了,所以没有详细的安装过程。

初始化app

建立pity/app/__init__.py

![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e302ea85cdbd426eb468d9023d534d38~tplv-k3u1fbpfcp-zoom-1.image)

图片

编辑__init__.py

1
2
3
ini复制代码from flask import Flask

pity = Flask(__name__)

代码讲解: 这是flask的**「约定用法」**, 引入Flask类并实例化了一个Flask对象, 其中__name__为通俗写法。

至此,我们就得到了这样的一个名为”pity”的Flask**「实例」**。

编写Web服务文件

编写pity/run.py

1
2
3
4
5
6
7
8
9
10
python复制代码from app import pity


@pity.route('/')
def hello_world():
    return 'Hello World!'


if __name__ == "__main__":
    pity.run("0.0.0.0", threaded=True, port="7777")

其中@pity.route("/")是一个**「装饰器」**, 代表hello_world这个函数与路由/进行绑定,也就是说当访问到/路由的时候,函数hello_world会自动执行。

pity.run("0.0.0.0", threaded=True, port="7777")这句话表示启动web服务, 第一个参数0.0.0.0表示接受任何ip的访问,threaded表示如果有多人同时访问一个接口时是非阻塞的,port代表服务挂载的端口,这里我们以**「clearlove」**为端口号: 7777。

尝试一下吧!

运行run.py,可在pycharm运行也可以在终端里输入python3 run.py运行。

图片

可以看到Running on http://0.0.0.0:7777, 说明服务启动成功了!

验证一下

咱们都知道**「HTTP」是有很多种方法的,咱们这种pity.route如果没有指定方法的话,默认就是「GET」**方法。

打开浏览器输入: http://localhost:7777/

图片

如果看到这个hello world说明你成功了!

今天的课程就到这里了,下期见。说实话写的有点累,代码没写几行,屁话写了一大堆,后面可能要加快速度了!

全部代码地址: github.com/wuranxu/pit…

「觉得有用的话可以帮忙点个Star哦QAQ」

本文使用 文章同步助手 同步

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

坑蒙拐骗微服务,掌灯填坑架构人 !

发表于 2021-06-26

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。

兄弟姐妹们,一定要找好自己赖以生存的老窝。南橘北枳,根正才能苗红,否则你看起来一些主流的技术,可能就会成为毒药。

接下来我就给你讲一个技术降级的故事。怎么样由牛x的技术,换成老掉牙的单体应用。故事内容真实可靠,因为它来自一次真实的咨询。

  1. 集中式互联网特点

这个年头,ServiceMesh都已经在大厂开始铺开,弱一点的也已经是k8s驱动的微服务。这些花架子,全都比SpringCloud这样的一代维服务框架,高了不止一个档次。

这很好,新技术踩在旧技术身上,不停的向前蠕动,最终成为一个有机的整体,也成为技术革新的根本。

注意,我这里使用了蠕动两个字,而不是前进。蠕动意味着丑陋且缓慢,而前进意味着革新和勇往直前。

整个基础设施,就像一条巨大的毛毛虫,升级升级边边角角,最终破茧成为新的物种。它的升级过程是缓慢的,系统关系是复杂多变的。说是微服务,但它们仍然有以下特点:

  1. 微指的是服务粒度,而不是模块独立性。缺了大部分模块中的某一个,系统就不能正常运行。
  2. 脱离了自己公司的环境,就无法运行。
  3. 几乎无法重建。

你会发现,即使部分业务上云;或者你被某个信仰云搞怕了,想要迁云—都会花费较大的力气。

这么来说吧。即使把你公司里的所有代码,都给偷了出来,你还是不能把项目在你的开发机器上跑起来。大家默认了这个线上环境是稳定的,各种接口和数据以及DevOps工具是完备的。想要数据,直接调其他部门的接口就可以了。

  1. 某些公司的痛

但是但是但是,你默认的这个前提,正是某些公司的需求!某些公司的软肋!

因为,除了大部分toC的互联网公司,除了能够集中跑在一个地方的类SAAS服务,还有很大比重的实施性项目,在闷头发大财。

不要误解,我说的发财,是说老板和销售,而不是程序员。程序员还没这个资格,因为这种公司,上面还有项目经理这一层。

这些系统,需要在某个地方(可能是火星,也可能是客户现场)完成编码,然后被发布到未知的环境之中。

同学们注意了,无论公司包装的再美好,只要是这种模式,那就是外包。比如包装的漂漂亮亮的thoughtworks,除了几个咨询职位,本质上还是外包。

不是说这种公司不好,只是这种公司不适合走技术路线的程序员。单体应用,是最适合它们的。

有自己产品的也不行。只要是伺候的B端大爷,那定制化没跑了。如果产品模型抽象的乱七八糟,那么不好意思,就是外包。

今天,你在黑龙江刚实施了一套系统;明天,就就要带着这套系统去广西,进行为期3个月的定制改造。光是部署,就废了九牛二虎之力。

就是在这种场景下,还是有人不加犹豫,选择了微服务。

  1. 外表华丽的微服务

微服务有很好的愿景,也有很好的案例。有了微服务的加持,类似奈飞之类的公司,业务得以爆发性的增长。牛x的案例也是数不胜数。

微服务要解决的问题,也带有非常大的迷惑性。

迷惑性之一,就可以在PPT里或者年度会议上吹牛逼。微字,分布式,高并发,存算分离…,只有这些如此豪横的名词,才能在技术圈拿得出门面。此时,技术界和忽悠界产生了完美的联通。

迷惑性之二,就是在互联网环境里,微服务确实有效。微服务能够降低某些模块的风险,部署灵活,稳定性高。服务松耦合,扩展性高。

看到扩展性三个字,某些决策层就开始脑子发热荷尔蒙上升—这就是我的菜!!

就连不懂技术的老板,也会笑乐了和猴一样。

救救他们!

别TM老盯着优点不放啊,你以为你是鸿蒙,你以为你是宣传部门啊。

无数的案例表明,任何华丽的表象下面,都需要大量配套去扫地,微服务也不例外。

微服务运行,其实只需要包含注册中心就行了,其他什么RPC、熔断之类的,其实在框架内部,并没什么额外部署成本。

但是,这种阉割性的微服务,几乎没什么作用。要想要发挥它的功效,就要建设服务监控、服务追踪、服务治理等;如果模块非常多,还是建设虚拟化…

就这类公司大多数系统的那么点量来说,这些配套系统都不好意思给它们上。

但是好家伙,小伙子们一发力,一个项目拆出来20多个微服务。

小伙子们记好了,方向错了,你越努力,效果就越差。你在那加班加点的干,其实是在害公司。

为什么能拆成20多个服务?其实,服务粒度是个伪命题。有的人喜欢拆到功能界限;有的人会再加一刀把读写分离也拆了;有的人把服务关系画成一张蜘蛛网;有的人喜欢深入一点的调用—一层套一层。

这些都没什么关系,因为这是水平问题造成的后果,随着服务治理都可以趋向完美。意识层面的问题才是大事—光顾着吹牛逼体验新技术了,自己技术团队什么水平,心里就没棵B树。

就那么几个人的团队,拆成20几个服务,还没有配套的CI工具,除了折腾人,就没点什么好处。

要命的是,只要你实施一次,这些乱七八糟的东西,就要重新搞一遍吗,你确定每个人都能搞得定么?

上APM吧,上监控吧,上CI吧。互联网公司在搞的东西,你一样没拉下。关键是,人家每个方向都是团队在搞的东西,现在全交给了你一个人。

  1. 改回去吧

错了么?错了!外包公司(原谅我这种叫法,你也可以叫项目类公司)最注重的,就是成本。这么搞,相当于每实施一次,就建立了一个小型公司,把所有的东西重来一遍。

有办法么?有啊。上云就可以了,把这些基础设置交给云去做。但是上云,是另一种形式的中心化,只不过把SAAS底层的IAAS交给云了。把云机器当作普通机器来用,和上不上云没什么区别。

另外,客户不同意啊。我自己有机器,你给我瞎上云干啥,我根本不相信这些云。

这个时候,你就只能干瞪眼。

还有一种办法,那就是把这些拆好的微服务,再TM合并起来。最终打包成一两个jar包。发布的时候,拖到服务器上直接启动就好了。

这种合并要注意不要把频率高的小数据量查询和报表类的服务放在一起,否则共用一套资源(连接池、JVM等)会相互影响。最终建议分成三个就好了:普通服务、报表服务、定时任务。

这种决定是与主流技术相反的,相当于降级。当下了这种决定,小伙伴们嘴都撅的老高—以后出去找工作也不好吹了。但有什么办法呢?

最原始的方法,能够适应任何恶劣的环境,能够忍受任何客户的刁钻。这是由公司的现状决定的。

唯一的问题是,很多人就这么干废了。

End

每一年,我都会看到很多很多传统行业的人,想要进入到互联网这个圈子。外包和项目类公司,很多也和传统公司无异。具体的区分界限,以前也有较深入的比较。

《传统企业的人才们,先别忙着跳“互联网”!》

如果你恰巧在这种行业中,不要迷信互联网公司的技术栈,它们真的水土不服。互联网的挑战主要是量,而你的挑战是成本。老板想的是快点完工回款,而不是系统的长治久安。这时候,你用的技术花哨,但是没人深入去做,最后就会是一团乱麻。

正是由于对微服务特别了解,xjjdog才推荐这些公司不要采用微服务,很好笑是吧。当然,微服务很好很有魅力,拿来练手是没问题的,但是记得啊,练的差不多在系统上线前,赶紧跑啊。否则锅就是你的了。

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,进一步交流。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…631632633…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%