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

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


  • 首页

  • 归档

  • 搜索

并发编程——线程中sleep(),yield(),join(

发表于 2021-08-11

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

本文系作者 chaoCode原创,转载请私信并在文章开头附带作者和原文地址链接。

违者,作者保留追究权利。

前言

今天简单的讲一讲线程中sleep(),join(),yield(),wait(),notify(),notifyAll()这些方法的使用以及区别。

不过在讲这些方法之前,需要简单的介绍一下锁池和等待池的概念。

专栏推荐:
并发编程专栏

锁池和等待池

1.锁池

所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列进行等待cpu资源分配。

2.等待池

当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了notify()或notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放到锁池,而notifyAll()是将等待池的所有线程放到锁池当中

sleep()

首先看一下sleep方法的源码,我们发现sleep是一个静态方法,它接受一个long类型的毫秒值参数,而且是一个本地方法(native修饰),而且会抛出InterruptedException(中断异常)。
sleep源码.png
sleep()这个方法的使用很简单,因为它是一个Thread的静态方法,所以就可以直接Thread.sleep(毫秒值),休眠指定的毫秒数。

1、使当前线程(即调用该方法的线程)暂停执行一段时间,让其他线程有机会执行。但是时间到了之后线程会进入就绪队列,重新去竞争cpu资源。

2.sleep()会释放cpu资源,但是不会释放同步锁(类锁和对象锁)

例如有两个线程同时执行(没有synchronized)一个线程优先级为MAX_PRIORITY,另一个为MIN_PRIORITY,如果没有Sleep()方法,只有高优先级的线程执行完毕后,低优先级的线程才能够执行;但是高优先级的线程sleep(500)后,低优先级就有机会执行了。

总之,sleep()可以使低优先级的线程得到执行的机会,当然也可以让同优先级、高优先级的线程有执行的机会。

yield()

使当前正在执行的线程向另一个线程交出运行权。注意这是一个静态方法。

该方法与sleep()类似,只是不能由用户指定暂停多长时间,并且yield()方法只能让同优先级的线程有执行的机会。
1、yield()执行后线程直接进入就绪状态。
2、yield()会释放cpu资源,但是不会释放同步锁(类锁和对象锁)
yield源码.png

join()

执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那线程B会进入到阻塞队列,直到join结束或中断线程B才开始进入阻塞队列。
可以实现一个线程的顺序执行。

join.png
**下面举一个小例子:

我排队打饭,smile女神来了,我让她先打饭,但是这个时候她男朋友来了,smile女神让他男朋友先打饭。呜呜呜~**

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
java复制代码public class UseJoin {

static class Smile implements Runnable {
private Thread thread;

public Smile(Thread thread) {
this.thread = thread;
}

public Smile() {
}

public void run() {
System.out.println("smile开始排队打饭.....");
try {
if(thread!=null) thread.join();
} catch (InterruptedException e) {
}
SleepTools.second(2);//休眠2秒
System.out.println(Thread.currentThread().getName()
+ " smile打饭完成.");
}
}

static class SmileBoyfriend implements Runnable {

public void run() {
SleepTools.second(2);//休眠2秒
System.out.println("smileBoyfriend开始排队打饭.....");
System.out.println(Thread.currentThread().getName()
+ " smileBoyfriend打饭完成.");
}
}

public static void main(String[] args) throws Exception {
SmileBoyfriend smileBoyfriend = new SmileBoyfriend();
Thread sbf = new Thread(smileBoyfriend);
Smile smile = new Smile(sbf);
Thread s = new Thread(smile);
s.start();//我排队打饭
sbf.start();//smile女神来了,但是这个时候她男朋友来了,smile女神让他男朋友先打饭
System.out.println("chaoCode开始排队打饭.....");
s.join();
Thread.sleep(2000);//让主线程休眠2秒
System.out.println(Thread.currentThread().getName() + " chaoCode打饭完成.");
}
}

执行结果,可想而知,抱抱可怜的自己。

join执行结果.png

wait()和notify()、notifyAll()

1、wait方法用于协调多个线程对共享数据的存取,所以必须在Synchronized语句块内使用

2、wait()方法使当前线程暂停执行并释放会cpu资源,以及同步锁(类锁和对象锁)

3、调用wait()后必须调用notify()或notifyAll()后线程才会从等待池进入到锁池,当我们的线程竞争得到同步锁后就会重新进入绪状态等待cpu资源分配

当调用notify()方法后,将从对象的等待池中移走一个任意的线程并放到锁标志等待池中,只有锁标志等待池中线程能够获取锁标志;如果锁标志等待池中没有线程,则notify()不起作用。

notifyAll()则从对象等待池中移走所有等待那个对象的线程并放到锁标志等待池中。

注意:

1、这三个方法都是java.lang.Object的方法。

2、notif()方法要配合wait()方法使用,一般在wait()之后调用或者在线程结束时调用才会成功。

image.png

感谢诸君的观看,文中如有纰漏,欢迎在评论区来交流。如果这篇文章帮助到了你,欢迎点赞👍关注。

本文转载自: 掘金

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

可重入锁&不可重入锁

发表于 2021-08-11

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

什么是可重入锁?

  • 同一个线程可以重入上锁的代码段,不同的线程则需要进行阻塞
  • Java的可重入锁有:ReentrantLock(显式的可重入锁)、synchronized(隐式的可重入锁)
  • 可重入锁的最大作用是避免死锁
  • 同步锁可以再次进入(同一个线程)

就比如锁方法A上锁lock而方法B也上锁lock,并且在方法A中调用方法B,不会出现死锁的情况,因为它们处于同一个线程用的是同一把锁,所以可重入锁运行再次进入。

不可重入锁

这段代码模拟了不可重入锁的构造,运行后发现出现死锁的情况这是因为在method1方法中上锁之后没有释放,在调用method2的时候内部再一次调用了lock方法,上次method1调用lock方法的时候已经将isLock = true,所以一直处于while循环当中

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
java复制代码public class NotReentrantLockDemo {
static MyLock lock = new MyLock();
public static void main(String[] args) throws InterruptedException {
new NotReentrantLockDemo().method1();
}
public void method1() throws InterruptedException {
lock.lock();
System.out.println("this is method1");
method2();
lock.unlock();
}
public void method2() throws InterruptedException {
lock.lock();
System.out.println("this is method2");
lock.unlock();
}
}

class MyLock{
private boolean isLock = false;
public synchronized void lock() throws InterruptedException {
while(isLock){
wait();
}
isLock = true;
}
public synchronized void unlock(){
isLock = false;
notify();
}
}

测试结果:
仅打印this is method1后出现阻塞

可重入锁

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
java复制代码public class ReentrantLockDemo {
static MyReentrantLock lock = new MyReentrantLock();
public static void main(String[] args) throws InterruptedException {
new ReentrantLockDemo().method1();
}
public void method1() throws InterruptedException {
lock.lock();
System.out.println("this is method1");
method2();
lock.unlock();
}
public void method2() throws InterruptedException {
lock.lock();
System.out.println("this is method2");
lock.unlock();
}
}
class MyReentrantLock{
private boolean isLock = false;
Thread lockThread = null;
int lockCount = 0;
public synchronized void lock() throws InterruptedException {
while(isLock && Thread.currentThread()!=lockThread){
wait();
}
isLock = true;
lockCount++;
lockThread = Thread.currentThread();
}
public synchronized void unlock(){
if(lockThread == Thread.currentThread()){
lockCount --;
if(lockCount == 0){
isLock = false;
notify();
}
}
}
}

测试结果:

this is method1

this is method2

解析

可以看到与不可重入锁相比,可重入锁新增两个属性上锁的线程lockThread和上锁的次数lockCount,通过这两个变量的控制实现防止死锁的目的。

synchronized是可重入锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class ReentrantLockDemo {
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(()->{resource.method1();}).start();
}

}
class Resource{
public synchronized void method1(){
System.out.println("this is method1");
System.out.println(Thread.currentThread().getId());
method2();
}
public synchronized void method2(){
System.out.println("this is method2");
System.out.println(Thread.currentThread().getId());
}
}

运行结果:

this is method1

12

this is method2

12

ReentrantLock是可重入锁

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
java复制代码public class ReentrantLockDemo {


public static void main(String[] args) throws InterruptedException {

new Thread(new Task()).start();
}

}
class Task implements Runnable {
static Lock lock = new ReentrantLock();

@Override
public void run() {
lock.lock();
process1();
lock.unlock();
}

public void process1() {

System.out.println("this is process1" + "/t id=" + Thread.currentThread().getId());
process2();

}

public void process2() {
lock.lock();
System.out.println("this is process2" + "/t id=" + Thread.currentThread().getId());
lock.unlock();
}
}

this is process1/t id=13

this is process2/t id=13

本文转载自: 掘金

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

SpringBoot整合Security安全框架、控制权限

发表于 2021-08-11

我想每个写项目的人,都肯定会遇到控制权限这个问题.
例如这个这个链接只能管理员访问,那个链接丫只能超级管理员访问等等,实现方式也有多种多样,控制的粒度也不一样。
以前刚学的时候,不会框架,大都是手写注解+过滤器来进行权限的控制,但这样增加了过滤器的负担。用起来也会稍微有些麻烦,粒度不太好控制。

用框架的话,就是封装了更多的操作,让一切更简单吧。当然不局限于Security,还有像Shiro安全框架,这两种非常常见。
一起加油吧!!!😁

下面就开始吧!!!👇

一、前言

介绍:

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

