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

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


  • 首页

  • 归档

  • 搜索

可重入锁-synchronized是可重入锁吗?Reentr

发表于 2020-11-14

前言

面试题:synchronized是可重入锁吗?


答案:synchronized是可重入锁。ReentrantLock也是的。

1、什么是可重入锁呢?

关于什么是可重入锁,我们先来看一段维基百科的定义。

若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。

通俗来说:当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。


再换句话说:可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。

2、自己写代码验证下可重入和不可重入

我们启动一个线程t1,调用addOne()方法来执行加1操作。在addOne方法里面t1会获得rtl锁,然后调用get()方法,在get()方法里再次请求获取trl锁。


因为最终能打印value=1,说明t1在第二次获取锁的时候并没有阻塞。说明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
33
34
35
36
37
38
39
40
java复制代码import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantTest {
private final Lock rtl = new ReentrantLock();
int value = 0;

public static void main(String[] args) throws InterruptedException {
ReentrantTest test = new ReentrantTest();
// 新建一个线程 进行加1操作
Thread t1 = new Thread(() -> test.addOne());
t1.start();
// main线程等待t1线程执行完
t1.join();
System.out.println(test.value);
}


public int get() {
// 获取锁
rtl.lock();
try {
return value;
} finally {
// 保证锁能释放
rtl.unlock();
}
}

public void addOne() {
// 获取锁
rtl.lock();
try {
value = 1 + get();
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}
换成synchronized的加锁方式,同样能打印value的值。证明synchronized也是可重入锁。
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
csharp复制代码public class ReentrantTest {
private final Object object = new Object();
int value = 0;

public static void main(String[] args) throws InterruptedException {
ReentrantTest test = new ReentrantTest();
// 新建一个线程 进行加1操作
Thread t1 = new Thread(() -> test.addOne());
t1.start();

t1.join();
System.out.println(test.value);
}


public int get() {
// 再此获取锁
synchronized (object) {
return value;
}
}

public void addOne() {
// 获取锁
synchronized (object) {
value = 1 + get();
}
}
}

3、自己如何实现一个可重入和不可重入锁呢

不可重入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class Lock{
private boolean isLocked = false;
public synchronized void lock()
throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
}

public synchronized void unlock(){
isLocked = false;
notify();
}
}

可重入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ini复制代码public class Lock{
boolean isLocked = false;
Thread lockedBy = null;
int lockedCount = 0;
public synchronized void lock() throws InterruptedException{
Thread callingThread = Thread.currentThread();
while(isLocked && lockedBy != callingThread){
wait();
}
isLocked = true;
lockedCount++;
lockedBy = callingThread;
}
public synchronized void unlock(){
if(Thread.curentThread() == this.lockedBy){
lockedCount--;
if(lockedCount == 0){
isLocked = false;
notify();
}
}
}
}
从代码实现来看,可重入锁增加了两个状态,锁的计数器和被锁的线程,实现基本上和不可重入的实现一样,如果不同的线程进来,这个锁是没有问题的,但是如果进行递归计算的时候,如果加锁,不可重入锁就会出现死锁的问题。

4、ReentrantLock如何实现可重入的

使用ReentrantLock你要知道:
ReentrantLock支持公平和非公平2种创建方式,默认创建的是非公平模式的锁。

看下它的构造方法:

1
2
3
4
5
6
scss复制代码public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

看下非公平锁,它是继承抽象类Sync的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scala复制代码static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;

/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}

看下公平锁,它也是继承抽象类Sync的:

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复制代码static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;

final void lock() {
acquire(1);
}

/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
NonfairSync、FairSync 和抽象类Sync 都是ReentrantLock的内部类。


Sync的定义,它是继承AbstractQueuedSynchronizer的,AbstractQueuedSynchronizer既是我们常说的**AQS**(后面我也会整理一篇)
1
2
scala复制代码abstract static class Sync extends AbstractQueuedSynchronizer {
}
好了,继承关系清楚了 ,现在我们看下ReentrantLock是如何实现可重入的


我们在addOne()和get()两个方法加锁的地方都打上断点。然后开始调式:
  • addOne方法获取锁的时候走到NonfairSync的“compareAndSetState(0, 1)”,通过CAS设置state的值为1,调用成功,并设置当前锁被持有的线程为当前线程t1;
  • 继续调试,get方法获取锁的时候走到NonfairSync的“compareAndSetState(0, 1)”,通过CAS设置state的值为1,调用失败(因为已经被当前线程t1锁占有),走到else里面,继续往里看;
  • 走到NonfairSync的tryAcquire方法,再往里走;
  • 会调用Sync抽象类里面的nonfairTryAcquire方法。源码解释我都写在下面了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码final boolean nonfairTryAcquire(int acquires) {
// 当前线程
final Thread current = Thread.currentThread();
// state变量的值
int c = getState();
// 因为c当前值为1,所以走else里面
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 判断当前线程 是不是 当前锁被持有的线程 ,判断为 true
else if (current == getExclusiveOwnerThread()) {
// c + acquires = 1 + 1 = 2
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);// 将state的值赋值为2
return true;
}
return false;
}
到此,可重入锁加锁的过程分析完毕。解锁的过程一样,希望你能自己debug下【调用的是Sync抽象类里面的tryRelease方法】


我这里总结一下:
  • 当线程尝试获取锁时,可重入锁先尝试获取并更新state值
    如果state == 0表示没有其他线程在执行同步代码,则通过CAS把state置为1 会成功,当前线程继续执行。
    如果status != 0,通过CAS把state置为1 会失败,然后判断当前线程是否是获取到这个锁的线程,如果是的话执行state+1,且当前线程可以再次获取锁。
  • 释放锁时,可重入锁同样先获取当前state的值,在当前线程是持有锁的线程的前提下。
    如果status-1 == 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程才会真正释放锁。
你需要注意的是state变量的定义,其实AQS的实现类都是通过控制state的值来控制锁的状态的。它被**volatile所修饰,能保证可见性**。
1
arduino复制代码private volatile int state;
扩展:如果要通过AQS的state来实现非可重入锁怎么实现呢?明确这两点就可以了:
  • 获取锁时:去获取并尝试更新当前status的值,如果status != 0的话会导致其获取锁失败,当前线程阻塞。
  • 释放锁时:在确定当前线程是持有锁的线程之后,直接将status置为0,将锁释放。

5、可重入锁的特点

可重入锁的一个优点是可**一定程度避免死锁**。
可重入锁能避免一定线程的等待,可想而知**可重入锁性能会高于非可重入锁**。你可以写程序测试一下哦!!!

推荐阅读:

Java内存模型-volatile的应用(实例讲解)

synchronized的三种应用方式(实例讲解)

大彻大悟synchronized原理,锁的升级

一文弄懂Java的线程池

本文转载自: 掘金

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

什么是接口的幂等性,如何实现接口幂等性?一文搞定 每天一个知

发表于 2020-11-13

微信搜索《Java鱼仔》,每天一个知识点不错过

每天一个知识点

什么是接口的幂等性,如何实现接口幂等性?

(一)幂等性概念

幂等性原本是数学上的概念,用在接口上就可以理解为:同一个接口,多次发出同一个请求,必须保证操作只执行一次。
调用接口发生异常并且重复尝试时,总是会造成系统所无法承受的损失,所以必须阻止这种现象的发生。
比如下面这些情况,如果没有实现接口幂等性会有很严重的后果:
支付接口,重复支付会导致多次扣钱
订单接口,同一个订单可能会多次创建。

(二)幂等性的解决方案

唯一索引
使用唯一索引可以避免脏数据的添加,当插入重复数据时数据库会抛异常,保证了数据的唯一性。

乐观锁
这里的乐观锁指的是用乐观锁的原理去实现,为数据字段增加一个version字段,当数据需要更新时,先去数据库里获取此时的version版本号

1
sql复制代码select version from tablename where xxx

更新数据时首先和版本号作对比,如果不相等说明已经有其他的请求去更新数据了,提示更新失败。

1
sql复制代码update tablename set count=count+1,version=version+1 where version=#{version}

悲观锁
乐观锁可以实现的往往用悲观锁也能实现,在获取数据时进行加锁,当同时有多个重复请求时其他请求都无法进行操作

分布式锁
幂等的本质是分布式锁的问题,分布式锁正常可以通过redis或zookeeper实现;在分布式环境下,锁定全局唯一资源,使请求串行化,实际表现为互斥锁,防止重复,解决幂等。

token机制
token机制的核心思想是为每一次操作生成一个唯一性的凭证,也就是token。一个token在操作的每一个阶段只有一次执行权,一旦执行成功则保存执行结果。对重复的请求,返回同一个结果。token机制的应用十分广泛。

(三)token机制的实现

这里展示通过token机制实现接口幂等性的案例:github文末自取
首先引入需要的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

3.1、配置请求的方法体和枚举类

首先配置一下通用的请求返回体

1
2
3
4
5
6
java复制代码public class Response {
private int status;
private String msg;
private Object data;
//省略get、set、toString、无参有参构造方法
}

以及返回code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码public enum ResponseCode {
// 通用模块 1xxxx
ILLEGAL_ARGUMENT(10000, "参数不合法"),
REPETITIVE_OPERATION(10001, "请勿重复操作"),
;
ResponseCode(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
private Integer code;
private String msg;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}

3.2 自定义异常以及配置全局异常类

1
2
3
4
5
java复制代码public class ServiceException extends RuntimeException{
private String code;
private String msg;
//省略get、set、toString以及构造方法
}

配置全局异常捕获器

1
2
3
4
5
6
7
8
9
java复制代码@ControllerAdvice
public class MyControllerAdvice {
@ResponseBody
@ExceptionHandler(ServiceException.class)
public Response serviceExceptionHandler(ServiceException exception){
Response response=new Response(Integer.valueOf(exception.getCode()),exception.getMsg(),null);
return response;
}
}

3.3 编写创建Token和验证Token的接口以及实现类

1
2
3
4
5
java复制代码@Service
public interface TokenService {
public Response createToken();
public Response checkToken(HttpServletRequest request);
}

具体实现类,核心的业务逻辑都写在注释中了

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复制代码@Service
public class TokenServiceImpl implements TokenService {

@Autowired
private RedisTemplate redisTemplate;

@Override
public Response createToken() {
//生成uuid当作token
String token = UUID.randomUUID().toString().replaceAll("-","");
//将生成的token存入redis中
redisTemplate.opsForValue().set(token,token);
//返回正确的结果信息
Response response=new Response(0,token.toString(),null);
return response;
}

@Override
public Response checkToken(HttpServletRequest request) {
//从请求头中获取token
String token=request.getHeader("token");
if (StringUtils.isBlank(token)){
//如果请求头token为空就从参数中获取
token=request.getParameter("token");
//如果都为空抛出参数异常的错误
if (StringUtils.isBlank(token)){
throw new ServiceException(ResponseCode.ILLEGAL_ARGUMENT.getCode().toString(),ResponseCode.ILLEGAL_ARGUMENT.getMsg());
}
}
//如果redis中不包含该token,说明token已经被删除了,抛出请求重复异常
if (!redisTemplate.hasKey(token)){
throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getCode().toString(),ResponseCode.REPETITIVE_OPERATION.getMsg());
}
//删除token
Boolean del=redisTemplate.delete(token);
//如果删除不成功(已经被其他请求删除),抛出请求重复异常
if (!del){
throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getCode().toString(),ResponseCode.REPETITIVE_OPERATION.getMsg());
}
return new Response(0,"校验成功",null);
}
}

3.4 配置自定义注解

这是比较重要的一步,通过自定义注解在需要实现接口幂等性的方法上添加此注解,实现token验证

1
2
3
4
java复制代码@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
}

接口拦截器

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
java复制代码public class ApiIdempotentInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod= (HandlerMethod) handler;
Method method=handlerMethod.getMethod();
ApiIdempotent methodAnnotation=method.getAnnotation(ApiIdempotent.class);
if (methodAnnotation != null){
// 校验通过放行,校验不通过全局异常捕获后输出返回结果
tokenService.checkToken(request);
}
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}

3.5 配置拦截器以及redis

配置webConfig,添加拦截器

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(apiIdempotentInterceptor());
}

@Bean
public ApiIdempotentInterceptor apiIdempotentInterceptor() {
return new ApiIdempotentInterceptor();
}
}

配置redis,使得中文可以正常传输

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
java复制代码@Configuration
public class RedisConfig {
//自定义的redistemplate
@Bean(name = "redisTemplate")
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
//创建一个RedisTemplate对象,为了方便返回key为string,value为Object
RedisTemplate<String,Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
//设置json序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer=new
Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper=new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance);
//string的序列化
StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();
//key采用string的序列化方式
template.setKeySerializer(stringRedisSerializer);
//value采用jackson的序列化方式
template.setValueSerializer(jackson2JsonRedisSerializer);
//hashkey采用string的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
//hashvalue采用jackson的序列化方式
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}

最后是controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@RestController
@RequestMapping("/token")
public class TokenController {
@Autowired
private TokenService tokenService;

@GetMapping
public Response token(){
return tokenService.createToken();
}

@PostMapping("checktoken")
public Response checktoken(HttpServletRequest request){
return tokenService.checkToken(request);
}
}

其余代码在文末github链接上自取

(四)结果验证