官网:

SpringSecurity 最新

SpringSecurity 5.0.6版本

优缺点:

优点

  • Spring Boot 官方提供了大量的非常方便的开箱即用的 Starter ,包括 Spring Security 的 Starter ,使得在 Spring Boot 中使用 Spring Security 变得更加容易。
  • Spring Security功能强大,比较好用。

缺点

  1. Spring Security 是一个重量级的安全管理框架,
  2. Spring Security概念复杂,配置繁琐(这个确实,没法逃开)

案例:

我们在访问一个网站时,大都都会设置普通用户能有的权限,然后管理员有的权限,再就是超级管理员等等,这次就是实现这样一个案例。

项目结构:

在这里插入图片描述

二、环境准备

2.1、数据库表

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码CREATE TABLE `account`  (
`id` int(10) NOT NULL AUTO_INCREMENT,
`username` varchar(25) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`role` varchar(25) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

INSERT INTO `account` VALUES (1, 'user', '$2a$10$1MHNdZS.oCICxLRVbnNBZe4CRn9Rk1MVQhasSMhHr0G4BCNQjPpna', 'ROLE_USER');
INSERT INTO `account` VALUES (2, 'admin', '$2a$10$dKkrkgVzaCPX74TvxOjwNuFJjIRJeAuDPKFntwNwRvRHkwIAHV5Q6', 'ROLE_ADMIN');
INSERT INTO `account` VALUES (3, 'super_admin', '$2a$10$CqOXnSp6oks9UTvsops4U.0vMGbUE2Bp28xKaPmlug4W8Mk59Sj8y', 'ROLE_SUPER_ADMIN');
INSERT INTO `account` VALUES (4, 'test', '$2a$10$SQsuH1XfxHdsVmf2nE75wOAE6GHm1nd/xDp/08KYJmtbzJt2J6xIG', 'TEST');

2.2、导入依赖

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
xml复制代码<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--java版本太高 向下兼容的包-->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
</dependencies>

2.3、配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
properties复制代码# 应用名称
spring.application.name=demo
# 应用服务 WEB 访问端口
server.port=8080

spring.datasource.name=defaultDataSource
# 数据库连接地址
spring.datasource.url=jdbc:mysql://localhost:3306/security?serverTimezone=UTC
# 数据库用户名&密码:
spring.datasource.username=root
spring.datasource.password=123456

mybatis-plus.mapper-locations=classpath:mapper/**/*.xml

logging.level.com.crush.security.mapper=DEBUG

# token 存活时间
token.expire=3600000
token.key=123456

2.4、WebSecurityConfig Security的主要配置类:

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
java复制代码import com.crush.security.auth.filter.JwtAuthenticationFilter;
import com.crush.security.auth.filter.JwtAuthorizationFilter;
import com.crush.security.auth.handle.MacLoginUrlAuthenticationEntryPoint;
import com.crush.security.auth.handle.MyAccessDeniedHandler;
import com.crush.security.auth.handle.MyLogoutSuccessHandler;
import com.crush.security.auth.service.UserDetailServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;


/**
* @author crush
*/
@Configuration
@EnableWebSecurity
//启用全局配置
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

/**放行的路径*/
private final String[] PATH_RELEASE = {
"/login",
"/all"
};
/***根据用户名找到用户*/
@Autowired
private UserDetailServiceImpl userDetailService;

@Autowired
private MacLoginUrlAuthenticationEntryPoint macLoginUrlAuthenticationEntryPoint;

@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;

@Autowired
private MyLogoutSuccessHandler myLogoutSuccessHandler;


@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable();
http.authorizeRequests()
/**antMatchers (这里的路径) permitAll 这里是允许所有人 访问*/
.antMatchers(PATH_RELEASE).permitAll()
/** 映射任何请求 */
.anyRequest()

/** 指定任何经过身份验证的用户都允许使用URL。*/
.authenticated()

/** 指定支持基于表单的身份验证 */
.and().formLogin().permitAll()

/** 允许配置异常处理。可以自己传值进去 使用WebSecurityConfigurerAdapter时,将自动应用此WebSecurityConfigurerAdapter 。*/
.and().exceptionHandling()

/** 设置要使用的AuthenticationEntryPoint。 macLoginUrlAuthenticationEntryPoint 验证是否登录*/
.authenticationEntryPoint(macLoginUrlAuthenticationEntryPoint)

/** 指定要使用的AccessDeniedHandler 处理拒绝访问失败。*/
.accessDeniedHandler(myAccessDeniedHandler)

/** 提供注销支持。 使用WebSecurityConfigurerAdapter时,将自动应用此WebSecurityConfigurerAdapter 。
* 默认设置是访问URL “ / logout”将使HTTP会话无效,清理配置的所有rememberMe()身份验证,清除SecurityContextHolder ,
* 然后重定向到“ / login?success”,从而注销用户*/
.and().logout().logoutSuccessHandler(myLogoutSuccessHandler)

/** 处理身份验证表单提交。 授予权限 */
.and().addFilter(new JwtAuthenticationFilter(authenticationManager()))
/** 处理HTTP请求的BASIC授权标头,然后将结果放入SecurityContextHolder 。 */
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
/**不需要session */
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}

@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}

/**
*
* 因为使用了BCryptPasswordEncoder来进行密码的加密,所以身份验证的时候也的用他来判断哈、,
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
}

/** * 密码加密*/
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

2.5、Security身份验证

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
java复制代码import com.crush.security.entity.MyUser;
import com.crush.security.utils.JwtTokenUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;

/**
* 处理身份验证表单提交。
*
* @author crush
*/
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

private AuthenticationManager authenticationManager;

public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}

/**
* 执行实际的身份验证。
* 该实现应执行以下操作之一:
* 返回已验证用户的已填充验证令牌,指示验证成功
* 返回null,表示身份验证过程仍在进行中。 在返回之前,实现应执行完成该过程所需的任何其他工作。
* 如果身份验证过程失败,则抛出AuthenticationException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
//输入流中获取到登录的信息
try {
MyUser loginUser = new ObjectMapper().readValue(request.getInputStream(), MyUser.class);
logger.info("loginUser===>" + loginUser);
/**
* authenticate
* 尝试对传递的Authentication对象进行身份Authentication ,
* 如果成功,则返回完全填充的Authentication对象(包括授予的权限)
* */
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword(), new ArrayList<>())
);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}

/**
* 成功验证后调用的方法
* 如果验证成功,就生成token并返回
*/
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// 查看源代码会发现调用getPrincipal()方法会返回一个实现了`UserDetails`接口的对象
// 所以就是JwtUser啦
MyUser user = (MyUser) authResult.getPrincipal();
String role = "";
// 因为在JwtUser中存了权限信息,可以直接获取,由于只有一个角色就这么干了
Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
for (GrantedAuthority authority : authorities) {
role = authority.getAuthority();
}
// 根据用户名,角色创建token并返回json信息
String token = JwtTokenUtils.createToken(user.getUsername(), role, false);
user.setPassword(null);
user.setToken(JwtTokenUtils.TOKEN_PREFIX + token);
response.setStatus(HttpServletResponse.SC_OK);
response.setHeader("token", JwtTokenUtils.TOKEN_PREFIX + token);
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(new ObjectMapper().writeValueAsString(user));
}

/**
* 验证失败时候调用的方法
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(new ObjectMapper().writeValueAsString( "登录失败,账号或密码错误"));
}
}

2.6、Security授权

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
java复制代码import com.crush.security.utils.JwtTokenUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;

/**
* 处理HTTP请求的BASIC授权标头,然后将结果放入SecurityContextHolder 。
*/
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {

public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {

String tokenHeader = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
// 如果请求头中没有Authorization信息则直接放行了
if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtils.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
// 如果请求头中有token,则进行解析,并且设置认证信息
SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
super.doFilterInternal(request, response, chain);
}

/** * 这里从token中获取用户信息并新建一个token*/
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, "");
String username = JwtTokenUtils.getUsername(token.trim());
String role = JwtTokenUtils.getUserRole(token);
if (username != null) {
return new UsernamePasswordAuthenticationToken(username, null,
Collections.singleton(new SimpleGrantedAuthority(role))
);
}
return null;
}
}

2.7、UserDetailsService

UserDetailServiceImpl 实现了UserDetailsService,用来加载用户特定数据的核心接口。

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
java复制代码import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.crush.security.entity.MyUser;
import com.crush.security.service.IMyUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class UserDetailServiceImpl implements UserDetailsService {

final
IMyUserService userService;

public UserDetailServiceImpl(IMyUserService userService) {
this.userService = userService;
}

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
MyUser user = userService.getOne(new QueryWrapper<MyUser>().eq("username", username));
return user;
}
}

2.7、MacLoginUrlAuthenticationEntryPoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码/**
*
* 身份验证没有通过回调
*/
@Component
public class MacLoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
writer.write(new ObjectMapper().writeValueAsString("未登录!"));
}
}

2.8、MyAccessDeniedHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码/**
* 权限不足回调
*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {

@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
PrintWriter writer = httpServletResponse.getWriter();
writer.write(new ObjectMapper().writeValueAsString("不好意思,你的权限不足!"));
}
}

2.9、MyLogoutSuccessHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码/**
* 退出回调
*/
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setStatus(HttpServletResponse.SC_OK);
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
writer.write(new ObjectMapper().writeValueAsString( "退出成功"));
}
}

2.10、JWT的工具类

生成token

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
java复制代码package com.crush.security.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import java.util.Date;
import java.util.HashMap;

public class JwtTokenUtils {

public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";

private static final String SECRET = "jwtsecretdemo";
private static final String ISS = "echisan";

/**
* 过期时间是3600秒,既是1个小时
*/
private static final long EXPIRATION = 3600L;

/**
* 选择了记住我之后的过期时间为7天
*/
private static final long EXPIRATION_REMEMBER = 604800L;

// 添加角色的key
private static final String ROLE_CLAIMS = "rol";

/**
* 修改一下创建token的方法
*
* @param username
* @param role
* @param isRememberMe
* @return
*/
public static String createToken(String username, String role, boolean isRememberMe) {
String token = null;
try {
long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
HashMap<String, Object> map = new HashMap<>();
map.put(ROLE_CLAIMS, role);
token = Jwts.builder()
.signWith(SignatureAlgorithm.HS512, SECRET)
// 这里要早set一点,放到后面会覆盖别的字段
.setClaims(map)
.setIssuer(ISS)
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.compact();
} catch (ExpiredJwtException e) {
e.getClaims();
}
return token;
}


/**
* 从token中获取用户名
*
* @param token
* @return
*/
public static String getUsername(String token) {
return getTokenBody(token).getSubject();
}

/**
* 从token中获取roles
*
* @param token
* @return
*/
public static String getUserRole(String token) {
return (String) getTokenBody(token).get(ROLE_CLAIMS);
}

/**
* 是否已过期
*
* @param token
* @return
*/
public static boolean isExpiration(String token) {
return getTokenBody(token).getExpiration().before(new Date());
}

private static Claims getTokenBody(String token) {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
}

public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String user = encoder.encode("test");
System.out.println(user);
}
}

弄完上面这些,相关配置就都搞定了,剩下就是最简单的编码啦。

三、代码

entity

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
java复制代码@Data
@EqualsAndHashCode(callSuper = false)
@TableName("account")
public class MyUser implements Serializable, UserDetails {

private static final long serialVersionUID = 1L;

private int id;

private String username;

private String password;

// 1:启用 , 0:禁用
@TableField(exist = false)
private Integer enabled = 1;

// 1:锁住 , 0:未锁
@TableField(exist = false)
private Integer locked = 0;

private String role;

@TableField(exist = false)
private String token;

//授权
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role);
authorities.add(authority);
return authorities;
}

@Override
public boolean isAccountNonExpired() { return true; }

@Override
public boolean isAccountNonLocked() { return locked == 0; }

@Override
public boolean isCredentialsNonExpired() { return true; }

@Override
public boolean isEnabled() { return enabled == 1; }
}

mapper

1
2
3
4
5
6
java复制代码import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.crush.security.entity.MyUser;
import org.springframework.stereotype.Repository;

@Repository
public interface MyUserMapper extends BaseMapper<MyUser> {}

service、impl

1
2
3
4
5
6
java复制代码import com.baomidou.mybatisplus.extension.service.IService;
import com.crush.security.entity.MyUser;

public interface IMyUserService extends IService<MyUser> {

}
1
2
3
4
5
6
7
8
9
java复制代码import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.crush.security.entity.MyUser;
import com.crush.security.mapper.MyUserMapper;
import com.crush.security.service.IMyUserService;
import org.springframework.stereotype.Service;

@Service
public class MyUserServiceImpl extends ServiceImpl<MyUserMapper, MyUser> implements IMyUserService {
}

controller

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
java复制代码package com.crush.security.controller;


import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

@RequestMapping("/all")
String all() {
return "在WebSecurityConfig中配置了放行,任何人都可以进行访问";
}

@PreAuthorize("permitAll()")
@RequestMapping("/test")
String test() {
return "所有登录的人都可以访问";
}

@PreAuthorize("hasRole('USER')")
@RequestMapping("/user/userList")
String userList() {
return "role: user";
}

@PreAuthorize("hasRole('ADMIN')")
@RequestMapping("/admin/updateUser")
String updateUser() {
return "role: admin";
}

@PreAuthorize("hasRole('SUPER_ADMIN')")
@RequestMapping("/admin/superAdmin")
String superAdmin() {
return "role: superAdmin";
}

@PreAuthorize("hasAnyRole('ADMIN','USER')")
@RequestMapping("/userAndAdmin")
String userAndAdminTest() {
return "role: admin and user";
}

@PreAuthorize("hasAnyRole('ADMIN')or hasAnyRole('SUPER_ADMIN')")
@RequestMapping("/AdminAndSuperAdminTest")
String AdminAndSuperAdminTest() {
return "role: admin and super_admin";
}

// hasAnyAuthority() 也是可以多个字符串 权限验证,可以不跟ROLE_前缀
@PreAuthorize("hasAuthority('TEST') ")
@RequestMapping("/ceshi2")
String ceshi2() {
return "hasAuthority:权限验证,不过查的也是role那个字段,不过不用拼接上ROLE而已";
}
}

四、测试

注:我使用的测试工具是Postman,另外login接口接收的数据是需要JSON类型的。

1)登录

注意这里的token,我们是需要把他记住,下次去请求要携带上。

在这里插入图片描述

2)测试管理员

在这里插入图片描述

3)测试hasAnyAuthority ()注解

hasAnyAuthority() 也是可以多个字符串 权限验证,可以不跟ROLE_前缀

在这里插入图片描述

在这里插入图片描述

五、总结

Security框架和SpringBoot集成,其实上手特别快,但是如果要想研究的比较深刻的话,我觉得是比较困难的,上文讲过,security是属于一个重量级的框架,里面很多东西特别多。使用方面肯定是没有任何问题的。

你卷我卷,大家卷,什么时候这条路才是个头啊。😇(还是直接上天吧)

有时候也想停下来歇一歇,一直做一个事情,感觉挺难坚持的。😁

你好,如果你正巧看到这篇文章,并且觉得对你有益的话,就给个赞吧,让我感受一下分享的喜悦吧,蟹蟹。🤗

如若有写的有误的地方,也请大家不啬赐教!!

同样如若有存在疑惑的地方,请留言或私信,定会在第一时间回复你。

持续更新中

源码:Gitee-Security

本文转载自: 掘金

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

最新版Spring Cloud Alibaba微服务架构-O

发表于 2021-08-11

前言

Spring Cloud OpenFeign是Feign的升级版,目前Github上面已经更新到11.6版本了,Feign是Netflix开发的声明式、模板化的HTTP客户端, Feign可以帮助我们更快捷、优雅地调用HTTP API。

在这里插入图片描述

一、OpenFeign配置使用

OpenFeign的使用首先肯定得依赖Spring Cloud,作为Alibaba微服务架构,以下依赖必不可少。

在这里插入图片描述

1、引入pom依赖

1
2
3
4
5
java复制代码<!-- openfeign服务调用 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2、消费端开启Feign服务调用

在订单服务(dt-order-servic)的启动器上开启Feign的服务调用,以此来调用库存服务端的接口方法。

@EnableFeignClients :开启服务调用

在这里插入图片描述

3、消费端动态代理Service接口

1
2
3
4
5
6
java复制代码@FeignClient(name = "dt-stock-service",path = "/stock")
public interface StockFeignService {

@GetMapping(value = "/getPort")
String getPort();
}

@FeignClient(name = “dt-stock-service”,path = “/stock”)

dt-stock-service:服务提供者的服务名称,path:服务提供者的统一前缀(即Controller的前缀路径,如果没有就不填)

在这里插入图片描述

3、消费端测试

编写控制器,调用服务暴露接口方法:

在这里插入图片描述

在这里插入图片描述

二、OpenFeign日志配置

在默认情况下,OpenFeign默认的日志是关闭的,在我们开发过程中,需要调试接口,或者看看调用性能,就需要配置OpenFeign的日志,把OpenFeign的调用日志清晰的打印出来,方便我们开发。

Feign日志级别:
NONE,无记录(DEFAULT)。
BASIC,只记录请求方法和URL以及响应状态代码和执行时间。
HEADERS,记录基本信息以及请求和响应标头。
FULL,记录请求和响应的头文件,正文和元数据

1、全局配置日志

OpenFeignConfig.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码/**
* 全局配置:OpenFeign的全局日志配置
* 局部配置:不加@Configuration注释
* @author DT
* @date 2021/8/9 22:00
*/
@Configuration
public class OpenFeignConfig {

@Bean
public Logger.Level feignLoggerLevel(){
// 输出日志级别FULL
return Logger.Level.FULL;
}
}

SpringBoot的日志级别默认为info,大于full,导致feign的日志配置不会输出,所以加以下配置:

1
2
3
4
yml复制代码#SpringBoot的日志级别默认为info,大于full,导致feign的日志配置不会输出,所以加以下配置
logging:
level:
com.dt.springcloud.openfeign: debug

在这里插入图片描述

输出打印:

在这里插入图片描述

2、针对单个服务配置日志

在这里插入图片描述

使用configuration = OpenFeignConfig.class

在这里插入图片描述

当然我们也可以通过配置文件的方式来配置.

3、通过配置文件配置日志

1、全局配置

针对所有服务配置:

1
2
3
4
5
6
7
8
9
10
yml复制代码# SpringBoot的日志级别默认为info,大于full,导致feign的日志配置不会输出,所以加以下配置
logging:
level:
com.dt.springcloud.openfeign: debug
# feign日志全局配置
feign:
client:
config:
default:
loggerLevel: BASIC

在这里插入图片描述

2、针对某个服务单独配置