首先通过token接口创建一个token出来,此时redis中也存在了改token

在这里插入图片描述

在jmeter中同时运行50个请求,我们可以观察到,只有第一个请求校验成功,后续的请求均提示请勿重复操作。

在这里插入图片描述

在这里插入图片描述

jmeter压测文件(Token Plan.jmx)和代码自取:github自取

本文转载自: 掘金

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

Java八大基本数据类型总结

发表于 2020-11-13

1、介绍

Java语言提供了8种基本数据类型。分别是 byte、short、int、long、float、double、boolean、char。  **注**:String 是对象,不属于基本数据类型

8种基本数据按类型分可以分为

  • 4个 整数型:byte、short、int、long
  • 2个浮点型:float、double
  • 1个字符类型:char
  • 1个布尔型:boolean

**注意:** (1) 基本数据类型 "==" 比较都是值。


           (2)Boolean 《Java虚拟机规范》给出了4个字节,但还要看虚拟机实现是否按照规 范来,所以1个字节、4个字节都是有可能的。

2、拆箱和装箱问题

**1)拆箱和装箱概念**

装箱就是自动将基本数据类型转换为包装器类型;使用Integer.valueOf方法。

 拆箱就是自动将包装器类型转换为基本数据类型;使用`Integer.intValue`方法。




举个例子:
1
2
3
4
5
ini复制代码Integer total = 99; 
//执行上面那句代码的时候,系统为我们执行了,即自动装箱
Integer total = Integer.valueOf(99);int totalprim = total;
//执行上面那句代码的时候,系统为我们执行了,即自动拆箱
int totalprim = total.intValue();
**2)范围问题**


       如下题目     
1
2
3
4
5
6
ini复制代码Integer i = 400; 
Integer j = 400;
System.out.println(i==j); //false
Integer o = 12;
Integer k = 12;
System.out.println(o==12); //true
上面提到,使用 `Integer`去创建数据,其实是一个`Integer.valueOf` 过程,`Integer.valueOf` 源码如下:   
1
2
3
4
5
6
arduino复制代码public static Integer valueOf(int i) {    
if (i >= IntegerCache.low && i <= IntegerCache.high) {
return IntegerCache.cache[i + (-IntegerCache.low);
}
return new Integer(i);
}

分析:如果值的范围在-128到127之间,它就从高速缓存返回实例。否则 new 一个Integer对象。new Integer 就是一个装箱的过程了,装箱的过程会创建对应的对象,这个会消耗内存,所以装箱的过程会增加内存的消耗,影响性能。

所以说最后是i 和 j 两个对象比较,内存地址不一样,结果就是false了。

**3)==的值比较问题**


 例子如下:
1
2
3
4
5
6
ini复制代码int a =200;
Integer b = new Integer(200);
Integer c = 200;
System.out.println(a==b); //true
System.out.println(a==c); //true
System.out.println(b==c); //false

分析: a==b,a==c,只要和基本数据类型(即 int)比较,Integer就会调用value.intValue()拆箱成基本数据类型,你也可以理解为:当有基本数据类型,只比较值b==c,这两个是永远不会相等的,拆箱装箱只是针对基本数据类型的比较才有,Integer并不是基本数据类型,b、c两者存放的内存地址不一样,所以不相等。

总结:

①、无论如何,Integer与new Integer不会相等。不会经历拆箱过程,因为它们存放内存的位置不一样。

②、两个都是非new出来的Integer,如果数在-128到127之间,则是true,否则为false。

③、两个都是new出来的,即两个new Integer比较,则为false。

④、int与Integer、new Integer()进行==比较时,结果永远为true,因为会把Integer自动拆箱为int,其实就是相当于两个int类型比较。

3、int 和 Integer

  • Integer 继承了Object类,是对象类型,有自己的属性和方法,是 int 的包装类。int是java基本数据类型。
  • Integer默认值null,int默认值 0。
  • int 可以直接做运算,Integer 不能直接运算,拆箱转化为int才能进行运算。

4、默认值问题

在java中:
  • 整数的默认类型是 int。
  • 浮点数默认类型是 double,否则需要 在后面加f、d
浮点数如果不加`f`,默认就是double类型的,前面再用 float修饰,就会报错。可以使用两种方法解决:  1)末尾加f   2)使用float进行强转

5、Integer.parseInt()和Integer.valueOf()的区别

parseInt() 和 valueOf() 都是Integer 对象的方法。入参都是一个String字符串。

parseInt

public static int parseInt(String s) throws NumberFormatException 将字符串参数作为带符号十进制整数来转换。如果无法转换,抛出 NumberFormatException。

valueOf

public static Integer valueOf(String s) throws NumberFormatException 返回初始化为指定String值的新的Integer对象,如果无法转换,抛出 NumberFormatException。
1
2
3
4
5
6
ini复制代码String str = "-12";
int num = Integer.parseInt(str);
System.out.println(num); // -12
Integer num2 = Integer.valueOf(str);
System.out.println(num2); // -12
int num3 =Integer.parseInt("HaC"); //java.lang.NumberFormatException

int 与 Integer转换

int a=A.intValue();

Integer A=Integer.valueOf(a);

注意: Integer派别:Integer、Short、Byte、Character、Long这几个类的valueOf方法的实现是类似的。

**Double派别**:Double、Float的valueOf方法的实现是类似的。**每次都返回不同的对象。**

6、精度丢失问题

1
2
3
4
5
6
7
8
ini复制代码byte a = 5;
int b =2;
float c = 6f;
double d =0.03;
double d2 =300.03;
System.out.println(a/b); // 2 a会转化为int类型
System.out.println(b/c); // 0.33333334 b会转化为float类型
System.out.println(a+d+d2);//301.05999999999995 a会转化为double类型,但是结果理应是301.06

不同类型的数据在运算的时候,会向高精度的数据类型转换。

其实double类型数值的计算经常会出现这种精度丢失的问题,尤其是有小数点的情况下,常常会因为精度丢失而导致程序出错。因为计算机是通过二进制进行运算的,而计算机在表示小数的二进制是会有精度问题的。

所以我们在运算高精度的数据的时候,可以使用 java.math.BigDecimal 类

7、字符串与整数拼接问题

例如:

1
2
3
4
5
ini复制代码String a = "1";
int b = 1;
int c = 2;
System.out.println(a + b + c); //112
System.out.println(b + c + a); //31

解析:a + b + c 从左到右按顺序运算,a是String, a+b 的结果也是String的拼接是字符串11 ,然后再拼接 c,c也被强转String了,最后是 String,其值112

b + c + a 从左到右按顺序运算,b + c 的结果是 3,是一个int ,然后拼接a,变成 String。

8、留几个题目分析

1) 设有下面两个赋值语句

1
2
ini复制代码a = Integer.parseInt("1024");
b = Integer.valueOf("1024").intValue();

下述说法正确的是()

A a是整数类型变量,b是整数类对象。

B a是整数类对象,b是整数类型变量。

C a和b都是整数类对象并且它们的值相等。

D a和b都是整数类型变量并且它们的值相等。

答案是 D,intValue()是把Integer对象类型变成int的基础数据类型;

2)表达式(short)10/10.2*2运算后结果是什么类型?

A short

B int

C double

D float

答案是 C,Java中,你如果 没有在数字后面声明,浮点数默认为double。

要注意是(short)10/10.2*2,而不是(short) (10/10.2 *2),前者只是把10强转为short,又由于式子中存在浮点数,所以会对结果值进行一个自动类型的提升,浮点数默认为double,所以答案是double;后者是把计算完之后值强转short。

本文转载自: 掘金

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

操作系统 中断 & 系统调用浅析

发表于 2020-11-12

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文 GitHub · Android-NoteBook 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)

前言

  • 在分析 Android 源码的过程中,往往会经历 app -> framework -> native -> kernel 的过程,最终就来到了用户程序与内核层序的边界,即:系统调用(System Call);
  • 清晰地理解系统调用的相关概念,对于后续深刻理解其他重点知识大有裨益。在这篇文章里,我将简单分析 中断 &系统调用 的相关概念,如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

  1. 中断机制

1.1 什么是中断?

  • 定义

中断(interrupt)是计算机系统中的基本机制之一。即:在计算机运行过程中,当发生某个事件后,CPU 会停止当前程序流,转而去处理该事件,并在处理完毕后继续执行原程序流。

相关概念 描述
中断分类 硬中断(Hardware Interrupt)& 软中断(Software Interrupt)
中断向量表(Interrupt Vector Table) 记录了中断号与中断服务程序内存地址的映射关系
中断服务程序 / 中断处理程序(Interrupt Service) 通过中断向量表定位到的特定处理程序

1.2 为什么引入中断?

中断机制的好处是 化主动为被动,避免 CPU 轮询等待某条件成立。如果没有中断机制,那么“某个条件成立”就需要 CPU 轮询判断,这样就会增加系统的开销。而使用中断机制,就可以在条件成立之后,向 CPU 发送中断事件,强制中断 CPU 执行程序,转而去执行中断处理程序。

1.3 硬中断

硬中断由外部设备(例如:磁盘,网卡,键盘,时钟)产生,用来通知操作系统外设状态变化。

时钟中断: 一种硬中断,用于定期打断 CPU 执行的线程,以便切换给其他线程以得到执行机会。

硬中断的处理流程如下:

  • 1、外设 将中断请求发送给中断控制器;
  • 2、中断控制器 根据中断优先级,有序地将中断传递给 CPU;
  • 3、CPU 终止执行当前程序流,将 CPU 所有寄存器的数值保存到栈中;
  • 4、CPU 根据中断向量,从中断向量表中查找中断处理程序的入口地址,执行中断处理程序;
  • 5、CPU 恢复寄存器中的数值,返回原程序流停止位置继续执行。

1.4 软中断

软中断是一条 CPU 指令,由当前正在运行的进程产生。

软中断模拟了硬中断的处理过程:

  • 1、无
  • 2、无
  • 3、CPU 终止执行当前程序流,将 CPU 所有寄存器的数值保存到栈中;
  • 4、CPU 根据中断向量,从中断向量表中查找中断处理程序的入口地址,执行中断处理程序;
  • 5、CPU 恢复寄存器中的数值,返回原程序流停止位置继续执行。

系统调用: 是一种软中断处理程序,用于让程序从用户态陷入内核态,以执行相应的操作。


  1. 系统调用

2.1 操作系统与应用的边界

  • 内核空间

操作系统(Operating System)是管理计算机硬件与软件资源的程序,操作系统内核驻留在受保护的内核空间。

  • 用户空间

应用是运行在操作系统上运行的程序,工作在用户空间。

  • 隔离

处于安全性和稳定性考虑,用户空间程序无法直接执行内核代码(例如:I/O 读写、创建新进程/线程),也无法访问内核数据,必须通过系统调用。

2.2 系统调用的定义

系统调用(Syscall) 是一种软中断处理程序,用于让程序从用户态陷入内核态,以执行相应的操作。

2.3 系统调用的作用

当发生系统调用时,会让程序从用户态陷入内核态,以执行相应的操作。

2.4 系统调用中断处理程序的流程

  • 1、程序从用户态陷入内核态
  • 2、根据系统调用号,在系统调用表中查找对应的系统调用函数的内存地址,执行系统调用函数。
  • 3、程序从内核态返回用户态

参考资料

  • 《程序是怎样跑起来的》(第 9、11 章)—— [日]矢泽久雄 著
  • 《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理(第2版)》(第 1 章)—— 新设计团队 著
  • 《Linux内核分析及应用》(第1.5 节、第 4 章)—— 陈科 著
  • 《Linux 系统调用(syscall)原理》 —— Gityuan 著
  • 《Linux内核分析-系统调用用户态到内核态流程(四、五)》 —— Linux 分享官 著
  • 《程序员的自我修养·链接、装载与库》(第 12 章)—— 俞甲子 石凡 潘爱民 著

推荐阅读

  • 密码学 | Base64是加密算法吗?
  • 算法面试题 | 回溯算法解题框架
  • 算法面试题 | 链表问题总结
  • 计算机网络 | 图解 DNS & HTTPDNS 原理
  • Android | 说说从 android:text 到 TextView 的过程
  • Android | 面试必问的 Handler,你确定不看看?
  • Android | 带你探究 LayoutInflater 布局解析原理
  • Android | View & Fragment & Window 的 getContext() 一定返回 Activity 吗?

感谢喜欢!你的点赞是对我最大的鼓励!欢迎关注彭旭锐的GitHub!

本文转载自: 掘金

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

使用nginx和vsftp搭建图片服务器并使用Java上传图

发表于 2020-11-12

安装vsftp

1、首先,安装vsftpd

1
shell复制代码yum -y install vsftpd

2、验证是否安装成功

1
shell复制代码rpm -qa vsftpd

3、查看vsftp相关配置文件

1
shell复制代码ll /etc/vsftpd/

vsftpd.conf文件是主要的配置文件,一些关键的配置都在其中

ftpusers文件是禁止使用vsftpd的用户列表文件,记录不允许访问FTP服务器的用户名单

user_list这个文件禁止或允许使用vsftpd的用户列表文件,这个文件中指定的用户缺省情况(即在vsftpd.conf中设置userlist_deny=YES)下也不能访问FTP服务器,在设置了userlist_deny=NO时,仅允许user_list中指定的用户访问FTP服务器