当然你也可以针对某个服务进行配置单独的日志,将调用的微服务名称改成default就配置成全局的了:

1
2
3
4
5
6
7
yml复制代码# feign针对某个服务配置日志
feign:
client:
config:
#想要调用的微服务名称
dt-stock-service:
loggerLevel: BASIC

在这里插入图片描述

三、OpenFeign自定义拦截器

OpenFeign拦截器,无非就是和我们SpringMVC中的拦截器一样,每次feign发起http调用之前,会去执行拦截器中的逻辑,比如统一添加header头信息,对body体中的信息做修改或替换。feign提供了 feign.RequestInterceptor 接口,只需实现该接口,实现对应方法,并将实现类通过 @Configuration交给spring容器管理,即可加上我们自己的通用处理逻辑。

1、全局配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@CommonsLog
@Configuration
public class CustomFeignInterceptor implements RequestInterceptor {

@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header("token","ABC123456");
byte[] body = requestTemplate.body();
String url = requestTemplate.url();
String method = requestTemplate.method();
Map<String, Collection<String>> map = requestTemplate.headers();
log.info("OpenFeign拦截器启动......");
log.info("body->>>"+(body == null ? null : body.length));
log.info("url->>>"+url);
log.info("method->>>"+method);
log.info("header->>>"+map.get("token"));
}
}

在这里插入图片描述

查看打印结果:

在这里插入图片描述
如果还需要做其他处理,可以对RequestTemplate做相应处理来实现,比如令牌的验证,刷新,鉴权等等。

2、针对某个服务单独配置

除了上面的第一种全局配置方式以外,我们也可以在配置文件中,单独针对某个被调用的服务进行配置,和我们上面的日志配置方式一样。

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
yml复制代码# feign针对某个服务配置
feign:
client:
config:
#想要调用的微服务名称
dt-stock-service:
connectTimeout: 5000 # 连接超时时间
readTimeout: 5000 # 读取超时时间
loggerLevel: BASIC
requestInterceptors: # 配置拦截器,相当于代码配置方式中的RequestInterceptor
- com.dt.springcloud.interceptor.CustomFeignInterceptor

dt-stock-service 想要调用的服务名称。

总结

没有谁的幸运,是凭空而来,只有当你足够努力,你才会足够幸运。这世界不会辜负每一份努力和坚持,时光不会怠慢执着而勇敢的每一个人

本篇文章结束了,后面我们再继续深入研究微服务其他的组件的使用以及原理剖析,创作不易,喜欢的请关注小编CSDN:blog.csdn.net/qq_41107231 以及掘金:juejin.cn/user/394024…

本文转载自: 掘金

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

图解Golang的GMP 原理与调度流程

发表于 2021-08-11

Golang “调度器” 的由来?

单进程时代没有调度器

我们知道,一切的软件都是跑在操作系统上,真正用来干活 (计算) 的是 CPU。早期的操作系统每个程序就是一个进程,直到一个程序运行完,才能进行下一个进程,这就是“单进程时代”。

单进程.png

早期的单进程操作系统,面临 2 个问题:

  1. 单一的执行流程,计算机只能一个任务一个任务处理。
  2. 进程阻塞所带来的 CPU 时间浪费。

那么能不能有多个进程来宏观一起来执行多个任务呢?

后来操作系统就具有了最早的并发能力:多进程并发,当一个进程阻塞的时候,切换到另外等待执行的进程,这样就能尽量把 CPU 利用起来,CPU 就不浪费了。

多进程/多线程时期的调度器

多进程.png

在多进程 / 多线程的操作系统中,就解决了阻塞的问题,因为一个进程阻塞 cpu 可以立刻切换到其他进程中去执行,而且调度 cpu 的算法可以保证在运行的进程都可以被分配到 cpu 的运行时间片。这样从宏观来看,似乎多个进程是在同时被运行。

但新的问题就又出现了,进程拥有太多的资源,进程的创建、切换、销毁,都会占用很长的时间,CPU 虽然利用起来了,但如果进程过多,CPU 有很大的一部分都被用来进行进程调度了。

怎么才能提高 CPU 的利用率呢?

cpu成本.png

很明显,CPU 调度切换的是进程和线程。尽管线程看起来很美好,但实际上多线程开发设计会变得更加复杂,要考虑很多同步竞争等问题,如锁、竞争冲突等。

协程来提高CPU利用率

多进程、多线程已经提高了系统的并发能力,但是在当今互联网高并发场景下,为每个任务都创建一个线程是不现实的,因为会消耗大量的内存 (进程虚拟内存会占用 4GB [32 位操作系统], 而线程也要大约 4MB)。

大量的进程 / 线程出现了新的问题

  • 高内存占用
  • 调度的高消耗 CPU

然后工程师们就发现,其实一个线程分为 “内核态 “线程和” 用户态 “线程。

一个 “用户态线程” 必须要绑定一个 “内核态线程”,但是 CPU 并不知道有 “用户态线程” 的存在,它只知道它运行的是一个 “内核态线程”(Linux 的 PCB 进程控制块)。

这样,我们再去细化去分类一下,内核线程依然叫 “线程 (thread)”,用户线程叫 “协程 (co-routine)”。

N:M 的协程与线程的映射关系

n对m的协程与线程.png

协程跟线程是有区别的,线程由 CPU 调度是抢占式的,协程由用户态调度是协作式的,一个协程让出 CPU 后,才执行下一个协程。

协程 goroutine

Go 为了提供更容易使用的并发方法,使用了 goroutine 和 channel。goroutine 来自协程的概念,让一组可复用的函数运行在一组线程之上,即使有协程阻塞,该线程的其他协程也可以被 runtime 调度,转移到其他可运行的线程上。最关键的是,程序员看不到这些底层的细节,这就降低了编程的难度,提供了更容易的并发。

Go 中,协程被称为 goroutine,它非常轻量,一个 goroutine 只占几 KB,并且这几 KB 就足够 goroutine 运行完,这就能在有限的内存空间内支持大量 goroutine,支持了更多的并发。虽然一个 goroutine 的栈只占几 KB,但实际是可伸缩的,如果需要更多内容,runtime 会自动为 goroutine 分配。

Goroutine 特点:

  • 占用内存更小(几 kb)
  • 调度更灵活 (runtime 调度)

Goroutine 调度器 GMP 模型

在 Go 中,线程是运行 goroutine 的实体,调度器的功能是把可运行的 goroutine 分配到工作线程上。

gmp.png

  • 全局队列(Global Queue):存放等待运行的 G。
  • P 的本地队列:同全局队列类似,存放的也是等待运行的 G,存的数量有限,不超过 256 个。新建 G’时,G’优先加入到 P 的本地队列,如果队列满了,则会把本地队列中一半的 G 移动到全局队列。
  • P 列表:所有的 P 都在程序启动时创建,并保存在数组中,最多有 GOMAXPROCS(可配置) 个。
  • M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,P 队列为空时,M 也会尝试从全局队列拿一批 G 放到 P 的本地队列,或从其他 P 的本地队列偷一半放到自己 P 的本地队列。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。

开启一个 Goroutine 的调度流程

go func 创建和执行的流程

gmp2.jpeg

从上图我们可以分析出几个结论:

​ 1、我们通过 go func () 来创建一个 goroutine;

​ 2、有两个存储 G 的队列,一个是局部调度器 P 的本地队列、一个是全局 G 队列。新创建的 G 会先保存在 P 的本地队列中,如果 P 的本地队列已经满了就会保存在全局的队列中;

​ 3、G 只能运行在 M 中,一个 M 必须持有一个 P,M 与 P 是 1:1 的关系。M 会从 P 的本地队列弹出一个可执行状态的 G 来执行,如果 P 的本地队列为空,就会想其他的 MP 组合偷取一个可执行的 G 来执行;

​ 4、一个 M 调度 G 执行的过程是一个循环机制;

​ 5、当 M 执行某一个 G 时候如果发生了 syscall 或则其余阻塞操作,M 会阻塞,如果当前有一些 G 在执行,runtime 会把这个线程 M 从 P 中摘除 (detach),然后再创建一个新的操作系统的线程 (如果有空闲的线程可用就复用空闲线程) 来服务于这个 P;

​ 6、当 M 系统调用结束时候,这个 G 会尝试获取一个空闲的 P 执行,并放入到这个 P 的本地队列。如果获取不到 P,那么这个线程 M 变成休眠状态, 加入到空闲线程中,然后这个 G 会被放入全局队列中。

Go 协程调度器各场景调度过程解析

场景一:G1创建G2

P 拥有 G1,M1 获取 P 后开始运行 G1,G1 使用 go func() 创建了 G2,为了局部性 G2 优先加入到 P1 的本地队列。

场景一:G1创建G2.png

场景二:G1执行完毕

G1 运行完成后 (函数:goexit),M 上运行的 goroutine 切换为 G0,G0 负责调度时协程的切换(函数:schedule)。从 P 的本地队列取 G2,从 G0 切换到 G2,并开始运行 G2 (函数:execute)。实现了线程 M1 的复用。

场景二:G1执行完毕.png

场景三:G2创建过多的 Goroutine

假设每个 P 的本地队列只能存 3 个 G。G2 要创建了 6 个 G,前 3 个 G(G3, G4, G5)已经加入 p1 的本地队列,p1 本地队列满了。

场景三:G2创建过多的G.png

场景四:G2本地队列满再创建 Goroutine

G2 在创建 G7 的时候,发现 P1 的本地队列已满,需要执行负载均衡 (把 P1 中本地队列中前一半的 G,还有新创建 G 转移到全局队列)