4、先备份一份vsftpd.conf文件

1
shell复制代码 cp /etc/vsftpd/vsftpd.conf /etc/vsftpd/vsftpd-default.conf

5、修改vsftpd.conf配置文件

1
shell复制代码vim /etc/vsftpd/vsftpd.conf

image-20201112191932974

把anonymous_enable=YES改为NO,表示不允许匿名访问

image-20201112192347055

然后把listen=NO改为YES,listen_ipv6=YES改为NO

再在该文件的最后添加以下数据:

1
2
3
4
5
shell复制代码#仅允许user_list文件中的用户访问FTP服务
userlist_deny=NO
#被动模式端口范围
pasv_min_port=30000
pasv_max_port=30999

6、创建一个用来登录FTP服务的用户

1
2
3
shell复制代码useradd ftpuser
passwd ftpuser
#输入两次密码

7、将ftpuser用户加进user_list文件最后一行

1
shell复制代码vim /etc/vsftpd/user_list

8、启动FTP服务

1
shell复制代码systemctl start vsftpd

9、服务器开放21端口和30000/30999端口范围

10、浏览器访问测试是否成功:

ftp://服务器ip地址/

Nginx进行配置

打开nginx配置文件

1
shell复制代码vim /usr/local/nginx/conf/nginx.conf

修改以下内容

image-20201112200536867

重启nginx:

1
2
3
shell复制代码cd /usr/local/nginx/sbin/

./nginx -s reload

服务器开放端口:9999

Java实现上传图片的功能

1、添加依赖:

1
2
3
4
5
xml复制代码<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.6</version>
</dependency>

2、application.properties配置文件添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
properties复制代码#配置文件上传器
spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=100MB
#ftp相关配置
#服务器地址
FTP.ADDRESS=192.168.xx.xxx
#FTP服务端口,默认是21
FTP.PORT=21
#访问FTP服务的用户名
FTP.USERNAME=ftpuser
#访问FTP服务的用户名对应的密码
FTP.PASSWORD=ftpuser
#图片存放在服务器指定的文件夹
FTP.BASEPATH=/home/ftpuser/images
#访问图片的基本url,如果端口号为80就不用加端口,192,168.xx.xxx为服务器ip地址
IMAGE.BASE.URL=http://192.168.xx.xxx:9999/images

3、FtpUtils:用于上传文件的工具类

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

/**
* Description: 向FTP服务器上传文件
* @param host FTP服务器ip
* @param port FTP服务器端口
* @param username FTP登录账号
* @param password FTP登录密码
* @param basePath FTP服务器基础目录,/home/ftpuser/images
* @param filename 上传到FTP服务器上的文件名
* @param input 输入流
* @return 成功返回true,否则返回false
*/
public static boolean uploadFile(String host, int port, String username, String password, String basePath,
String filename, InputStream input) {
boolean result = false;
FTPClient ftp = new FTPClient();
try {
int reply;
ftp.connect(host, port);// 连接FTP服务器
// 如果采用默认端口,可以使用ftp.connect(host)的方式直接连接FTP服务器
ftp.login(username, password);// 登录
reply = ftp.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
ftp.disconnect();
return result;
}

//设置为被动模式
ftp.enterLocalPassiveMode();
//设置编码格式为utf-8
ftp.setControlEncoding("UTF-8");
//设置上传文件的类型为二进制类型
ftp.setFileType(FTP.BINARY_FILE_TYPE);
//设置存储图片的文件夹
ftp.changeWorkingDirectory(basePath);
//上传文件
if (!ftp.storeFile(filename, input)) {
return result;
}
input.close();
ftp.logout();
result = true;
} catch (IOException e) {
e.printStackTrace();
} finally {
if (ftp.isConnected()) {
try {
ftp.disconnect();
} catch (IOException ioe) {
}
}
}
return result;
}
}

4、用于生成随机图片名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class IDUtils {

/**
* 生成随机图片名
*/
public static String genImageName() {
//取当前时间的长整形值包含毫秒
long millis = System.currentTimeMillis();
//long millis = System.nanoTime();
//加上三位随机数
Random random = new Random();
int end3 = random.nextInt(999);
//如果不足三位前面补0
String str = millis + String.format("%03d", end3);

return str;
}
}

5、上传图片的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
java复制代码/**
* 上传图片
*/
@Controller
@RequestMapping("/admin")
public class UploadController {

@Value("${FTP.ADDRESS}")
private String host;
// 端口
@Value("${FTP.PORT}")
private int port;
// ftp用户名
@Value("${FTP.USERNAME}")
private String userName;
// ftp用户密码
@Value("${FTP.PASSWORD}")
private String passWord;
// 文件在服务器端保存的主目录
@Value("${FTP.BASEPATH}")
private String basePath;
// 访问图片时的基础url
@Value("${IMAGE.BASE.URL}")
private String baseUrl;

/**
* 上传图片到服务器
* @param uploadFile
* @return
*/
@PostMapping("/upload/file")
public String pictureUpload(@RequestParam("file") MultipartFile file) {
try {
//1、给上传的图片生成新的文件名
//1.1获取原始文件名
String oldName = file.getOriginalFilename();
//1.2使用IDUtils工具类生成新的文件名,新文件名 = newName + 文件后缀
String newName = IDUtils.genImageName();
newName = newName + oldName.substring(oldName.lastIndexOf("."));

//2、把图片上传到图片服务器
//2.1获取上传的io流
InputStream input = file.getInputStream();
//2.2调用FtpUtil工具类进行上传
boolean result = FtpUtil.uploadFile(host, port, userName, passWord, basePath, newName, input);
if(result) {
//返回给前端图片访问路径
return baseUrl+"/"+newName;
}else {
return "false";
}
} catch (IOException e) {
e.printStackTrace();
return "false";
}
}
}

6、测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
html复制代码<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>

<form action="/admin/upload/file" enctype="multipart/form-data" method="post">
<input type="text" name="username"><br>
<input type="password" name="password"><br>
<input type="file" name="file"><br>
<input type="submit" value="提交">
</form>

</body>
</html>

7、提交图片后后端会返回一个图片路径,复制到地址栏看看是否能访问

到此,使用java上传图片就完成了。

本文转载自: 掘金

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

try catch 有多烦人,我就有多暴躁!一次搞定 Exc

发表于 2020-11-12

背景

软件开发过程中,不可避免的是需要处理各种异常,就我自己来说,至少有一半以上的时间都是在处理各种异常情况,所以代码中就会出现大量的try {...} catch {...} finally {...} 代码块,不仅有大量的冗余代码,而且还影响代码的可读性。比较下面两张图,看看您现在编写的代码属于哪一种风格?然后哪种编码风格您更喜欢?

丑陋的 try catch 代码块

优雅的Controller

上面的示例,还只是在Controller层,如果是在Service层,可能会有更多的try catch代码块。这将会严重影响代码的可读性、“美观性”。

所以如果是我的话,我肯定偏向于第二种,我可以把更多的精力放在业务代码的开发,同时代码也会变得更加简洁。

既然业务代码不显式地对异常进行捕获、处理,而异常肯定还是处理的,不然系统岂不是动不动就崩溃了,所以必须得有其他地方捕获并处理这些异常。

那么问题来了,如何优雅的处理各种异常?

什么是统一异常处理

Spring在3.2版本增加了一个注解@ControllerAdvice,可以与@ExceptionHandler、@InitBinder、@ModelAttribute 等注解注解配套使用,对于这几个注解的作用,这里不做过多赘述,若有不了解的,可以参考Spring3.2新注解@ControllerAdvice,先大概有个了解。

不过跟异常处理相关的只有注解@ExceptionHandler,从字面上看,就是 异常处理器 的意思,其实际作用也是:若在某个Controller类定义一个异常处理方法,并在方法上添加该注解,那么当出现指定的异常时,会执行该处理异常的方法,其可以使用springmvc提供的数据绑定,比如注入HttpServletRequest等,还可以接受一个当前抛出的Throwable对象。

但是,这样一来,就必须在每一个Controller类都定义一套这样的异常处理方法,因为异常可以是各种各样。这样一来,就会造成大量的冗余代码,而且若需要新增一种异常的处理逻辑,就必须修改所有Controller类了,很不优雅。

当然你可能会说,那就定义个类似BaseController的基类,这样总行了吧。

这种做法虽然没错,但仍不尽善尽美,因为这样的代码有一定的侵入性和耦合性。简简单单的Controller,我为啥非得继承这样一个类呢,万一已经继承其他基类了呢。大家都知道Java只能继承一个类。

那有没有一种方案,既不需要跟Controller耦合,也可以将定义的 异常处理器 应用到所有控制器呢?所以注解@ControllerAdvice出现了,简单的说,该注解可以把异常处理器应用到所有控制器,而不是单个控制器。借助该注解,我们可以实现:在独立的某个地方,比如单独一个类,定义一套对各种异常的处理机制,然后在类的签名加上注解@ControllerAdvice,统一对 不同阶段的、不同异常 进行处理。这就是统一异常处理的原理。

注意到上面对异常按阶段进行分类,大体可以分成:进入Controller前的异常 和Service 层异常,具体可以参考下图:

不同阶段的异常

目标

消灭95%以上的 try catch 代码块,以优雅的 Assert(断言) 方式来校验业务的异常情况,只关注业务逻辑,而不用花费大量精力写冗余的 try catch 代码块。

统一异常处理实战

在定义统一异常处理类之前,先来介绍一下如何优雅的判定异常情况并抛异常。

用 Assert(断言) 替换 throw exception

想必 Assert(断言) 大家都很熟悉,比如 Spring 家族的 org.springframework.util.Assert,在我们写测试用例的时候经常会用到,使用断言能让我们编码的时候有一种非一般丝滑的感觉,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码    @Test    
public void test1() {
...
User user = userDao.selectById(userId);
Assert.notNull(user, "用户不存在.");
...
}

@Test
public void test2() {
// 另一种写法
User user = userDao.selectById(userId);
if (user == null) {
throw new IllegalArgumentException("用户不存在.");
}
}

有没有感觉第一种判定非空的写法很优雅,第二种写法则是相对丑陋的 if {...} 代码块。那么 神奇的 Assert.notNull() 背后到底做了什么呢?下面是 Assert 的部分源码:

1
2
3
4
5
6
7
8
9
typescript复制代码public abstract class Assert {
public Assert() {
}

public static void notNull(@Nullable Object object, String message) {
if (object == null) {
throw new IllegalArgumentException(message);
}
}}

可以看到,Assert 其实就是帮我们把 if {...} 封装了一下,是不是很神奇。虽然很简单,但不可否认的是编码体验至少提升了一个档次。那么我们能不能模仿org.springframework.util.Assert,也写一个断言类,不过断言失败后抛出的异常不是IllegalArgumentException 这些内置异常,而是我们自己定义的异常。下面让我们来尝试一下。

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
typescript复制代码Assertpublic interface Assert {
/**
* 创建异常
* @param args
* @return
*/
BaseException newException(Object... args);
/**
* 创建异常
* @param t
* @param args
* @return
*/
BaseException newException(Throwable t, Object... args);
/**
* <p>断言对象<code>obj</code>非空。如果对象<code>obj</code>为空,则抛出异常
*
* @param obj 待判断对象
*/
default void assertNotNull(Object obj) {
if (obj == null) {
throw newException(obj);
}
}
/**
* <p>断言对象<code>obj</code>非空。如果对象<code>obj</code>为空,则抛出异常
* <p>异常信息<code>message</code>支持传递参数方式,避免在判断之前进行字符串拼接操作
*
* @param obj 待判断对象
* @param args message占位符对应的参数列表
*/
default void assertNotNull(Object obj, Object... args) {
if (obj == null) {
throw newException(args);
}
}}

上面的Assert断言方法是使用接口的默认方法定义的,然后有没有发现当断言失败后,抛出的异常不是具体的某个异常,而是交由2个newException接口方法提供。因为业务逻辑中出现的异常基本都是对应特定的场景,比如根据用户id获取用户信息,查询结果为null,此时抛出的异常可能为UserNotFoundException,并且有特定的异常码(比如7001)和异常信息“用户不存在”。所以具体抛出什么异常,有Assert的实现类决定。

看到这里,您可能会有这样的疑问,按照上面的说法,那岂不是有多少异常情况,就得有定义等量的断言类和异常类,这显然是反人类的,这也没想象中高明嘛。别急,且听我细细道来。

善解人意的Enum

自定义异常BaseException有2个属性,即code、message,这样一对属性,有没有想到什么类一般也会定义这2个属性?没错,就是枚举类。且看我如何将 Enum 和 Assert 结合起来,相信我一定会让你眼前一亮。如下:

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
typescript复制代码public interface IResponseEnum {
int getCode();
String getMessage();}
/**
* <p>业务异常</p>
* <p>业务处理时,出现异常,可以抛出该异常</p>
*/
public class BusinessException extends BaseException {
private static final long serialVersionUID = 1L;
public BusinessException(IResponseEnum responseEnum, Object[] args, String message) {
super(responseEnum, args, message);
}
public BusinessException(IResponseEnum responseEnum, Object[] args, String message, Throwable cause) {
super(responseEnum, args, message, cause);
}}public interface BusinessExceptionAssert extends IResponseEnum, Assert {
@Override
default BaseException newException(Object... args) {
String msg = MessageFormat.format(this.getMessage(), args);
return new BusinessException(this, args, msg);
}
@Override
default BaseException newException(Throwable t, Object... args) {
String msg = MessageFormat.format(this.getMessage(), args);
return new BusinessException(this, args, msg, t);
}}@Getter
@AllArgsConstructor
public enum ResponseEnum implements BusinessExceptionAssert {
/**
* Bad licence type
*/
BAD_LICENCE_TYPE(7001, "Bad licence type."),
/**
* Licence not found
*/
LICENCE_NOT_FOUND(7002, "Licence not found.")
;
/**
* 返回码
*/
private int code;
/**
* 返回消息
*/
private String message;
}

看到这里,有没有眼前一亮的感觉,代码示例中定义了两个枚举实例:BAD_LICENCE_TYPE、LICENCE_NOT_FOUND,分别对应了BadLicenceTypeException、LicenceNotFoundException两种异常。以后每增加一种异常情况,只需增加一个枚举实例即可,再也不用每一种异常都定义一个异常类了。然后再来看下如何使用,假设LicenceService有校验Licence是否存在的方法,如下:

1
2
3
4
5
6
7
typescript复制代码/**
* 校验{@link Licence}存在
* @param licence
*/
private void checkNotNull(Licence licence) {
ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence);
}

若不使用断言,代码可能如下:

1
2
3
4
5
6
7
typescript复制代码private void checkNotNull(Licence licence) {
if (licence == null) {
throw new LicenceNotFoundException();
// 或者这样
throw new BusinessException(7001, "Bad licence type.");
}
}

使用枚举类结合(继承)Assert,只需根据特定的异常情况定义不同的枚举实例,如上面的BAD_LICENCE_TYPE、LICENCE_NOT_FOUND,就能够针对不同情况抛出特定的异常(这里指携带特定的异常码和异常消息),这样既不用定义大量的异常类,同时还具备了断言的良好可读性,当然这种方案的好处远不止这些,请继续阅读后文,慢慢体会。

注:上面举的例子是针对特定的业务,而有部分异常情况是通用的,比如:服务器繁忙、网络异常、服务器异常、参数校验异常、404等,所以有CommonResponseEnum、ArgumentResponseEnum、ServletResponseEnum,其中 ServletResponseEnum 会在后文详细说明。

定义统一异常处理器类

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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
scss复制代码@Slf4j
@Component
@ControllerAdvice
@ConditionalOnWebApplication
@ConditionalOnMissingBean(UnifiedExceptionHandler.class)
public class UnifiedExceptionHandler {
/**
* 生产环境
*/
private final static String ENV_PROD = "prod";
@Autowired
private UnifiedMessageSource unifiedMessageSource;
/**
* 当前环境
*/
@Value("${spring.profiles.active}")
private String profile;
/**
* 获取国际化消息
*
* @param e 异常
* @return
*/
public String getMessage(BaseException e) {
String code = "response." + e.getResponseEnum().toString();
String message = unifiedMessageSource.getMessage(code, e.getArgs());
if (message == null || message.isEmpty()) {
return e.getMessage();
}
return message;
}
/**
* 业务异常
*
* @param e 异常
* @return 异常结果
*/
@ExceptionHandler(value = BusinessException.class)
@ResponseBody
public ErrorResponse handleBusinessException(BaseException e) {
log.error(e.getMessage(), e);
return new ErrorResponse(e.getResponseEnum().getCode(), getMessage(e));
}
/**
* 自定义异常
*
* @param e 异常
* @return 异常结果
*/
@ExceptionHandler(value = BaseException.class)
@ResponseBody
public ErrorResponse handleBaseException(BaseException e) {
log.error(e.getMessage(), e);
return new ErrorResponse(e.getResponseEnum().getCode(), getMessage(e));
}
/**
* Controller上一层相关异常
*
* @param e 异常
* @return 异常结果
*/
@ExceptionHandler({
NoHandlerFoundException.class,
HttpRequestMethodNotSupportedException.class,
HttpMediaTypeNotSupportedException.class,
MissingPathVariableException.class,
MissingServletRequestParameterException.class,
TypeMismatchException.class,
HttpMessageNotReadableException.class,
HttpMessageNotWritableException.class,
// BindException.class,
// MethodArgumentNotValidException.class
HttpMediaTypeNotAcceptableException.class,
ServletRequestBindingException.class,
ConversionNotSupportedException.class,
MissingServletRequestPartException.class,
AsyncRequestTimeoutException.class
})
@ResponseBody
public ErrorResponse handleServletException(Exception e) {
log.error(e.getMessage(), e);
int code = CommonResponseEnum.SERVER_ERROR.getCode();
try {
ServletResponseEnum servletExceptionEnum = ServletResponseEnum.valueOf(e.getClass().getSimpleName());
code = servletExceptionEnum.getCode();
} catch (IllegalArgumentException e1) {
log.error("class [{}] not defined in enum {}", e.getClass().getName(), ServletResponseEnum.class.getName());
}
if (ENV_PROD.equals(profile)) {
// 当为生产环境, 不适合把具体的异常信息展示给用户, 比如404.
code = CommonResponseEnum.SERVER_ERROR.getCode();
BaseException baseException = new BaseException(CommonResponseEnum.SERVER_ERROR);
String message = getMessage(baseException);
return new ErrorResponse(code, message);
}
return new ErrorResponse(code, e.getMessage());
}
/**
* 参数绑定异常
*
* @param e 异常
* @return 异常结果
*/
@ExceptionHandler(value = BindException.class)
@ResponseBody
public ErrorResponse handleBindException(BindException e) {
log.error("参数绑定校验异常", e);
return wrapperBindingResult(e.getBindingResult());
}
/**
* 参数校验异常,将校验失败的所有异常组合成一条错误信息
*
* @param e 异常
* @return 异常结果
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
@ResponseBody
public ErrorResponse handleValidException(MethodArgumentNotValidException e) {
log.error("参数绑定校验异常", e);
return wrapperBindingResult(e.getBindingResult());
}
/**
* 包装绑定异常结果
*
* @param bindingResult 绑定结果
* @return 异常结果
*/
private ErrorResponse wrapperBindingResult(BindingResult bindingResult) {
StringBuilder msg = new StringBuilder();
for (ObjectError error : bindingResult.getAllErrors()) {
msg.append(", ");
if (error instanceof FieldError) {
msg.append(((FieldError) error).getField()).append(": ");
}
msg.append(error.getDefaultMessage() == null ? "" : error.getDefaultMessage());
}
return new ErrorResponse(ArgumentResponseEnum.VALID_ERROR.getCode(), msg.substring(2));
}
/**
* 未定义异常
*
* @param e 异常
* @return 异常结果
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
public ErrorResponse handleException(Exception e) {
log.error(e.getMessage(), e);
if (ENV_PROD.equals(profile)) {
// 当为生产环境, 不适合把具体的异常信息展示给用户, 比如数据库异常信息.
int code = CommonResponseEnum.SERVER_ERROR.getCode();
BaseException baseException = new BaseException(CommonResponseEnum.SERVER_ERROR);
String message = getMessage(baseException);
return new ErrorResponse(code, message);
}
return new ErrorResponse(CommonResponseEnum.SERVER_ERROR.getCode(), e.getMessage());
}}

可以看到,上面将异常分成几类,实际上只有两大类,一类是ServletException、ServiceException,还记得上文提到的 按阶段分类 吗,即对应 进入Controller前的异常 和Service 层异常;然后 ServiceException 再分成自定义异常、未知异常。对应关系如下:

  • 进入Controller前的异常: handleServletException、handleBindException、handleValidException
  • 自定义异常: handleBusinessException、handleBaseException
  • 未知异常: handleException

接下来分别对这几种异常处理器做详细说明。

异常处理器说明

handleServletException

一个http请求,在到达Controller前,会对该请求的请求信息与目标控制器信息做一系列校验。这里简单说一下:

NoHandlerFoundException:首先根据请求Url查找有没有对应的控制器,若没有则会抛该异常,也就是大家非常熟悉的404异常;

HttpRequestMethodNotSupportedException:若匹配到了(匹配结果是一个列表,不同的是http方法不同,如:Get、Post等),则尝试将请求的http方法与列表的控制器做匹配,若没有对应http方法的控制器,则抛该异常;

HttpMediaTypeNotSupportedException:然后再对请求头与控制器支持的做比较,比如content-type请求头,若控制器的参数签名包含注解@RequestBody,但是请求的content-type请求头的值没有包含application/json,那么会抛该异常(当然,不止这种情况会抛这个异常);

MissingPathVariableException:未检测到路径参数。比如url为:/licence/{licenceId},参数签名包含@PathVariable("licenceId"),当请求的url为/licence,在没有明确定义url为/licence的情况下,会被判定为:缺少路径参数;

MissingServletRequestParameterException:缺少请求参数。比如定义了参数@RequestParam(“licenceId”) String licenceId,但发起请求时,未携带该参数,则会抛该异常;

TypeMismatchException: 参数类型匹配失败。比如:接收参数为Long型,但传入的值确是一个字符串,那么将会出现类型转换失败的情况,这时会抛该异常;

HttpMessageNotReadableException:与上面的HttpMediaTypeNotSupportedException举的例子完全相反,即请求头携带了"content-type: application/json;charset=UTF-8",但接收参数却没有添加注解@RequestBody,或者请求体携带的 json 串反序列化成 pojo 的过程中失败了,也会抛该异常;

HttpMessageNotWritableException:返回的 pojo 在序列化成 json 过程失败了,那么抛该异常;

handleBindException

参数校验异常,后文详细说明。

handleValidException

参数校验异常,后文详细说明。

handleBusinessException、handleBaseException

处理自定义的业务异常,只是handleBaseException处理的是除了 BusinessException意外的所有业务异常。就目前来看,这2个是可以合并成一个的。

handleException

处理所有未知的异常,比如操作数据库失败的异常。

注:上面的handleServletException、handleException 这两个处理器,返回的异常信息,不同环境返回的可能不一样,以为这些异常信息都是框架自带的异常信息,一般都是英文的,不太好直接展示给用户看,所以统一返回SERVER_ERROR代表的异常信息。

异于常人的404

上文提到,当请求没有匹配到控制器的情况下,会抛出NoHandlerFoundException异常,但其实默认情况下不是这样,默认情况下会出现类似如下页面:

Whitelabel Error Page

这个页面是如何出现的呢?实际上,当出现404的时候,默认是不抛异常的,而是forward跳转到/error控制器,spring也提供了默认的error控制器,如下:

那么,如何让404也抛出异常呢,只需在properties文件中加入如下配置即可:

1
ini复制代码spring.mvc.throw-exception-if-no-handler-found=truespring.resources.add-mappings=false

如此,就可以异常处理器中捕获它了,然后前端只要捕获到特定的状态码,立即跳转到404页面即可

捕获404对应的异常

统一返回结果

在验证统一异常处理器之前,顺便说一下统一返回结果。说白了,其实是统一一下返回结果的数据结构。code、message 是所有返回结果中必有的字段,而当需要返回数据时,则需要另一个字段 data 来表示。

所以首先定义一个 BaseResponse 来作为所有返回结果的基类;

然后定义一个通用返回结果类CommonResponse,继承 BaseResponse,而且多了字段 data;

为了区分成功和失败返回结果,于是再定义一个 ErrorResponse

最后还有一种常见的返回结果,即返回的数据带有分页信息,因为这种接口比较常见,所以有必要单独定义一个返回结果类 QueryDataResponse,该类继承自 CommonResponse,只是把 data 字段的类型限制为 QueryDdata,QueryDdata中定义了分页信息相应的字段,即totalCount、pageNo、 pageSize、records。

其中比较常用的只有 CommonResponse 和 QueryDataResponse,但是名字又贼鬼死长,何不定义2个名字超简单的类来替代呢?于是 R 和 QR 诞生了,以后返回结果的时候只需这样写:new R<>(data)、new QR<>(queryData)。

所有的返回结果类的定义这里就不贴出来了

验证统一异常处理

因为这一套统一异常处理可以说是通用的,所有可以设计成一个 common包,以后每一个新项目/模块只需引入该包即可。所以为了验证,需要新建一个项目,并引入该 common包。

主要代码

下面是用于验证的主要源码:

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
java复制代码@Service
public class LicenceService extends ServiceImpl<LicenceMapper, Licence> {
@Autowired
private OrganizationClient organizationClient;
/**
* 查询{@link Licence} 详情
* @param licenceId
* @return
*/
public LicenceDTO queryDetail(Long licenceId) {
Licence licence = this.getById(licenceId);
checkNotNull(licence);
OrganizationDTO org = ClientUtil.execute(() -> organizationClient.getOrganization(licence.getOrganizationId()));
return toLicenceDTO(licence, org); }
/**
* 分页获取
* @param licenceParam 分页查询参数
* @return
*/
public QueryData<SimpleLicenceDTO> getLicences(LicenceParam licenceParam) {
String licenceType = licenceParam.getLicenceType();
LicenceTypeEnum licenceTypeEnum = LicenceTypeEnum.parseOfNullable(licenceType);
// 断言, 非空
ResponseEnum.BAD_LICENCE_TYPE.assertNotNull(licenceTypeEnum);
LambdaQueryWrapper<Licence> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Licence::getLicenceType, licenceType);
IPage<Licence> page = this.page(new QueryPage<>(licenceParam), wrapper);
return new QueryData<>(page, this::toSimpleLicenceDTO);
}
/**
* 新增{@link Licence}
* @param request 请求体
* @return
*/
@Transactional(rollbackFor = Throwable.class)
public LicenceAddRespData addLicence(LicenceAddRequest request) {
Licence licence = new Licence();
licence.setOrganizationId(request.getOrganizationId());
licence.setLicenceType(request.getLicenceType());
licence.setProductName(request.getProductName());
licence.setLicenceMax(request.getLicenceMax());
licence.setLicenceAllocated(request.getLicenceAllocated());
licence.setComment(request.getComment());
this.save(licence);
return new LicenceAddRespData(licence.getLicenceId());
}
/**
* entity -> simple dto
* @param licence {@link Licence} entity
* @return {@link SimpleLicenceDTO}
*/
private SimpleLicenceDTO toSimpleLicenceDTO(Licence licence) {
// 省略
}
/**
* entity -> dto
* @param licence {@link Licence} entity
* @param org {@link OrganizationDTO}
* @return {@link LicenceDTO}
*/
private LicenceDTO toLicenceDTO(Licence licence, OrganizationDTO org) {
// 省略
}
/**
* 校验{@link Licence}存在
* @param licence
*/
private void checkNotNull(Licence licence) {
ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence);
}
}