(实现中并不一定是新的 G,如果 G 是 G2 之后就执行的,会被保存在本地队列,利用某个老的 G 替换新 G 加入全局队列)

场景四:G2本地队列满再创建G.png

这些 G 被转移到全局队列时,会被打乱顺序。所以 G3,G4,G7 被转移到全局队列。

场景六:唤醒正在休眠的M

规定:在创建 G 时,运行的 G 会尝试唤醒其他空闲的 P 和 M 组合去执行。

场景五:唤醒正在休眠的G.png

假定 G2 唤醒了 M2,M2 绑定了 P2,并运行 G0,但 P2 本地队列没有 G,M2 此时为自旋线程(没有 G 但为运行状态的线程,不断寻找 G)。

场景七:被唤醒的M2从全局队列取G

M2 尝试从全局队列 (简称 “GQ”) 取一批 G 放到 P2 的本地队列(函数:findrunnable())。M2 从全局队列取的 G 数量符合下面的公式:

n = min(len(GQ)/GOMAXPROCS + 1, len(GQ/2))

至少从全局队列取 1 个 g,但每次不要从全局队列移动太多的 g 到 p 本地队列,给其他 p 留点。这是从全局队列到 P 本地队列的负载均衡。

场景七:唤醒的去全局队列拿.png

场景八:M2从M1中偷取G

假设 G2 一直在 M1 上运行,经过 2 轮后,M2 已经把 G7、G4 从全局队列获取到了 P2 的本地队列并完成运行,全局队列和 P2 的本地队列都空了,如场景 7 图的左半部分。

场景八:M2从M1中偷取.png

全局队列已经没有 G,那 m 就要执行 work stealing (偷取):从其他有 G 的 P 哪里偷取一半 G 过来,放到自己的 P 本地队列。P2 从 P1 的本地队列尾部取一半的 G,本例中一半则只有 1 个 G8,放到 P2 的本地队列并执行。

场景九:自旋的最大限制

G1 本地队列 G5、G6 已经被其他 M 偷走并运行完成,当前 M1 和 M2 分别在运行 G2 和 G8,M3 和 M4 没有 goroutine 可以运行,M3 和 M4 处于自旋状态,它们不断寻找 goroutine。

场景九:自旋的最大限制.png

为什么要让 m3 和 m4 自旋,自旋本质是在运行,线程在运行却没有执行 G,就变成了浪费 CPU。

为什么不销毁现场,来节约 CPU 资源。因为创建和销毁 CPU 也会浪费时间,我们希望当有新 goroutine 创建时,立刻能有 M 运行它,如果销毁再新建就增加了时延,降低了效率。当然也考虑了过多的自旋线程是浪费 CPU,所以系统中最多有 GOMAXPROCS 个自旋的线程 (当前例子中的 GOMAXPROCS=4,所以一共 4 个 P),多余的没事做线程会让他们休眠。

场景十:G发生系统调用/阻塞

假定当前除了 M3 和 M4 为自旋线程,还有 M5 和 M6 为空闲的线程 (没有得到 P 的绑定,注意我们这里最多就只能够存在 4 个 P,所以 P 的数量应该永远是 M>=P, 大部分都是 M 在抢占需要运行的 P),G8 创建了 G9,G8 进行了阻塞的系统调用,M2 和 P2 立即解绑,P2 会执行以下判断:如果 P2 本地队列有 G、全局队列有 G 或有空闲的 M,P2 都会立马唤醒 1 个 M 和它绑定,否则 P2 则会加入到空闲 P 列表,等待 M 来获取可用的 p。本场景中,P2 本地队列有 G9,可以和其他空闲的线程 M5 绑定。

场景十:阻塞解绑.png

场景十一:G发生系统调用/非阻塞

G8 创建了 G9,假如 G8 进行了非阻塞系统调用。

场景十一:非阻塞解绑.png

M2 和 P2 会解绑,但 M2 会记住 P2,然后 G8 和 M2 进入系统调用状态。当 G8 和 M2 退出系统调用时,会尝试获取 P2,如果无法获取,则获取空闲的 P,如果依然没有,G8 会被记为可运行状态,并加入到全局队列,M2 因为没有 P 的绑定而变成休眠状态 (长时间休眠等待 GC 回收销毁)。

总结

Go 调度器很轻量也很简单,足以撑起 goroutine 的调度工作,并且让 Go 具有了原生(强大)并发的能力。Go 调度本质是把大量的 goroutine 分配到少量线程上去执行,并利用多核并行,实现更强大的并发。

本文转载自: 掘金

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

财务系统之结算体系架构业务篇 论财务"乐趣"与"痛楚" 乐趣

发表于 2021-08-11

论财务”乐趣”与”痛楚”

大家提到财务第一感觉往往是枯燥无味,千篇一律,天天面对一堆毫无感情的海量数据。但是数字之上有神米,乐趣也往往蕴含其中。

乐趣

错误数据终结者

信息流系统、交易流系统等系统,大家往往关注的是数据的一致性,只要字段有值即可或数据之间的级联是否正确。但财务关注的是数据的可用性,准确性,从财务核算层面去校验数据的使用是是否正确,业务流程是否正确,简单一点就是”此时应该是B而不是A”。比如: 编号:00001,但是出现了两个名称(商人公司、商人有限公司),对应非财务系统来说,无关紧要,只要编号没变就ok,因为业务逻辑是靠编号来串联和驱动的。但是针对财务系统来说,打款、税票等都会出大问题,财务要求数据在不同阶段可以有不同版本,但是不允许同一阶段有n多版本。财务系统体现出来的数据在这个阶段数据是准确的,不允许有歧义性。信息流、交易流、支付流等数据流最终用途是出财报,那么任何情况的错误都是不允许的。

高价值领域专家

财务系统的pm和dev第一要求必须要有闭环思路,要了解数据怎么来、数据如何加工、数据后续的处理是什么。那么在做项目时就得了解各种上游系统,了解它们的业务流程,了解它们的系统架构,了解它们的数据结构,了解他们的代码。同时还要清楚的知道财务系统产生的数据要做什么用,大家会如何用。 在每个财务的项目上必须了解上游和下游(管报或财报),从而确保整条链路是正确的。那么久而久之你就会熟悉整个公司的所有业务流程和财务流程,妥妥的领域专家。

痛楚

沟通与推进

财务的项目会花更多时间在沟通业务流程,沟通数据流程上面,当遇到业务流程不合理,产生了错误数据时,还得再花时间去推上游系统进行改进,给上游系统当QA,这些都是费心费力费时的事情。

业务快速发展

线下的业务快速发展,线上业务系统没有及时快速迭代跟进,线下线上脱轨,导致了数据不符合财务预期。这是出现业财不一致的根本原因。在任何一个业务快速发展的公司,这种情况避免不了,对于财务系统来说,唯一的方法就是全方位的监控异常数据,发现了异常数据后再推进上游系统迭代。

结算概念

专业含义:结算是会计用语,指把某一时期内的所有收支情况进行总结、核算。结算亦称货币结算,是在商品经济条件下,各经济单位间由于商品交易、劳务供应和资金调拨等经济活动而引起的货币收付行为。

通俗一点讲就是:双方在一定时期内产生交易行为,基于交易进行收取或支付金额的一系列行为。

结算分为:应付结算和应收结算。以下内容主要介绍针对To B的应付结算体系系统。

结算案例

为了让大家更好理解,举一个B端商品采购的例子。

商人公司在1月份5号,20号,29号,分别向后粮采购饮料,总共价值20万。商人公司收到了多笔的收货单,系统在2.1号生成对账单,后粮核对账单货款金额(20万),给商人公司开了20万货款发票,结算在核对发票的实开票金额无误后,根据实付金额(20万)生成付款单,在预计付款日(2月15号)时资金组就往外打款。

案例中涉及到的重要概念:

  1. 签约公司:也叫主体公司,是合同中的甲方。以上例子的商人公司。
  2. 乙方(实体供应商/签约供应商):合同中乙方。以上例子的后粮。
  3. 主数据(商品信息,门店信息等等):这些主数据是产生交易数据的基础。比如商品信息,货物类结算是对商品的结算。以上例子的饮料。
  4. 合同/协议:结算是基于合同/协议来进行,结算的周期,支付金额,费用等都是基于合同协议来产生的。
  5. 交易数据:交易数据是结算的明细数据。把一定周期内的交易数据进行多个维度的汇总,形成了账单。以上例子的 收货单。
  6. 账单:是结算单,一定周期交易数据的汇总。包含货款、费用等等。
  7. 预付款单:提前预付给乙方公司金额单据,是基于采购单金额比例来计算的。
  8. 税票(应收发票):对于甲方公司来说,对于货款发票来说,就需要让乙方公司开应收发票。
  9. 付款单:基于账单产生对外付款的单据。可以针对同一个乙方的多次对账单进行一次性付款。

结算业务流程

1.jpeg

结算业务过程一般会分为四个阶段(如上图所示):

商务洽谈阶段

在这个阶段,系统能做的事情就是维护和管理信息流数据。大部分工作还是采购人员的线下工作。

信息流会分为:主数据和合同协议数据。

主数据会包含:商品信息、门店信息、仓库信息、签约公司信息、供应商信息、费用类型信息等等。

合同数据会有各种分类,比如基于时间周期来分为框架合同、一次性合同;基于交易数据类型会分为:商品合同、租赁合同、资产合同等等。