PS: 这里使用的DAO框架是mybatis-plus。启动时,自动插入的数据为:

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码-- licenceINSERT INTO licence (licence_id, organization_id, licence_type, product_name, licence_max, licence_allocated)
VALUES (1, 1, 'user','CustomerPro', 100,5);
INSERT INTO licence (licence_id, organization_id, licence_type, product_name, licence_max, licence_allocated)
VALUES (2, 1, 'user','suitability-plus', 200,189);
INSERT INTO licence (licence_id, organization_id, licence_type, product_name, licence_max, licence_allocated)
VALUES (3, 2, 'user','HR-PowerSuite', 100,4);
INSERT INTO licence (licence_id, organization_id, licence_type, product_name, licence_max, licence_allocated)
VALUES (4, 2, 'core-prod','WildCat Application Gateway', 16,16);

-- organizationsINSERT INTO organization (id, name, contact_name, contact_email, contact_phone)
VALUES (1, 'customer-crm-co', 'Mark Balster', 'mark.balster@custcrmco.com', '823-555-1212');
INSERT INTO organization (id, name, contact_name, contact_email, contact_phone)
VALUES (2, 'HR-PowerSuite', 'Doug Drewry','doug.drewry@hr.com', '920-555-1212');
开始验证
捕获自定义异常

\1. 获取不存在的 licence 详情:http://localhost:10000/licence/5。成功响应的请求:licenceId=1

检验非空

捕获 Licence not found 异常

Licence not found

  1. 根据不存在的 licence type 获取 licence 列表:http://localhost:10000/licence/list?licenceType=ddd。可选的 licence type 为:user、core-prod 。

校验非空

捕获 Bad licence type 异常

Bad licence type

捕获进入 Controller 前的异常

\1. 访问不存在的接口:http://localhost:10000/licence/list/ddd

捕获404异常

\2. http 方法不支持:http://localhost:10000/licence

PostMapping

捕获 Request method not supported 异常

Request method not supported

\3. 校验异常1:http://localhost:10000/licence/list?licenceType=

getLicences

LicenceParam

捕获参数绑定校验异常

licence type cannot be empty

  1. 校验异常2:post 请求,这里使用postman模拟。

addLicence

LicenceAddRequest

请求url即结果

捕获参数绑定校验异常

注:因为参数绑定校验异常的异常信息的获取方式与其它异常不一样,所以才把这2种情况的异常从 进入 Controller 前的异常 单独拆出来,下面是异常信息的收集逻辑:

异常信息的收集

捕获未知异常

假设我们现在随便对 Licence 新增一个字段 test,但不修改数据库表结构,然后访问:http://localhost:10000/licence/1。

增加test字段

捕获数据库异常

Error querying database

小结

可以看到,测试的异常都能够被捕获,然后以 code、message 的形式返回。每一个项目/模块,在定义业务异常的时候,只需定义一个枚举类,然后实现接口 BusinessExceptionAssert,最后为每一种业务异常定义对应的枚举实例即可,而不用定义许多异常类。使用的时候也很方便,用法类似断言。

扩展

在生产环境,若捕获到 未知异常 或者 ServletException,因为都是一长串的异常信息,若直接展示给用户看,显得不够专业,于是,我们可以这样做:当检测到当前环境是生产环境,那么直接返回 “网络异常”。

生产环境返回“网络异常”

可以通过以下方式修改当前环境:

修改当前环境为生产环境

总结

使用 断言 和 枚举类 相结合的方式,再配合统一异常处理,基本大部分的异常都能够被捕获。为什么说大部分异常,因为当引入 spring cloud security 后,还会有认证/授权异常,网关的服务降级异常、跨模块调用异常、远程调用第三方服务异常等,这些异常的捕获方式与本文介绍的不太一样,不过限于篇幅,这里不做详细说明,以后会有单独的文章介绍。

另外,当需要考虑国际化的时候,捕获异常后的异常信息一般不能直接返回,需要转换成对应的语言,不过本文已考虑到了这个,获取消息的时候已经做了国际化映射,逻辑如下:

获取国际化消息

最后总结,全局异常属于老生长谈的话题,希望这次通过手机的项目对大家有点指导性的学习。大家根据实际情况自行修改。

也可以采用以下的jsonResult对象的方式进行处理,也贴出来代码.

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复制代码@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 没有登录
* @param request
* @param response
* @param e
* @return
*/
@ExceptionHandler(NoLoginException.class)
public Object noLoginExceptionHandler(HttpServletRequest request,HttpServletResponse response,Exception e)
{
log.error("[GlobalExceptionHandler][noLoginExceptionHandler] exception",e);
JsonResult jsonResult = new JsonResult();
jsonResult.setCode(JsonResultCode.NO_LOGIN);
jsonResult.setMessage("用户登录失效或者登录超时,请先登录");
return jsonResult;
}
/**
* 业务异常
* @param request
* @param response
* @param e
* @return
*/
@ExceptionHandler(ServiceException.class)
public Object businessExceptionHandler(HttpServletRequest request,HttpServletResponse response,Exception e)
{
log.error("[GlobalExceptionHandler][businessExceptionHandler] exception",e);
JsonResult jsonResult = new JsonResult();
jsonResult.setCode(JsonResultCode.FAILURE);
jsonResult.setMessage("业务异常,请联系管理员");
return jsonResult;
}
/**
* 全局异常处理
* @param request
* @param response
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
public Object exceptionHandler(HttpServletRequest request,HttpServletResponse response,Exception e)
{
log.error("[GlobalExceptionHandler][exceptionHandler] exception",e);
JsonResult jsonResult = new JsonResult();
jsonResult.setCode(JsonResultCode.FAILURE);
jsonResult.setMessage("系统错误,请联系管理员");
return jsonResult;
}
}

本文来源:cnblogs.com/jurendage/p/11255197.html

更多资讯请关注公众号:Java进阶之旅!

本文转载自: 掘金

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

Spring Boot中实现多数据源动态切换效果(2):通过

发表于 2020-11-12

目录:

  • Spring Boot中实现多数据源动态切换效果(1):通过继承AbstractRoutingDataSource类实现
  • Spring Boot中实现多数据源动态切换效果(2):通过开源项目Dynamic Datasource Spring Boot Starter实现

在Spring Boot中,可以通过多种方式实现多数据源的动态切换效果,在本篇文章中我介绍第二种实现方案。

一 具体实现

(1)测试使用的数据库

这里我们创建3个数据库,分别是:db01、db02、db03,然后这3个数据库都有一张名为user_info的表,表结构一样,只是数据不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mysql复制代码-- 建表语句
DROP TABLE IF EXISTS `user_info`;
CREATE TABLE `user_info` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`name` varchar(255) DEFAULT NULL COMMENT '姓名',
`age` int(11) DEFAULT NULL COMMENT '年龄',
`addr_city` varchar(255) DEFAULT NULL COMMENT '所在城市',
`addr_district` varchar(255) DEFAULT NULL COMMENT '所在区',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- db01中表「user_info」的数据
INSERT INTO `user_info` VALUES ('1', '张三', '20', '北京', '朝阳区');
INSERT INTO `user_info` VALUES ('2', '李四', '18', '北京', '东城区');

-- db02中表「user_info」的数据
INSERT INTO `user_info` VALUES ('1', '王五', '22', '上海', '普陀区');
INSERT INTO `user_info` VALUES ('2', '赵六', '24', '上海', '浦东新区');

-- db03中表「user_info」的数据
INSERT INTO `user_info` VALUES ('1', '孙七', '28', '成都', '武侯区');
INSERT INTO `user_info` VALUES ('2', '周八', '26', '成都', '天府新区');

(2)在pom.xml文件中添加相关依赖

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.2.1</version>
</dependency>

最新版本:mvnrepository.com/artifact/co…

(3)新增application-datasource2.yml配置文件

新建这个用于测试的配置文件,主要配置了接下来需要用到的多个数据源,其配置如下:

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
yaml复制代码server:
port: 8080
servlet:
session.timeout: 300

logging:
level:
org.springframework.web: debug
cn.zifangsky: debug
file:
name: web-exercise.log
path: logs

spring:
datasource:
# HikariCP 连接池配置
hikari:
pool-name: exercise_HikariCP
minimum-idle: 5 #最小空闲连接数量
idle-timeout: 30000 #空闲连接存活最大时间,默认600000(10分钟)
maximum-pool-size: 20 #连接池最大连接数,默认是10
auto-commit: true #此属性控制从池返回的连接的默认自动提交行为,默认值:true
max-lifetime: 1800000 #此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
connection-timeout: 30000 #数据库连接超时时间,默认30秒,即30000
dynamic:
primary: db01 #设置默认的数据源或者数据源组,默认值为master
datasource:
db01:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/db01?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&failOverReadOnly=false&useSSL=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
username: root
password: root
db02:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/db02?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&failOverReadOnly=false&useSSL=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
username: root
password: root
db03:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/db03?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&failOverReadOnly=false&useSSL=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
username: root
password: root

#mybatis
mybatis:
type-aliases-package: cn.zifangsky.example.webexercise.mapper
mapper-locations: classpath:mapper/*.xml

(4)新建一个测试使用的Mapper

在上篇文章的基础上,再新建一个测试使用的Mapper,跟上篇文章的那个Mapper类似,只是使用的注解不同而已。

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
java复制代码package cn.zifangsky.example.webexercise.mapper;

import cn.zifangsky.example.webexercise.model.UserInfo;
import com.baomidou.dynamic.datasource.annotation.DS;
import org.apache.ibatis.annotations.Param;

@DS("db02")
public interface UserInfoDynamicMapper2 {
/**
* 通过默认数据源查询,方法级别的注解优先级更高
*/
@DS("db01")
UserInfo selectByDefaultDataSource(Integer id);

/**
* 方法级别没有添加注解,则使用接口级别的注解,通过 db02 数据源查询
*/
UserInfo selectByDB02DataSource(Integer id);

/**
* 通过 db03 数据源查询
*/
@DS("db03")
UserInfo selectByDB03DataSource(Integer id);

/**
* 测试事务是否回滚(数据插入 db02 数据源)
*/
@DS("db02")
int addToDB02(UserInfo record);

/**
* 测试事务是否回滚(数据插入 db03 数据源)
*/
@DS("db03")
int addToDB03(UserInfo record);

/**
* 从 db03 数据源删除数据
*/
@DS("db03")
int deleteFromDB03ByName(@Param("name") String name);
}

其对应的UserInfoDynamicMapper.xml文件(文件内容除了类路径不同,其他跟上篇文章的那个Mapper.xml一样)是:

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.zifangsky.example.webexercise.mapper.UserInfoDynamicMapper2">
<resultMap id="BaseResultMap" type="cn.zifangsky.example.webexercise.model.UserInfo">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="age" jdbcType="INTEGER" property="age" />
<result column="addr_city" jdbcType="VARCHAR" property="addrCity" />
<result column="addr_district" jdbcType="VARCHAR" property="addrDistrict" />
</resultMap>
<sql id="Base_Column_List">
id, `name`, age, addr_city, addr_district
</sql>
<select id="selectByDefaultDataSource" parameterType="java.lang.Integer" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from user_info
where id = #{id,jdbcType=INTEGER}
</select>

<select id="selectByDB02DataSource" parameterType="java.lang.Integer" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from user_info
where id = #{id,jdbcType=INTEGER}
</select>

<select id="selectByDB03DataSource" parameterType="java.lang.Integer" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from user_info
where id = #{id,jdbcType=INTEGER}
</select>

<insert id="addToDB02" keyColumn="id" keyProperty="id" parameterType="cn.zifangsky.example.webexercise.model.UserInfo" useGeneratedKeys="true">
insert into user_info (`name`, age, addr_city,
addr_district)
values (#{name,jdbcType=VARCHAR}, #{age,jdbcType=INTEGER}, #{addrCity,jdbcType=VARCHAR},
#{addrDistrict,jdbcType=VARCHAR})
</insert>
<insert id="addToDB03" keyColumn="id" keyProperty="id" parameterType="cn.zifangsky.example.webexercise.model.UserInfo" useGeneratedKeys="true">
insert into user_info (`name`, age, addr_city,
addr_district)
values (#{name,jdbcType=VARCHAR}, #{age,jdbcType=INTEGER}, #{addrCity,jdbcType=VARCHAR},
#{addrDistrict,jdbcType=VARCHAR})
</insert>

<delete id="deleteFromDB03ByName" parameterType="java.lang.String">
delete from user_info
where name = #{name,jdbcType=VARCHAR}
</delete>
</mapper>

(5)使用单元测试测试「动态切换数据源」的效果

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
108
109
110
java复制代码package cn.zifangsky.example.webexercise.dataSource;

import cn.zifangsky.example.webexercise.mapper.UserInfoDynamicMapper2;
import cn.zifangsky.example.webexercise.mapper.UserInfoMapper;
import cn.zifangsky.example.webexercise.model.UserInfo;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.annotation.Transactional;

import java.sql.SQLException;

/**
* 测试动态切换数据源(Dynamic Datasource Spring Boot Starter)
*
* @author zifangsky
* @date 2020/11/6
* @since 1.0.0
*/
@DisplayName("测试动态切换数据源(Dynamic Datasource Spring Boot Starter)")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class DynamicDataSource2Test {

@Autowired
private UserInfoMapper userInfoMapper;

@Autowired
private UserInfoDynamicMapper2 userInfoDynamicMapper2;

@Test
@Order(1)
@DisplayName("普通方法——使用默认数据源")
public void testCommonMethod(){
UserInfo userInfo = userInfoMapper.selectByPrimaryKey(1);

Assertions.assertNotNull(userInfo);
Assertions.assertEquals("张三", userInfo.getName());
}

@Test
@Order(2)
@DisplayName("通过默认数据源查询,方法级别的注解优先级更高")
public void testSelectByDefaultDataSource(){
UserInfo userInfo = userInfoDynamicMapper2.selectByDefaultDataSource(1);

Assertions.assertNotNull(userInfo);
Assertions.assertEquals("张三", userInfo.getName());
}

@Test
@Order(3)
@DisplayName("方法级别没有添加注解,则使用接口级别的注解,通过 db02 数据源查询")
public void testSelectByDB02DataSource(){
UserInfo userInfo = userInfoDynamicMapper2.selectByDB02DataSource(1);

Assertions.assertNotNull(userInfo);
Assertions.assertEquals("王五", userInfo.getName());
}

@Test
@Order(4)
@DisplayName("方法级别添加注解,手动指定通过 db03 数据源查询")
public void testSelectByDB03DataSource(){
UserInfo userInfo = userInfoDynamicMapper2.selectByDB03DataSource(1);

Assertions.assertNotNull(userInfo);
Assertions.assertEquals("孙七", userInfo.getName());
}

@Test
@Order(5)
@DisplayName("在一个方法执行过程中嵌套操作多个数据源的情况")
public void testNestedMultiDataSource(){
//1. 从 db02 查询一条数据
UserInfo userInfo = userInfoDynamicMapper2.selectByDB02DataSource(1);

//2. 插入到 db03
userInfo.setId(null);
userInfoDynamicMapper2.addToDB03(userInfo);
}

@Test
@Order(6)
@DisplayName("从 db03 数据源删除数据")
public void testDeleteFromDB03ByName(){
userInfoDynamicMapper2.deleteFromDB03ByName("王五");
}

@Test
@Order(7)
@DisplayName("嵌套多个数据源的事务回滚情况")
@Transactional(rollbackFor = Exception.class)
public void testTransaction() throws SQLException {
//1. 从 db01 查询一条数据
UserInfo userInfo = userInfoDynamicMapper2.selectByDefaultDataSource(1);

//2. 分别插入到 db02 和 db03
userInfo.setId(null);
userInfoDynamicMapper2.addToDB02(userInfo);
userInfoDynamicMapper2.addToDB03(userInfo);

//3. 手动抛出一个异常,测试事务回滚效果
throw new SQLException("SQL执行过程中发生某些未知异常");
}

}

注:以上测试代码基于Junit5 测试框架编写,需要的依赖如下:

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>

运行单元测试后,其测试结果跟上篇文章一样,这里就省略截图吧。

二 这两种方案如何选择

我看了一下开源项目Dynamic Datasource Spring Boot Starter的源代码,发现它也有一个DynamicRoutingDataSource(com/baomidou/dynamic/datasource/DynamicRoutingDataSource.java),然后具体的实现逻辑跟我在上篇文章中介绍的那种方案实际也是类似的。

com/baomidou/dynamic/datasource/DynamicRoutingDataSource类

不过,通过查看这个开源项目的官方文档可以得知,这个项目支持的特性比较丰富,截止目前有以下这些:

然后,经过了多次更新迭代后,这个开源项目也相对比较稳定。因此,在这里我给出的建议是:

  • 如果想要实现简单,或者说想要将以上截图中的部分特性拿来就用,那么可以考虑使用这个开源项目;
  • 如果想要实现的功能比较单一,而且有尽可能减少外部依赖的需求,那么通过上篇文章介绍的方案来手动实现也是可以的。

参考:

  • blog.csdn.net/qq493820798…
  • baomidou.gitee.io/dynamic-dat…

本文转载自: 掘金

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

深入理解Go语言(06):Context原理分析

发表于 2020-11-11

一、背景

在golang中,最主要的一个概念就是并发协程 goroutine,它只需用一个关键字 go 就可以开起一个协程,并运行。

一个单独的 goroutine运行,倒也没什么问题。如果是一个goroutine衍生了多个goroutine,并且它们之间还需要交互-比如传输数据,那彼此怎么传输数据呢?如果一个子goroutine取消了,要取消跟其相关的goroutine,怎么样才可以做到?

比如说:在go web服务器中,每个请求request都是在一个单独的goroutine进行,这些
goroutine可能又开启其他的goroutine进行其他操作,那么多个goroutine之间怎么传输数据、遇到了问题怎么取消goroutine?

有时在程序开发中,每个请求用一个goroutine去处理程序,然而,处理程序时往往还需要其他的goroutine去访问后端数据资源,比如数据库、RPC服务等,这些goroutine都在处理同一个请求,所以他们需要访问一些共享资源,如用户身份信息、认证token等,如果请求超时或取消,与此请求相关的所有goroutine都应该退出并释放资源。

由于golang里没有像C语言中线程id类似的goroutine id,所以不能通过id直接关闭goroutine。但是有其他的方法。

解决方法:

  • 用时间来表示过期、超时
  • 用信号来通知请求该停止了
  • 用channel通知请求结束

为此,golang给我们提供了一个简单的操作包:Context 包。

二、Context是什么

golang中的Context包,是专门用来简化对于处理单个请求衍生出多个goroutine,goroutine之间传输数据、取消goroutine、超时控制等相关操作的一个包。

三、Context功能

  • 3.1 控制goroutine退出
    • 及时退出 WithCancel
    • 时间点退出 WithDeadline
    • 时间间隔退出 WithTimeout
1
go复制代码func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

WithCancel,绑定一个parent,返回一个cancelCtx的Context,用返回的 CancelFunc 就可以主动关闭Context。一旦cancel被调用,即取消该创建的Context。

1
go复制代码func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

WithDeadline,带有效期的cancelCtx的Context,即到达指定时间点调用CancelFunc方法才会执行

1
go复制代码func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithTimeout,带有超时时间的cancelCtx的Context,它是WithDeadline的封装,只不过WithTimeout为时间间隔,Deadline为时间点。

1
2
3
go复制代码func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}
  • 3.2 设置值
1
go复制代码func WithValue(parent Context, key, val interface{})

四、源码分析

go version go1.13.9

4.1 整体程序分析

/src/context/context.go

  • 重要接口
+ Context:定义了Context接口的4个方法。
+ canceler:context接口取消,定义了2个方法。
  • 重要结构体
+ emptyCtx:实现了Context接口,它是个空的context,它永远不会被取消,没有值,没有deadline。其主要作为`context.Background()`和`context.TODO()`返回这种根context或者不做任何操作的context。如果用父子关系来理解,emptyCtx就是用来创建父context。
+ cancelCtx:可以被取消
+ timerCtx:超时会被取消
+ valueCtx:可以存储k-v键值数据
  • 重要函数
+ Backgroud:返回一个空的context,常用作根context
+ TODO:返回一个空的context,常用语重构时期,没有合适的context可用
+ newCancenCtx:创建一个可取消的context
+ parentCancelCtx:找到第一个可取消的父节点
+ WithCancel:基于父contxt,生成一个可取消的context
+ WithDeadline:创建一个带有截止时间的context
+ WithTimeout:创建一个带有过期时间的context
+ WithValue:创建一个存储键值对k-v的context

Background 与 TODO 用法有啥区别呢?

看函数其实它们俩没多大区别,只是使用和语义上有点区别:

  1. Background:是上下文默认值,所有其他上下文都应该从它衍生出来
  2. TODO:只是在不确定该使用哪种上下文时使用

4.2 Context接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码type Context interface {
// Deadline返回一个到期的timer定时器,以及当前是否以及到期
Deadline() (deadline time.Time, ok bool)

// Done在当前上下文完成后返回一个关闭的通道,代表当前context应该被取消,以便goroutine进行清理工作
// WithCancel:负责在cancel被调用的时候关闭Done
// WithDeadline: 负责在最后其期限过期时关闭Done
// WithTimeout:负责超时后关闭done
Done() <-chan struct{}

// 如果Done通道没有被关闭则返回nil
// 否则则会返回一个具体的错误
// Canceled 被取消
// DeadlineExceeded 过期
Err() error

// 返回对应key的value
Value(key interface{}) interface{}
}
  • Done():
    返回一个channel,可以表示 context 被取消的信号。
    当channel被关闭或者到了deadline时,返回一个被关闭的channel。这是一个只读channel。根据golang里相关知识,读取被关闭的channel会读取相应的零值。并且源码里没有地方会向这个 channel 里面塞入值,因此在子协程里读这个 channel,除非被关闭,否则读不出任何东西。也正是利用这一点,子协程从channel里读出了值(零值)后,就可以做一些清理工作,尽快退出。
  • Deadline():
    主要用于设定超时时间的Context上,它的返回值(返回父任务设置的超时时间)用于表示该Context取消的时间点,通过这个时间,就可以判断接下来的操作。比如超时,可以取消操作。
  • Value():
    获取前面设置的key对于的value值
  • Err():
    返回一个错误,表示channel被关闭的原因。比如是被取消,还是超时

4.3 emptyCtx结构体

emptyCtx是一个不会被取消、没有到期时间、没有值、不会返回错误的context的实现,其主要作为context.Background()和context.TODO()返回这种根context或者不做任何操作的context。如果用父子关系来理解,emptyCtx就是用来创建父context。

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
go复制代码type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
    return
}

func (*emptyCtx) Done() <-chan struct{} {
    return nil
}

func (*emptyCtx) Err() error {
    return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
    return nil
}

func (e *emptyCtx) String() string {
    switch e {
    case background:
        return "context.Background"
    case todo:
        return "context.TODO"
    }
    return "unknown empty Context"
}

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

4.4 cancelCtx结构体

cancelCtx struct:

1
2
3
4
5
6
7
go复制代码type cancelCtx struct {
    Context
    mu       sync.Mutex            // protects following fields
    done     chan struct{}         // created lazily, closed by first cancel call
    children map[canceler]struct{} // set to nil by the first cancel call
    err      error                 // set to non-nil by the first cancel call
}
  • Context:cancelCtx嵌入一个Context接口对象,作为一个匿名字段。这个Context就是父context
  • mu:保护之后的字段
  • children:内部通过这个children保存所有可以被取消的context的接口,到后面,如果当前context被取消的时候,只需要调用所有canceler接口的context就可以实现当前调用链的取消
  • done:取消的信号
  • err:错误信息

Done() 函数:

1
2
3
4
5
6
7
8
9
go复制代码func (c *cancelCtx) Done() <-chan struct{} {
    c.mu.Lock()
    if c.done == nil {
        c.done = make(chan struct{})
    }
    d := c.done
    c.mu.Unlock()
    return d
}

函数返回一个只读channel,而且没有地方向这个channel里写数据。所以直接调用这个只读channel会被阻塞。一般通过搭配 select 来使用。一旦关闭,就会立即读出零值。

cancel() 函数:

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
go复制代码func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    if err == nil {// 必须传一个err值,后面判断用
        panic("context: internal error: missing cancel error")
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // already canceled 已经被其他协程取消了
    }
    c.err = err

// 关闭channel,通知其他协程
    if c.done == nil {
        c.done = closedchan
    } else {
        close(c.done)
    }
//遍历它是所有子节点
    for child := range c.children {
        // NOTE: acquiring the child's lock while holding parent's lock.
        child.cancel(false, err)// 递归地取消所有子节点
    }
// 将子节点清空
    c.children = nil
    c.mu.Unlock()
    if removeFromParent {
// 从父节点中移除自己
        removeChild(c.Context, c)
    }
}