这个阶段完成后,会产生具有法律效应的合同协议,后续的阶段会完全依照合同协议进行交易、结算。

交易阶段

在交易环节中,主要针对交易流数据的管理,确保了交易数据的准确性,可预测性,从而达到最大限度的减少人为干预。实现各种单据状态的扭转;

实现智能下单等等。比如:从”采购单–>配送单–>收货单”一系列流程。

完整的ERP系统一般会包含以下的各种单据:

  • 采购单
  • 配送单
  • 收货单
  • 调整单
  • 调拨单

收货单是结算的主角,在单据完结时,系统就要进行结算了。

结算阶段

当交易环节中的单据(收货单)达到了完结状态,也就是可结算状态,且到了合同协议所约定的结算时间了,那么单据就会自动进入到结算阶段。

结算阶段分为三个部分: 结算单、税票、付款。

结算单

结算单中几个重要概念:

结算周期

  • 月结:按照自然月走,会圈一个月的交易数据,在下月1号出账单。
  • 半月结:按照半个月走,会圈半个月的交易数据,在每月16号或下月1号分别出账单。
  • 日结:按照天走,会圈一天的交易数据,T+1出账单。
  • 周结:按照周走,会圈一周的交易数据,下周一出账单。

货款

具体的交易数据,比如供应链系统中针对商品、资产、服务的收货单;插旗合同中的支付计划等数据。

费用

基于合同协议上商务条款中所约定的费用项以及费用算法计算出来的费用。上文也有提及费用分类(即账扣或票扣)

预计付款日

合同协议上所约定的付款的日期。

基于结算周期,在出账单日时系统自动把交易数据,费用数据进行汇总,从而生成结算单。

结算单会面临是否缺少交易数据、费用算法临时调整、商品税率信息错误、合同未生效等问题。

税票

结算单生成后,就涉及到税票事项。会基于合同协议中约定好先款后票或先票后款进行处理。

税票一般分为:专票、普票、电子票。不同类型的票 涉及到税法规则不一样。

不管以上两种方式的哪一种,都会涉及以下问题:

  • 开票问题
  • 收票问题
  • 发票辨真伪的问题
  • 发票如何自动匹配结算单问题

付款

当结算单达到可付款状态时,就能自动生成付款单或者需要人为(结算组/采购中台)去生成付款单,付款单审核通过后,通过银企直联直接打款。

那么付款会承担如何付、付多少钱、付款前校验、付款后核对等等职责。

核算阶段

公司每个月就会进行结账,那么会对已生成的结算单做应付暂估凭证;对付款成功的付款单做实付凭证等等,再基于这些凭证制作出财务报表;

会把 信息流、交易流、支付流 汇总成财务报表。至此一个结算过程就形成了一个闭环。

服务对象

一个结算过程中,需要公司的各个部门各个职位的人参与其中,包含以下重要的部门,但不止于这些。

结算组

最重要的部门,职责包含如下

  1. 负责核对结算单:在系统核对完毕后,会手工核对一下。
  2. 负责收票:催供应商发票、收取应收发票。
  3. 负责开票:生成开票任务,催税务组同事开票。
  4. 负责生成付款单。

税务组

  1. 开票,从百望/明硕等税控软件开票
  2. 报税

资金组

  1. 实际打款,负责银企客户端上操作

采购中台

  1. 前期商务洽谈,签订合同
  2. 问题反馈
  3. 后期维护沟通

供应商

  1. 采购订单处理
  2. 核对账单
  3. 录入货款发票、寄出纸质发票
  4. 收取费用发票

其它功能部门…..

结算单据

结算过程涉及到重要的单据如下:

  • 采购单
  • 收货单
  • 账单(结算单)
  • 预付款单
  • 发票收据
  • 付款单

下图是各个单据之间的关系如下图:

2.jpeg

需要注意2个点:

  • 采购单如果是预付了,那么对应的账单就不能再次支付了,此时账单只需要跟预付款单核销即可。
  • 账单在付款条件时先票后款时,需要审核发票,通过后才付款。

产品架构图

单据从产生到结算,到付款,到核算会涉及如下系统

3.jpeg

对账系统会承载 结算系统与其他系统数据、状态、金额多方面的对账。

结算系统体系

结算体系线上化目标

  • 提高人效,释放人员
  • 解决系统/业务痛点
+ 大数据处理问题
+ 结算单缺数据、费用费率调整、商品税率错误等问题
+ 发票验真伪、匹配结算单等问题
+ 资金风险控制问题
+ 银行对账实时性问题

结算体系成果

  1. 系统内金额计算准确,且级联关系准确,各个维度的金额100%一致。
  2. 系统容量与吞吐量易横向扩展。
  3. 结算多维度校验,校验准确率99.99%。
  4. 支付多维度防重及打款前校验,打款正确率100%。

思考与互动

  1. 在N多上游系统情况下,如何确保结算系统独善其身,尽量少受影响,保持高可用?
  2. 在大量数据情况下,如何确保结算系统能高性能,实时计算出账?

结束语

以上和大家分享了财务技术组在财务系统之结算体系的知识脉络,后续会陆续推出技术细节篇章。读者朋友们有想法或建议的话,欢迎留言交流。

如果你对相关的财务技术感兴趣,欢迎加入我们。 可投递简历至:tech-hiring@bianlifeng.com(邮件标题注明:便利蜂财务技术团队)。

作者

周同学,财务技术组负责人。

郑同学,财务技术组资深工程师,结算体系缔造者,深度参与财务体系的设计、开发、维护。目前正和小伙伴们一起,为“品质生活 便利中国”的使命愿景贡献自己的一份力量。

便利蜂招聘

  • bianlifeng.gllue.me/portal/home…
  • 点击进入,了解更多职位详情

本文转载自: 掘金

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

5行Python实现验证码识别,太稳了

发表于 2021-08-11
1
复制代码很久之前,分享过一次Python代码实现验证码识别的办法。

当时采用的是pillow+pytesseract,优点是免费,较为易用。但其识别精度一般,若想要更高要求的验证码识别,初学者就只能去选择使用百度API接口了。

但其实百度API接口和pytesseract其实都需要进行前期配置,对于初学者来说就不太友好了。

而且百度API必须要联网,对于某些机器不能联网的朋友而言,就得pass了

最近群里有位群友分享了一个新库,试用一下发现非常实用,特意今天分享给大家。最后,如果你的时间不是很紧张,并且又想快速的提高,最重要的是不怕吃苦,建议你可以联系维:762459510 ,那个真的很不错,很多人进步都很快,需要你不怕吃苦哦!大家可以去添加上看一下~

Github地址:github.com/sml2h3/dddd…

该库名也是非常有趣 —— ddddocr(谐音带带弟弟OCR)

环境要求:

1
erlang复制代码python >= 3.8Windows/Linux/Macox..

可以通过以下命令安装

1
复制代码pip install ddddocr

参数说明:

5行Python实现验证码识别,太稳了

在网上随机寻找了一个验证码图片,使用这个库来实战一下。

5行Python实现验证码识别,太稳了

来源:百度搜索

import ddddocr

ocr = ddddocr.DdddOcr()

with open(‘1.png’, ‘rb’) as f:

img_bytes = f.read()

res = ocr.classification(img_bytes)

print(res)

5行Python实现验证码识别,太稳了

成功识别出来了验证码文字!

而且优点也非常明显:首先代码非常精简,对比前文提到的两种方法,不需要额外设置环境变量等等,5行代码即可轻松识别验证码图片。另一方面,我们使用魔法命令%%time也测试出来吗,这段代码识别速度非常快。最后,如果你的时间不是很紧张,并且又想快速的提高,最重要的是不怕吃苦,建议你可以联系维:762459510 ,那个真的很不错,很多人进步都很快,需要你不怕吃苦哦!大家可以去添加上看一下~

下面用更多的验证码图片继续测试:

5行Python实现验证码识别,太稳了

我又找了6个验证码图片来测试,观察结果,发现这类简单的验证码基本可以进行快速识别。但也有部分结果有问题——字母大小写没有进行区分(比如第6张图片)。

总而言之,如果你需要进行验证码识别,且对精度要求不是过高。

那么,带带弟弟OCR(ddddocr)这个库是一个不错的选择~

本文转载自: 掘金

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

MyBatis事务管理

发表于 2021-08-11

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

MyBatis事务管理

在上一篇文章中我们讲解了MyBatis的入门程序,在MyBatis的核心配置文件中,我们在配置环境的时候有< transactionManager type="JDBC"></transactionManager>这么一个标签,该标签代表的就是配置一个事务管理器,其type属性表示配置的事务类型。在这里的type属性值即事务的类型可以有两种,除了上面的JDBC类型,还有MANAGED类型,这两种类型最主要的区别就是JDBC类型采用的是JDBC提供的事务管理机制,而MANAGED类型则是将事务管理交由容器处理,如交由Spring处理。下面将分别介绍这两种事务管理。关于事务的相关知识,可以查看我的另外一篇文章 ⏩ # MySQL事务。

image.png

🚩Transaction
在MyBatis中事务被抽象成一个Transation接口,该接口定义了一系列操作事务的方法,包括获取连接、提交事务、回滚事务、关闭连接。该接口有三个实现类,分别为JdbcTransaction、ManagedTransaction、SpringManagedTransaction

  • Transaction接口源码如下
1
2
3
4
5
6
7
8
9
10
11
csharp复制代码public interface Transaction {
Connection getConnection() throws SQLException;

void commit() throws SQLException;

void rollback() throws SQLException;

void close() throws SQLException;

Integer getTimeout() throws SQLException;
}
  • 其类图如下