这个函数功能就是关闭channel:c.done();
递归取消它的所有子节点;最后从父节点删除自己。
通过关闭channel,将取消信号传递给了它的所有子节点。
goroutine 接收到取消信号的方式就是 select 语句中的 读c.done 被选中

4.5 timerCtx 结构体

timerCtx struct:

1
2
3
4
5
go复制代码type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.
    deadline time.Time
}

timerCtx嵌入了cancelCtx结构体,所以cancelCtx的方法也可以使用。
timerCtx主要是用于实现WithDeadline和WithTimeout两个context实现,其继承了cancelCtx结构体,同时还包含一个timer.Timer定时器和一个deadline终止实现。Timer会在deadline到来时,自动取消context。

cancel()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err) //由于继承了cancelCtx,这里调用了cancelCtx的cancel()方法
    if removeFromParent {
        // Remove this timerCtx from its parent cancelCtx's children.
        removeChild(c.cancelCtx.Context, c)
    }
    c.mu.Lock()
    if c.timer != nil {
        c.timer.Stop()//停止定时器
        c.timer = nil
    }
    c.mu.Unlock()
}

这个函数继承了cancelCtx的方法cancel(),然后后面进行自身定时器Stop()的操作,这样就可以实现取消操作了。

4.6 valueCtx结构体

1
2
3
4
go复制代码type valueCtx struct {
    Context
    key, val interface{}
}

通过key-value来进行值保存

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码func (c *valueCtx) String() string {
    return contextName(c.Context) + ".WithValue(type " +
        reflectlite.TypeOf(c.key).String() +
        ", val " + stringify(c.val) + ")"
}

func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

4.7 WithCancel方法

WithCancel:

创建一个可取消的context

1
2
3
4
5
go复制代码func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

传入一个父context(通常是一个background作为根节点),返回新建context。
当 WithCancel 函数返回的 CancelFunc 被调用或者是父节点的 done channel 被关闭(父节点的 CancelFunc 被调用),此 context(子节点) 的 done channel 也会被关闭。

newCancelCtx()方法

1
2
3
go复制代码func newCancelCtx(parent Context) cancelCtx {
    return cancelCtx{Context: parent}
}

初始化cancelCtx结构体

propagateCancel()方法

这个函数主要作用是当parent context取消时候,进行child context的取消,这有2种模式:

  1. parent取消的时候通知child进行cancel取消
  2. parent取消的时候调用child的层层递归取消
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
go复制代码// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
// 父节点是空的,直接返回
    if parent.Done() == nil {
        return // parent is never canceled
    }

    if p, ok := parentCancelCtx(parent); ok {
        p.mu.Lock()
        if p.err != nil {
            // parent has already been canceled
            child.cancel(false, p.err)//父节点已经取消,它的子节点也需要取消
        } else {
//父节点未取消
            if p.children == nil {
                p.children = make(map[canceler]struct{})
            }
// 把这个child放到父节点上
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
    } else {
// 如果没有找到可取消的父 context。新启动一个协程监控父节点或子节点取消信号
        go func() {
            select {
// 保证父节点被取消的时候子节点会被取消
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}

parentCancelCtx

这个函数识别三种类型的Context:cancelCtx,timerCtx,valueCtx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码func parentCancelCtx(parent Context) (*cancelCtx, bool) {
for {
switch c := parent.(type) {
case *cancelCtx:
return c, true // 找到最近支持cancel的parent,由parent进行取消操作的调用
case *timerCtx:
return &c.cancelCtx, true // 找到最近支持cancel的parent,由parent进行取消操作的调用
case *valueCtx:
parent = c.Context // 递归
default:
return nil, false
}
}
}

4.8 按时间取消的函数

  • WithTimeout
  • WithDeadline

WithTimeout是直接调用WithDeadline函数,传入deadline是当前时间+timeout的时间,也就是从现在开始经过timeout时间就算超时。也就是说,WithDeadline用的是绝对时间。

WithTimeout():

1
2
3
go复制代码func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

WithDeadline()

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
go复制代码func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {
        // The current deadline is already sooner than the new one.
        return WithCancel(parent)
    }
    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  d,
    }

// 监听parent的取消,或者向parent注册自身
    propagateCancel(parent, c)
    dur := time.Until(d)
    if dur <= 0 {
// 已经过期
        c.cancel(true, DeadlineExceeded) // deadline has already passed
        return c, func() { c.cancel(false, Canceled) }
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
        c.timer = time.AfterFunc(dur, func() {
            c.cancel(true, DeadlineExceeded)
        })
    }
    return c, func() { c.cancel(true, Canceled) }
}

WithDeadline 方法在创建 timerCtx 的过程中,判断了父上下文的截止日期与当前日期,并通过 time.AfterFunc 创建定时器,当时间超过了截止日期后会调用 timerCtx.cancel 方法同步取消信号。

WithCancel、WithDeadline以及WithTimeout都返回了一个Context以及一个CancelFunc函数,返回的Context也就是我们当前基于parent创建了cancelCtx或则timerCtx,通过CancelFunc我们可以取消当前Context,即使timerCtx还未超时。

4.9 WithValue()

www.cnblogs.com/qcrao-2018/…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码//创建 valueCtx 的函数
func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    if !reflectlite.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}

func (c *valueCtx) String() string {
return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}

对 key 的要求是可比较,因为之后需要通过 key 取出 context 中的值,可比较是必须的。通过层层传递 context,最终形成这样一棵树.

和链表有点像,只是它的方向相反:Context 指向它的父节点,链表则指向下一个节点。通过 WithValue 函数,可以创建层层的 valueCtx,存储 goroutine 间可以共享的变量。取值的过程,实际上是一个递归查找的过程:

1
2
3
4
5
6
go复制代码func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}

它会顺着链路一直往上找,比较当前节点的 key 是否是要找的 key,如果是,则直接返回 value。否则,一直顺着 context 往前,最终找到根节点(一般是 emptyCtx),直接返回一个 nil。所以用 Value 方法的时候要判断结果是否为 nil。因为查找方向是往上走的,所以,父节点没法获取子节点存储的值,子节点却可以获取父节点的值。WithValue 创建 context 节点的过程实际上就是创建链表节点的过程

我的另一博客:www.cnblogs.com/jiujuan/p/1…

参考

  • www.cnblogs.com/qcrao-2018/…
  • blog.golang.org/context
  • draveness.me/golang/docs…
  • studygolang.com/articles/13…

本文转载自: 掘金

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

深入理解Go语言(07):内存分配原理

发表于 2020-11-11

一、Linux系统内存

在说明golang内存分配之前,先了解下Linux系统内存相关的基础知识,有助于理解golang内存分配原理。

1.1 虚拟内存技术

在早期内存管理中,如果程序太大,超过了空闲内存容量,就没有办法把全部程序装入到内存,这时怎么办? 在许多年前,人们采用了一种叫做覆盖技术,这样一种解决方案。

这是一种什么样的解决方案?
就是把程序分为若干个部分,称为覆盖块(overlay),核心思想就是分解(跟现代架构技术中分解、分模块思想很相近)。然后只把那些需要用到的指令和数据保存在内存中,而把其余的指令和数据保存在内存外。关键是需要程序员手动来分块。

这种技术有什么问题呢?
这种技术必须由程序员手工把一个大的程序划分为若干个小的功能模块,并确定各个模块之间的调用关系。手工做这种事情很费时费力,使得编程复杂度增加。但是,程序员总是爱“偷懒”的,于是,人们去寻找更好的方案。

这个方案就是虚拟内存技术,它的基本思路:
程序运行进程的总大小可以超过实际可用的物理内存的大小。每个进程都可以有自己独立的虚拟地址空间。然后通过CPU和MMU把虚拟内存地址转换为实际物理地址。

这个就相当于在物理内存和程序之间增加了一个中间层,虚拟内存。
虚拟存储也可以看作是对内存的一种抽象。而且这种抽象带来诸多好处:

  1. 它将内存看成是一个存储在磁盘上的地址空间的高速缓存,在内存中只保留了活动区域,可以根据需要在磁盘和内存间来回传送数据,高效使用内存。
  2. 它为每个进程提供了一致的地址空间,简化了存储的管理。
  3. 对进程起到保护作用,不被其他进程地址空间破坏,因为每个进程的地址空间都是相互独立。

(程序:静态的程序;进程:动态的,可以看作是程序的一个实例)

坏处:就是复杂度进一步增加,这也是必然的。不过相比带来的好处,复杂度的增加还是可以接受,并克服。

Linux中对进程的处理抽象成了一个结构体 task_struct,我前面文章有对这个结构体的介绍。下面就看看进程的内存。

1.2 进程的内存

进程内存在linux(32位)中的布局:

来自:manybutfinite.com/post/anatom…

最高位的1GB是linux内核空间,用户代码不能写,否则触发段错误。下面的3GB是进程使用的内存。

Kernel space:linux内核空间内存
Stack:进程栈空间,程序运行时使用。它向下增长,系统自动管理
Memory Mapping Segment:内存映射区,通过mmap系统调用,将文件映射到进程的地址空间,或者匿名映射。
Heap:堆空间。这个就是程序里动态分配的空间。linux下使用malloc调用扩展(用brk/sbrk扩展内存空间),free函数释放(也就是缩减内存空间)
BSS段:包含未初始化的静态变量和全局变量
Data段:代码里已初始化的静态变量、全局变量
Text段:代码段,进程的可执行文件

二、内存管理中的一些常见问题

1、未能释放已经不再使用的内存 - 内存泄漏

2、指向不可用的内存指针 - 野指针

3、指针所指向的对象已经被回收了,但是指向该对象的指针仍旧指向已经回收的内存地址 - 悬挂指针

4、分配或释放内存太快或者太慢

5、分配内存大小不合理,造成内存碎片问题

6、内存碎片问题

三、TCMalloc

可以查看前面的文章 TCMalloc内存分配简析,TCMalloc内存分配器的原理和golang内存分配器原理相近,所以理解了TCMalloc,golang内存分配原理也就理解大半,不过golang对它也有一些改动。

四、golang内存

4.1 golang怎么解决常见内存问题

golang是怎么解决 二 的内存管理中的常见问题的呢?

针对上面的1、2、3 这三种问题,golang使用自动垃圾回收机制,一般情况下,都不使用指针运算(要运算用unsafe包),很少的指针使用。当然,内存泄漏问题不能完全根除,但是可以解决一大部分问题。

针对下面的4、5、6 这三种问题,golang采用了多级缓存,预分配的方法,来加快内存分配和释放回收,尽量减少内存碎片。详见 TCMalloc内存分配简析 。

4.2 为什么要重新写一个内存分配器

内核已经有一个malloc的内存分配器,为什么还有重写一个内存分配器?

可以看到,malloc是一个很悠久的内存分配器,但是随着时代的发展,多核多线程已经普及,为了更好的应用多线程,提高程序效率,以及改进内存碎片,所以重新写了一个内存分配器。从这里 TCMalloc内存分配简析 可以看出TCMaloc的优点,它将内存划分为多级别,减少锁的开销。而且每个线程的缓存又分开了多个小的对象,以减少内存碎片。等等优化改进。

所以go内存分配也继承了这些优点。go还有一个原因,那就是go还有GC,需要配合内存的垃圾回收。

4.3 内存管理到底管理哪个区域

从上面的进程内存布局图,可以看出一个进程的内存划分了好多不同的区域,而内存管理主要管理的就是Stack和Heap,其中Stack (栈)区主要由编译器和系统管理,程序语言主要管理Heap(堆),主要是语言的runtime来管理。而且这里的进程内存指的是虚拟内存。

4.4 golang内存分配中的概念

golang内存分配的基本思想来自TCMalloc,所以go内存分配中的几个概念与TCMalloc很相似,可以看看TCMalloc 中的概念 。

mspan

mspan跟tcmalloc中的span相似,它是golang内存管理中的基本单位,也是由页组成的,每个页大小为8KB,与tcmalloc中span组成的默认基本内存单位页大小相同。mspan里面按照8*2n大小(8b,16b,32b …. ),每一个mspan又分为多个object。
就连名字也很像,mspan中的m应该是memory的第一个字母。

mcache

mcache跟tcmalloc中的ThreadCache相似,ThreadCache为每个线程的cache,同理,mcache可以为golang中每个Processor提供内存cache使用,每一个mcache的组成单位也是mspan。

mcentral

mcentral跟tcmalloc中的CentralCache相似,当mcache中空间不够用,可以向mcentral申请内存。可以理解为mcentral为mcache的一个“缓存库”,供mcaceh使用。它的内存组成单位也是mspan。
mcentral里有两个双向链表,一个链表表示还有空闲的mspan待分配,一个表示链表里的mspan都被分配了。

mheap

mheap跟tcmalloc中的PageHeap相似,负责大内存的分配。当mcentral内存不够时,可以向mheap申请。那mheap没有内存资源呢?跟tcmalloc一样,向OS操作系统申请。
还有,大于32KB的内存,也是直接向mheap申请。

总结

golang内存分配几个相关概念,用图来总结一下:

后面再进一步分析golang的内存分配原理。

我的另一博客:www.cnblogs.com/jiujuan/p/1…

五、参考

  • 可视化golang内存管理
  • 《操作系统的设计与实现》
  • a-program-in-memory linux内核分析很棒的文章

本文转载自: 掘金

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

CPU有缓存一致性协议(MESI),为何还需要volatil