image.png

采用JDBC的事务管理机制

在了解JDBC的事务管理机制之前,我们需要先知道MyBatis事务管理机制是如何运行的。首先,在MyBatis初始化加载主配置文件的过程中,会对我们主配置文件进行解析,MyBatis会根据我们< transactionManager >标签中配置的type属性来生成一个TransactionFactory事务工厂实例 ,TransactionFactory是一个接口,其主要有JdbcTransactionFactory和ManagedTransactionFactory这两个实现类,其中定义了两种创建Transaction的方法,一种根据Connection来创建,另一种根据Datasource数据源、事务隔离级别、是否自动提交来创建Transaction。如果type为JDBC,则生成一个JdbcTransactionFactory实例,它会创建JDBC类型的事务,就是JdbcTransaction。有了JdbcTransaction,JdbcTransaction就会从DataSource获取连接对象Connection,在事务处理上,JdbcTransaction是对java.sql.Connection的一个包装,它是使用Connection对象进行事务的管理。

  • TransactionFactory接口源码
1
2
3
4
5
6
7
8
java复制代码public interface TransactionFactory {
default void setProperties(Properties props) {
}
//根据指定的Connection来创建Transaction
Transaction newTransaction(Connection var1);
//根据数据源、事务隔离级别、是否自动提交事务来创建Transaction
Transaction newTransaction(DataSource var1, TransactionIsolationLevel var2, boolean var3);
}
  • TransactionFactory接口的实现类JdbcTransactionFactory源码,用来创建JdbcTransaction实例
1
2
3
4
5
6
7
8
9
10
11
12
13
typescript复制代码public class JdbcTransactionFactory implements TransactionFactory {
public JdbcTransactionFactory() {
}

//创建一个JdbcTransaction实例
public Transaction newTransaction(Connection conn) {
return new JdbcTransaction(conn);
}

public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
return new JdbcTransaction(ds, level, autoCommit);
}
}
  • JdbcTransaction实现类源码
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
kotlin复制代码public class JdbcTransaction implements Transaction {
protected Connection connection;
protected DataSource dataSource;
protected TransactionIsolationLevel level;
protected boolean autoCommit;

//获取连接
public Connection getConnection() throws SQLException {
if (this.connection == null) {
this.openConnection();
}

return this.connection;
}
//事务提交
public void commit() throws SQLException {
if (this.connection != null && !this.connection.getAutoCommit()) {
if (log.isDebugEnabled()) {
log.debug("Committing JDBC Connection [" + this.connection + "]");
}

this.connection.commit();
}

}
//事务回滚
public void rollback() throws SQLException {
if (this.connection != null && !this.connection.getAutoCommit()) {
if (log.isDebugEnabled()) {
log.debug("Rolling back JDBC Connection [" + this.connection + "]");
}

this.connection.rollback();
}

}
//关闭连接
public void close() throws SQLException {
if (this.connection != null) {
this.resetAutoCommit();
if (log.isDebugEnabled()) {
log.debug("Closing JDBC Connection [" + this.connection + "]");
}

this.connection.close();
}

}

......略
}

采用MANAGED的事务管理机制

采用MANAGED类型的事务管理机制与JDBC的事务管理机制不同的是MANAGED类型是将事务管理交由容器来处理,MyBatis本身不会去实现事务管理的相关操作。如果我们的type属性值设置为MANAGED类型,最终会生成ManagedTransaction实例,,通过对源码的观察,则,可以发现Transaction的实现类ManagedTransaction中的commit方法和rollback方法没有具体实现,所以ManagedTransaction是让容器来管理事务Transaction的整个生命周期。我们考虑一下,如果MyBatis和Spring框架整合,那么Spring会怎么来管理事务呢?关于Spring事务,可以看我的另一篇文章⏩Spring事务

  • ManagedTransaction类部分源码
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
java复制代码public class ManagedTransaction implements Transaction {
private DataSource dataSource;
private TransactionIsolationLevel level;
private Connection connection;
private final boolean closeConnection;

public Connection getConnection() throws SQLException {
if (this.connection == null) {
this.openConnection();
}

return this.connection;
}

public void commit() throws SQLException {
//没有内容
}

public void rollback() throws SQLException {
//没有内容
}

public void close() throws SQLException {
if (this.closeConnection && this.connection != null) {
if (log.isDebugEnabled()) {
log.debug("Closing JDBC Connection [" + this.connection + "]");
}

this.connection.close();
}

}

略...
}

🏁以上就是对MyBatis事务管理的介绍,如果有错误的地方,还请留言指正,如果觉得本文对你有帮助那就点个赞👍吧😋😻😍

默认标题_动态分割线_2021-07-15-0.gif

本文转载自: 掘金

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