发表于 2020-11-11

点赞再看,养成习惯,公众号搜一搜【一角钱技术】关注更多原创技术文章。本文 GitHub org_hejianhui/JavaStudy 已收录,有我的系列文章。

前言

  • 并发编程从操作系统底层工作的整体认识开始
  • 深入理解Java内存模型(JMM)及volatile关键字

前面我们从操作系统底层了解了现代计算机结构模型中的CPU指令结构、CPU缓存结构、CPU运行调度以及操作系统内存管理,并且学习了Java内存模型(JMM)和 volatile 关键字的一些特性。本篇来深入理解CPU缓存一致性协议(MESI),最后来讨论既然CPU有缓存一致性协议(MESI),为什么JMM还需要volatile关键字?

CPU高速缓存(Cache Memory)

CPU为何要有高速缓存

CPU在摩尔定律的指导下以每18个月翻一番的速度在发展,然而内存和硬盘的发展速度远远不及CPU。这就造成了高性能能的内存和硬盘价格及其昂贵。然而CPU的高度运算需要高速的数据。为了解决这个问题,CPU厂商在CPU中内置了少量的高速缓存以解决I\O速度和CPU运算速度之间的不匹配问题。

在CPU访问存储设备时,无论是存取数据抑或存取指令,都趋于聚集在一片连续的区域中,这就被称为局部性原理。

时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。

比如循环、递归、方法的反复调用等。

空间局部性(Spatial Locality):如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。

比如顺序执行的代码、连续创建的两个对象、数组等。

带有高速缓存的CPU执行计算的流程

  1. 程序以及数据被加载到主内存
  2. 指令和数据被加载到CPU的高速缓存
  3. CPU执行指令,把结果写到高速缓存
  4. 高速缓存中的数据写回主内存

目前流行的多级缓存结构

由于CPU的运算速度超越了1级缓存的数据I/O能力,CPU厂商又引入了多级的缓存结构。多级缓存结构示意图如下:

多核CPU多级缓存一致性协议MESI

多核CPU的情况下有多个一级缓存,如果保证缓存内部数据的一致,不让系统数据混乱。这里就引出了一个一致性的协议MESI。

MESI 协议缓存状态

MESI 是指4个状态的首字母。每个 Cache line 有4个状态,可用2个bit表示,它们分别是:

缓存行(Cache line):缓存存储数据的单元

注意:对于M 和 E 状态而言是精确的,它们在和该缓存行的真正状态是一致的,而 S 状态可能是非一致的。

如果一个缓存将处于S状态的缓存行作废了,而另一个缓存实际上可能已经独享了该缓存行,但是该缓存却不会将缓存行升迁为E状态,这是因为其他缓存不会广播它们作废掉该缓存行的通知,同样由于缓存并没有保存该缓存行的copy数量,因此(即使有这种通知)也没有办法确定自己是否已经独享了该缓存行。

从上面的意义来看 E状态 是一种投机性的优化:如果一个CPU想修改一个处于 S状态 的缓存行,总线事物需要将所有该缓存行的 copy 变成 invalid 状态,而修改 E状态 的缓存不需要使用总线事物。

MESI 状态转换


理解该图的前置说明:

  1. 触发事件
  2. cache分类
  • 前提:所有的cache共同缓存了主内存中的某一条数据。
  • 本地cache:指当前cpu的cache。
  • 触发cache:触发读写事件的cache。
  • 其他cache:指既除了以上两种之外的cache。
  • 注意:本地的事件触发 本地cache和触发cache为相同。

上图的切换解释:

下图示意了,当一个cache line的调整的状态的时候,另外一个cache line 需要调整的状态。

举例子来说:假设 cache 1 中有一个变量 x = 0 的 cache line 处于 S状态(共享)。
那么其他拥有 x 变量的 cache 2 、cache 3 等 x 的cache line 调整为 S状态(共享)或者调整为 I状态(无效)。

多核缓存协同操作

假设有三个CPU A、B、C,对应三个缓存分别是cache a、b、 c。在主内存中定义了x的引用值为0。
image.jpeg

单核读取

那么执行流程是:
CPU A 发出了一条指令,从主内存中读取x。

从主内存通过bus读取到缓存中(远端读取Remote read),这是该 Cache line 修改为 E状态(独享).
image.jpeg

双核读取

那么执行流程是:

  • CPU A 发出了一条指令,从主内存中读取x。
  • CPU A 从主内存通过bus读取到 cache a 中并将该 cache line 设置为 E状态。
  • CPU B 发出了一条指令,从主内存中读取x。
  • CPU B 试图从主内存中读取x时,CPU A 检测到了地址冲突。这时CPU A对相关数据做出响应。此时x 存储于cache a和cache b中,x在chche a和cache b中都被设置为S状态(共享)。

image.jpeg

修改数据

那么执行流程是:

  • CPU A 计算完成后发指令需要修改x.
  • CPU A 将x设置为 M状态(修改) 并通知缓存了x的CPU B, CPU B将本地cache b中的x设置为 I状态(无效)
  • CPU A 对x进行赋值。

image.jpeg

同步数据

那么执行流程是:

  • CPU B 发出了要读取x的指令。
  • CPU B 通知 CPU A,CPU A将修改后的数据同步到主内存时 cache a 修改为 E(独享)
  • CPU A同步CPU B的x,将cache a和同步后cache b中的x设置为 S状态(共享)。

image.jpeg

缓存行伪共享

什么是伪共享?

CPU缓存系统中是以缓存行(cache line)为单位存储的。目前主流的CPU Cache 的 Cache Line 大小都是64Bytes。在多线程情况下,如果需要修改“共享同一个缓存行的变量”,就会无意中影响彼此的性能,这就是伪共享(False Sharing)。

举个例子: 现在有2个long 型变量 a 、b,如果有t1在访问a,t2在访问b,而a与b刚好在同一个cache line中,此时t1先修改a,将导致b被刷新!

怎么解决伪共享?

Java8中新增了一个注解: @sun.misc.Contended 。加上这个注解的类会自动补齐缓存行,需要注意的是此注解默认是无效的,需要在jvm启动时设置 -XX:-RestrictContended 才会生效。

1
2
3
4
5
java复制代码@sun.misc.Contended
public final static class VolatileLong {
public volatile long value = 0L;
//public long p1, p2, p3, p4, p5, p6;
}

MESI优化和他们引入的问题

缓存的一致性消息传递是要时间的,这就使其切换时会产生延迟。当一个缓存被切换状态时其他缓存收到消息完成各自的切换并且发出回应消息这么一长串的时间中CPU都会等待所有缓存响应完成。可能出现的阻塞都会导致各种各样的性能问题和稳定性问题。

CPU切换状态阻塞解决-存储缓存(Store Bufferes)

比如你需要修改本地缓存中的一条信息,那么你必须将 I(无效)状态通知到其他拥有该缓存数据的CPU缓存中,并且等待确认。等待确认的过程会阻塞处理器,这会降低处理器的性能。应为这个等待远远比一个指令的执行时间长的多。

Store Bufferes

为了避免这种CPU运算能力的浪费,Store Bufferes 被引入使用。处理器把它想要写入到主存的值写到缓存,然后继续去处理其他事情。当所有失效确认(Invalidate Acknowledge)都接收到时,数据才会最终被提交。但这么做有两个风险。

Store Bufferes的风险

  • 第一:就是处理器会尝试从存储缓存(Store buffer)中读取值,但它还没有进行提交。这个的解决方案称为 Store Forwarding,它使得加载的时候,如果存储缓存中存在,则进行返回。
  • 第二:保存什么时候会完成,这个并没有任何保证。
1
2
3
4
5
6
7
8
9
10
11
java复制代码value = 3;
void exeToCPUA(){
value = 10;
isFinsh = true;
}
void exeToCPUB(){
if(isFinsh){
//value一定等于10?!
assert value == 10;
}
}

试想一下开始执行时,CPU A 保存着 isFinsh 在 E(独享)状态,而 value 并没有保存在它的缓存中。(例如,Invalid)。在这种情况下,value 会比 isFinsh 更迟地抛弃存储缓存。完全有可能 CPU B 读取 isFinsh 的值为true,而value的值不等于10。即isFinsh的赋值在value赋值之前。

这种在可识别的行为中发生的变化称为重排序(reordings)。注意,这不意味着你的指令的位置被恶意(或者好意)地更改。
它只是意味着其他的CPU会读到跟程序中写入的顺序不一样的结果。

硬件内存模型

执行失效也不是一个简单的操作,它需要处理器去处理。另外,存储缓存(Store Buffers)并不是无穷大的,所以处理器有时需要等待失效确认的返回。这两个操作都会使得性能大幅降低。为了应付这种情况,引入了失效队列(**invalid queue**)。它们的约定如下:

  • 对于所有的收到的Invalidate请求,Invalidate Acknowlege消息必须立刻发送
  • Invalidate并不真正执行,而是被放在一个特殊的队列中,在方便的时候才会去执行。
  • 处理器不会发送任何消息给所处理的缓存条目,直到它处理Invalidate。

即便是这样处理器已然不知道什么时候优化是允许的,而什么时候并不允许。

干脆处理器将这个任务丢给了写代码的人。这就是内存屏障(Memory Barriers)。

  • 写屏障 Store Memory Barrier(a.k.a. ST, SMB, smp_wmb)是一条告诉处理器在执行这之后的指令之前,应用所有已经在存储缓存(store buffer)中的保存的指令。
  • 读屏障Load Memory Barrier (a.k.a. LD, RMB, smp_rmb)是一条告诉处理器在执行任何的加载前,先应用所有已经在失效队列中的失效操作的指令。
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码void executedOnCpu0() {
value = 10;
//在更新数据之前必须将所有存储缓存(store buffer)中的指令执行完毕。
storeMemoryBarrier();
finished = true;
}
void executedOnCpu1() {
while(!finished);
//在读取之前将所有失效队列中关于该数据的指令执行完毕。
loadMemoryBarrier();
assert value == 10;
}

总结

既然CPU有缓存一致性协议(MESI),为什么JMM还需要volatile关键字?

volatile是java语言层面给出的保证,MSEI协议是多核cpu保证cache一致性的一种方法,中间隔的还很远,我们可以先来做几个假设:

  1. 回到远古时候,那个时候cpu只有单核,或者是多核但是保证sequence consistency,当然也无所谓有没有MESI协议了。那这个时候,我们需要java语言层面的volatile的支持吗?

当然是需要的,因为在语言层面编译器和虚拟机为了做性能优化,可能会存在指令重排的可能,而volatile给我们提供了一种能力,我们可以告诉编译器,什么可以重排,什么不可以。

  1. 那好,假设更进一步,假设java语言层面不会对指令做任何的优化重排,那在多核cpu的场景下,我们还需要volatile关键字吗?

答案仍然是需要的。因为 MESI只是保证了多核cpu的独占cache之间的一致性,但是cpu的并不是直接把数据写入L1 cache的,中间还可能有store buffer。有些arm和power架构的cpu还可能有load buffer或者invalid queue等等。因此,有MESI协议远远不够。

  1. 再接着,让我们再做一个更大胆的假设。假设cpu中这类store buffer/invalid queue等等都不存在了,cpu是数据是直接写入cache的,读取也是直接从cache读的,那还需要volatile关键字吗?

你猜的没错,还需要的。原因就在这个“一致性”上。consistency和coherence都可以被翻译为一致性,但是MSEI协议这里保证的仅仅coherence而不是consistency。那consistency和cohence有什么区别呢?

下面取自wiki的一段话:
Coherence deals with maintaining a global order in which writes to a single location or single variable are seen by all processors. Consistency deals with the ordering of operations to multiple locations with respect to all processors.

因此,MESI协议最多只是保证了对于一个变量,在多个核上的读写顺序,对于多个变量而言是没有任何保证的。很遗憾,还是需要volatile~~

  1. 好的,到了现在这步,我们再来做最后一个假设,假设cpu写cache都是按照指令顺序fifo写的,那现在可以抛弃volatile了吧?你觉得呢?

那肯定不行啊!因为对于arm和power这个weak consistency的架构的cpu来说,它们只会保证指令之间有比如控制依赖,数据依赖,地址依赖等等依赖关系的指令间提交的先后顺序,而对于完全没有依赖关系的指令,比如x=1;y=2,它们是不会保证执行提交的顺序的,除非你使用了volatile,java把volatile编译成arm和power能够识别的barrier指令,这个时候才是按顺序的。

最后总结,答案就是:还需要~~

参考资料

  • [1] igoro.com/archive/gal…
  • [2] en.wikipedia.org/wiki/Sequen…
  • [3] en.wikipedia.org/wiki/Consis…
  • [4] Maranget, Luc, Susmit Sarkar, and Peter Sewell. “A tutorial introduction to the ARM and POWER relaxed memory models.” Draft available from http://www. cl. cam. ac. uk/~ pes20/ppc-supplemental/test7. pdf (2012).
  • [5] www.zhihu.com/question/29…

PS:以上代码提交在 Github :github.com/Niuh-Study/…

文章持续更新,可以公众号搜一搜「 一角钱技术 」第一时间阅读, 本文 GitHub org_hejianhui/JavaStudy 已经收录,欢迎 Star。

本文转载自: 掘金

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

1…767768769…956

开发者博客

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