Netty 源码分析系列(七)字节缓冲区 ByteBuf(下

发表于 2021-08-11

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

前言

在了解了 ByteBuffer 的原理之后,再来理解Netty 的 ByteBuf 就比较简单了。

ByteBuf 是 Netty 框架封装的数据缓冲区,区别于 position、limit、flip等属性和操作来控制 ByteBuffer 的读写,ByteBuf 通过两个位置指针来协助缓冲区的读写操作,分别是readIndex和writeIndex。

readIndex、writeIndex和capacity变量存在以下关系:

1
复制代码0 <= readIndex <= writeIndex <= capacity

实现原理

初始化 ByteBuffer 时,readIndex 和 writeIndex 取值一开始都是0。如下图所示:

image-20210811125716242

当执行写入数据之后,writeIndex会增加,如下图所示:

image-20210811130142931

当执行读入数据之后则会使readIndex增加,但不会超过writeIndex,如下图:

image-20210811130551547

在读取之后,索引 0 到 readIndex位置的区域被视为废弃字节(discard)。可以调用discardReadBytes方法,来释放这部分空间,其作用类似于 ByteBuffer的compact()方法,移除无用的数据,实现缓冲区的重复利用。如下图,展示了执行discardReadBytes后的情况,相当于可写的空间变大了。

image-20210811130822648

ByteBuf 的使用案例

为了更好的理解ByteBuf,编写了以下示例:

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
java复制代码public class ByteBufDemo {

/**
* @param args
*/
public static void main(String[] args) {
// 创建一个缓冲区
ByteBuf buffer = Unpooled.buffer(10);
System.out.println("------------初始时缓冲区------------");
printBuffer(buffer);

// 添加一些数据到缓冲区中
System.out.println("------------添加数据到缓冲区------------");

String s = "love";
buffer.writeBytes(s.getBytes());
printBuffer(buffer);

// 读取数据
System.out.println("------------读取数据------------");

while (buffer.isReadable()) {
System.out.println(buffer.readByte());
}

printBuffer(buffer);

// 执行compact
System.out.println("------------执行discardReadBytes------------");
buffer.discardReadBytes();
printBuffer(buffer);

// 执行clear
System.out.println("------------执行clear清空缓冲区------------");
buffer.clear();
printBuffer(buffer);

}

/**
* 打印出ByteBuf的信息
*
* @param buffer
*/
private static void printBuffer(ByteBuf buffer) {
System.out.println("readerIndex:" + buffer.readerIndex());
System.out.println("writerIndex:" + buffer.writerIndex());
System.out.println("capacity:" + buffer.capacity());
}
}

输出结果:

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
java复制代码------------初始时缓冲区------------
readerIndex:0
writerIndex:0
capacity:10
------------添加数据到缓冲区------------
readerIndex:0
writerIndex:4
capacity:10
------------读取数据------------
108
111
118
101
readerIndex:4
writerIndex:4
capacity:10
------------执行discardReadBytes------------
readerIndex:0
writerIndex:0
capacity:10
------------执行clear清空缓冲区------------
readerIndex:0
writerIndex:0
capacity:10

Process finished with exit code 0

对比ByteBuffer和ByteBuf两个示例可以看出,Netty 提供了更加方便地创建ByteBuf的工具(unpooled),同时,也不必再执行flip()方法来切换读写模式。对比而言,ByteBuf更加易于使用。

ByteBuf 的3种使用模式

ByteBuf 共有三种使用模式:堆缓冲区模式(Heap Buffer)、直接缓冲区模式(Direct Buffer)和 复合缓冲区模式(Composite Buffer)。

堆缓冲模式

堆缓冲区模式又称为支撑数组,其数据是存放在JVM的堆空间,通过将数据存储在数组中实现。

优点:数据存储在JVM堆中可以快速的创建和快速释放,并且提供了数据快速访问的方法;

缺点:每次数据与 I/O 进行传输时,都需要将数据复制到直接缓冲区。

以下是堆缓冲区的代码示例:

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
java复制代码public class ByteBufHeapBufferDemo {

/**
* @param args
*/
public static void main(String[] args) {

// 创建一个堆缓冲区
ByteBuf buffer = Unpooled.buffer(10);
String s = "waylau";
buffer.writeBytes(s.getBytes());

// 检查是否是支撑数组
if (buffer.hasArray()) {

// 获取支撑数组的引用
byte[] array = buffer.array();

// 计算第一个字节的偏移量
int offset = buffer.readerIndex() + buffer.arrayOffset();

// 可读字节数
int length = buffer.readableBytes();
printBuffer(array, offset, length);
}
}

/**
* 打印出Buffer的信息
*
* @param buffer
*/
private static void printBuffer(byte[] array, int offset, int len) {
System.out.println("array:" + array);
System.out.println("array->String:" + new String(array));
System.out.println("offset:" + offset);
System.out.println("len:" + len);
}
}

输出结果:

1
2
3
4
5
6
java复制代码array:[B@5b37e0d2
array->String:waylau
offset:0
len:6

Process finished with exit code 0

直接缓冲区模式

直接缓冲区属于堆外分配的直接内存,不会占用堆得空间。

优点:使用 socket 传输数据时性能很好,避免了数据从 JVM 堆内存复制到直接缓冲区的过程,提高了性能。

缺点:相对于堆缓冲区而言,直接缓冲区分配内存空间和释放更为昂贵。

对于涉及大量的 I/O 数据的读写,建议使用直接缓冲区。而对于用于后端业务消息编解码模块,建议使用堆缓冲区。

以下是直接缓冲区代码示例:

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
java复制代码public class ByteBufDirectBufferDemo {

/**
* @param args
*/
public static void main(String[] args) {

// 创建一个直接缓冲区
ByteBuf buffer = Unpooled.directBuffer(10);
String s = "waylau";
buffer.writeBytes(s.getBytes());

// 检查是否是支撑数组.
// 不是支撑数组,则为直接缓冲区
if (!buffer.hasArray()) {

// 计算第一个字节的偏移量
int offset = buffer.readerIndex();

// 可读字节数
int length = buffer.readableBytes();

// 获取字节内容
byte[] array = new byte[length];
buffer.getBytes(offset, array);

printBuffer(array, offset, length);
}
}

/**
* 打印出Buffer的信息
*
* @param buffer
*/
private static void printBuffer(byte[] array, int offset, int len) {
System.out.println("array:" + array);
System.out.println("array->String:" + new String(array));
System.out.println("offset:" + offset);
System.out.println("len:" + len);
}
}

输出结果:

1
2
3
4
5
6
java复制代码array:[B@6d5380c2
array->String:waylau
offset:0
len:6

Process finished with exit code 0

复合缓冲区模式

复合缓冲区是 Netty 特有的缓冲区。本质上类似于提供一个或多个 ByteBuf 的组合视图,可以根据需要添加和删除不同类型的 ByteBuf。

优点:提供了一种访问方式让使用者自由地组合多个ByteBuf,避免了复制和分配新的缓冲区。

缺点:不支持访问其支撑数组。因此如果要访问,需要先将内容复制到堆内存中,再进行访问。

以下示例是复合缓冲区将堆缓冲区和直接缓冲区组合在一起,没有进行任何复制过程,仅仅创建了一个视图而已。

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
java复制代码public class ByteBufCompositeBufferDemo {

/**
* @param args
*/
public static void main(String[] args) {

// 创建一个堆缓冲区
ByteBuf heapBuf = Unpooled.buffer(3);
String way = "way";
heapBuf.writeBytes(way.getBytes());

// 创建一个直接缓冲区
ByteBuf directBuf = Unpooled.directBuffer(3);
String lau = "lau";
directBuf.writeBytes(lau.getBytes());

// 创建一个复合缓冲区
CompositeByteBuf compositeBuffer = Unpooled.compositeBuffer(10);
compositeBuffer.addComponents(heapBuf, directBuf); // 将缓冲区添加到符合缓冲区

// 检查是否是支撑数组.
// 不是支撑数组,则为复合缓冲区
if (!compositeBuffer.hasArray()) {

for (ByteBuf buffer : compositeBuffer) {
// 计算第一个字节的偏移量
int offset = buffer.readerIndex();

// 可读字节数
int length = buffer.readableBytes();

// 获取字节内容
byte[] array = new byte[length];
buffer.getBytes(offset, array);

printBuffer(array, offset, length);
}

}
}

/**
* 打印出Buffer的信息
*
* @param buffer
*/
private static void printBuffer(byte[] array, int offset, int len) {
System.out.println("array:" + array);
System.out.println("array->String:" + new String(array));
System.out.println("offset:" + offset);
System.out.println("len:" + len);
}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
java复制代码array:[B@4d76f3f8
array->String:way
offset:0
len:3
array:[B@2d8e6db6
array->String:lau
offset:0
len:3

Process finished with exit code 0

CompositeByteBuf是一个虚拟的缓冲区,其用途是将多个缓冲区显示为单个合并缓冲区,类似数据库中的视图。

总结

通过以上对于ByteBuf的介绍,相信小伙伴们对于ByteBuf的原理也有了一定的了解。下一节我们继续深入Netty的源码。

结尾

我是一个正在被打击还在努力前进的码农。如果文章对你有帮助,记得点赞、关注哟,谢谢!

本文转载自: 掘金

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

信创产业已成现象级新风口,快来加入争做“弄潮儿”

发表于 2021-08-11

摘要:为了让更多的技术开发者加入到自主创新的大潮中,2021 信创“大比武”活动在 7 月 14 日正式开启。

本文分享自华为云社区《信创产业已成现象级新风口,快来加入争做“弄潮儿”》,作者:技术火炬手 。

近年来,我国互联网、电商、游戏等产业蓬勃发展,但其背后的基础软硬件设施却不少是非自主研发产品,导致技术“卡脖子”的现象频出。危机中,一大批国产厂商正在崛起,他们从底层硬件、操作系统、数据库、应用软件等出发,用技术创新撬动既有的格局。

为了让更多的技术开发者加入到这波自主创新的大潮中,由沈阳市人民政府、长沙市人民政府、重庆市渝中区人民政府指导,中国电子工业标准化技术协会信息技术应用创新工作委员会(简称信创工委会)主办的 2021 信创“大比武”活动在 7 月 14 日正式开启。

信创打造中国标准

谈到自主创新,不得不提当下信息技术应用创新产业(简称信创)的发展,信创产业生态体系庞大,它的目标是实现信息技术领域的自主可控和信息安全。在基础硬件、应用软件、信息安全等领域打造“中国标准”。

从产业链角度看,主要包括基础硬件(芯片、存储器、服务器)、基础软件(操作系统、中间件、数据库)、用软件(办公软件、浏览器、邮件等)和信息安全(安全管理、安全技术、安全标准)。其中芯片、整机、操作系统、数据库、中间件是最重要的产业链环节。

国内产业自主创新并不等于彻底的技术封闭,开源是信创生态推进的重要手段。2021 年《企业开源现状》调查报告显示“最具创新能力的企业都在使用开源”。开源创新模式有助于快速聚集大众智慧、加速生态发展。根据 Gartner 统计,99%的组织在其 IT 系统中使用了开源软件。不仅如此,在今年 3 月份,“十四五”规划明确提出支持开源发展,开源成为中国实现信息技术领域弯道超车的关键,对实现核心基础技术自主可控具有重要意义。

开源对信创的意义,体现在 3 个方面:向开源项目贡献源代码,可以扩大技术影响力,增强和主流技术趋势的绑定;借助开源软件资源,可以节约不必要的成本;开源社区有助于汇聚开发者的力量,形成强大的创新来源。从现有信创生态来看,操作系统,到数据库、中间件,以及办公软件等,都依赖于开源生态。

建设信创“大楼”,开源 “砖瓦”是关键

近年来,国内企业逐渐侧重基础软件领域开源项目布局,在芯片、数据库、操作系统、云服务等领域均有较大突破。以 CPU 芯片为例,国产 CPU 中鲲鹏、飞腾、龙芯采用指令集授权或自研架构,自主先进程度相对较高。其中鲲鹏处理器,聚焦计算架构创新、处理器和开源基础软件的研发,携手外部上下游企业形成鲲鹏信创生态体系。操作系统方面,国产主流操作系统均是基于 Linux 内核的二次开发,随着企业在操作系统领域研发力度的持续加大,发展成效日渐显著,部分产品已完成自主研发与生产,产品性能大幅提升,进入规模化阶段。

为信创产业添砖加瓦

在“2021 信息技术应用创新论坛”中的 2021 信创“比武”活动,由信创工委会指导,华为主办,重庆鲲鹏创新中心、北京鲲鹏联合创新中心、开源操作系统 openEuler、开源数据库 openGauss、开源大数据引擎 openLooKeng 社区、同方股份信创业务集团共同承办的鲲鹏基础软件开发赛道,面向 openEuler、openGauss 和 openLooKeng 三大开源社区领域设置赛题。

作为鲲鹏计算平台的三大开源软件:openEuler 作为全场景协同的统一操作系统,协调不同架构之间数据的流动;企业级开源数据库 openGauss,可高效地存储、管理和运维数据库;openLooKeng 成为连接各种不同数据源的虚拟化引擎分析工具,他们分别承担着数据驱动的场景创新。

本次大赛将最终角逐出一等奖 1 个 ,奖金 100000 元;二等奖 2 名,每个奖金 50000 元;三等奖 3 名,每个奖金 20000 元;优胜奖 4 名,HUAWEI Mate40 和代码合入奖 6 名,HUAWEIMatePad 11。

再次提醒大家本次 2021 信创“大比武” ·鲲鹏基础软件开发赛道的时间安排:报名和作品提交时间截止到 8 月 31 日,鲲鹏赛道初赛截止 9 月 15 日,9 月 30 日进行决赛,最终在 10 月举行信创“大比武”总决赛。还等什么呢?快速搭载上这趟技术直通车,一展你的才华风采,和华为鲲鹏一起“创造”未来吧。点击进入报名页面,快速提交作品。

点击关注,第一时间了解华为云新鲜技术~

本文转载自: 掘金

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

1…568569570…956

开发者博客

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