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

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


  • 首页

  • 归档

  • 搜索

【Java劝退师】Spring 知识脑图 - 全栈开源框架

发表于 2020-11-05

Spring

Spring

全栈开源框架

一、解决的问题

1. IOC 解偶

透过 Spring 的 IOC 容器,将对象间的依赖关系交给 Spring 来控制,避免硬编码造成的程序耦合。

2. 简化 AOP 编程

透过 Spring 的 AOP 功能,让我们可以更方便地进行面向切面编程。

3. 声明式事务 @Transaction

可以通过注解方式进行事务控制。

4. 集成各种框架、API,降低使用难度

框架 : Struts、 Hibernate、MyBatis、 Hessian、Quartz

API : JDBCTemplate、 JavaMail、RestTemplate

二、核心思想

1. IOC Inversion of Control 控制反转

将 Java 对象的创建、管理的权力 交给第三方 (Spring 框架)

目的 : 解偶

原理 : 透过反射调用无参构造函数实例化对象并存到容器中。【没有无参构造将实例化失败】

2. DI Dependancy Injection 依赖注入

将被管理的 Java 对象赋值给某个属性

目的 : 解偶

原理 : 使用反射技术设置目标对象属性

3. AOP Aspect oriented Programming 面向切面编程

一种横向抽取技术,在特定的方法前、方法后,运行特定的逻辑

目的 : 减少重复代码

应用场景 : 事务控制、权限较验、日志纪录 、性能监控

三、特殊类

  1. BeanFactory - 容器类的顶层接口 - 基础规范
1. ApplictaionContext - 容器类的高级接口 - 国际化、资源访问( XML、配置类 )


    1. ClassPathXmlApplicationContext - 项目根路径下加载配置文档
    2. FileSystemXmlApplicationContext - 硬盘路径下加载配置文档
    3. AnnotationConfigApplicationContext - 从注解配置类加载配置
  1. FactoryBean - 自定义复杂 Bean 的创建过程
  2. BeanFactoryPostProcessor - BeanFactory 初始化完成后,进行后置处理
  3. BeanPostProcessor - Bean 对象实例化、依赖注入后,进行 Bean 级别的后置处理

四、注解

1. IOC 类型

  1. @Component(“Bean的ID”) 【类上】 - 默认 ID 为类名首字母小写,等价以下三个注解
1. @Controller
2. @Service
3. @Repository
  1. @Scope(“Bean的生命周期”) 【类上】
1. singleton【默认】- 与容器生命周期相同
2. prototype - 每次获取都是新的
3. request - 一个HTTP请求范围内相同
4. session - Session 范围内相同
5. globalSession - portlet-based 应用范围内相同
  1. @PostConstruct 【方法上】- 初始化后调用
  2. @PreDestory 【方法上】- 销毁前调用

2. DI 类型

  1. @Autoweird - 依照类型注入

类型对应的对象非唯一时,可以搭配 @Qualifier(name=”Bean的ID”) 使用
2. @Resource(name=”Bean的ID”, type=类) - 默认依照ID注入,如果ID找不到则按类型注入

3. 配置类型

  1. @ComponentScan - 需要扫描的包路径
  2. @Configuration - 标明此类是配置类
  3. @PropertySource - 引入外部配置文档
  4. @Import - 加载其它配置类
  5. @Value - 将配置文档的数据赋值到属性上
  6. @Bean - 将方法返回的对象存入 IOC 容器中,对象ID为方法名,也可以手动指定

4. AOP 类型

  1. @Pointcut - 配置切入点
  2. @Before - 前置通知
  3. @AfterReturning - 后置通知
  4. @AfterThrowing - 异常通知
  5. @After - 最终通知
  6. @Around - 环绕通知

五、AOP

1. 术语

  1. Joinpoint 连接点 : 所有的方法
  2. PointCut 切入点 : 具体想要影响的方法
  3. Advice 通知/增强 : 横切逻辑
  4. Target 目标 : 要被代理的对象
  5. Proxy 代理 : 被 AOP 织入增强后的类
  6. Weaving 织入 : 把 Advice 应用到 Target 产生 Proxy 的过程
  7. Aspect 切面 : 切入点 + 增强

目的 : 为了锁定在哪个地方插入什么横切逻辑

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
java复制代码@Component
@Aspect
public class LogUtil {

/**
* 切入点表达式
* [访问修饰符] 返回值 包名.包名.包名.类名.方法名(参数表表)
*
* 【.】 用在包,表示任意包,有几级包写几个
* 【..】用在包,表示当前包及其子包
*
* 【..】用在参数表表,表示有无参数均可
* 【*】 用在参数表表,表示至少一个参数
*/
@Pointcut("execution(* com.lagou.service.impl.*.*(..))")
public void pointcut(){}

@Before("pointcut()")
public void beforePrintLog(JoinPoint jp){
Object[] args = jp.getArgs();
System.out.println("前置通知:beforePrintLog,参数是:" + Arrays.toString(args));
}

@AfterReturning(value = "pointcut()",returning = "rtValue")
public void afterReturningPrintLog(Object rtValue){
System.out.println("后置通知:afterReturningPrintLog,返回值是:"+ rtValue);
}

@AfterThrowing(value = "pointcut()",throwing = "e")
public void afterThrowingPrintLog(Throwable e){
System.out.println("异常通知:afterThrowingPrintLog,异常是:"+ e);
}

@After("pointcut()")
public void afterPrintLog(){
System.out.println("最终通知:afterPrintLog");
}

/**
* 环绕通知
*/
@Around("pointcut()")
public Object aroundPrintLog(ProceedingJoinPoint pjp){

// 定义返回值
Object rtValue = null;
try{

// 前置通知
System.out.println("前置通知");

// 1.获取参数
Object[] args = pjp.getArgs();

// 2.运行切入点方法
rtValue = pjp.proceed(args);

// 后置通知
System.out.println("后置通知");

} catch (Throwable t){

// 异常通知
System.out.println("异常通知");
t.printStackTrace();

}finally {

// 最终通知
System.out.println("最终通知");

}
return rtValue;
}
}

六、声明式事务 @Transaction

1. 四大特性

原子性 Atomicity : 操作要么都发生,要么都不发生

一致性 Consistency : 数据库从一个一致状态转换到另一个一致状态

隔离性 Isolation : 事务不能被其它的事务所干扰

持久性 Durability : 数据的改变是永久性的

2. 隔离级别

脏读 : 读取到另一个事务未提交的数据 - Read Committed 读已提交

不可重复读 : 读到另一个事务 update 的数据,两次读取到的数据 内容 不一样 - Repeatable Read 可重复读

幻读 : 读取到另一个事务 insert 或 delete 的数据,两次读取到的数据 数量 不一样 - Serializable 串行化

3. 传播行为

  1. REQUIRED【默认】 - 当前没有事务,就新建⼀个事务,如果已经存在⼀个事务,加入到这个事务中
  2. SUPPORTS - 支持当前事务,如果当前没有事务,就以非事务方式运行 - 查找
  3. MANDATORY - 使用当前的事务,如果当前没有事务,就抛出异常
  4. REQUIRES_NEW - 新建事务,如果当前存在事务,把当前事务挂起
  5. NOT_SUPPORTED - 以非事务方式运行操作,如果当前存在事务,就把当前事务挂起
  6. NEVER - 以非事务方式运行,如果当前存在事务,则抛出异常
1
java复制代码@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)

4. 失效场景

  1. 应用在非 public 修饰的方法上
  2. 同一个类中方法调用
  3. 异常被 try catch 吃掉
  4. propagation 设置错误
  5. rollbackFor 设置错误
  6. 数据库引擎不支持事务

5. 原理

○ 透过 JDK 动态代理 与 Cglib 动态代理 实现,数据库事务归根结柢是 Connection 的事务

○ Connection 是从连接池(C3P0、Druid)拿来的,Connection 可以产生 preparedStatement,preparedStatement 可以运行 execute() 方法直接运行 SQL 语句

○ 在 JDK 1.8 的环境下,JDK 动态代理的性能已经优于 Cglib 动态代理,但缺点是使用 JDK 动态代理的被代理类需要至少实现一个接口

1. JDK 动态代理

被代理类至少需实现一个接口

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

private Target target;

private JdkDynamicProxyTest(Target target) {
this.target = target;
}

public static Target newProxyInstance(Target target) {
return (Target) Proxy.newProxyInstance(JdkDynamicProxyTest.class.getClassLoader(),
new Class<?>[]{Target.class},
new JdkDynamicProxyTest(target));

}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method.invoke(target, args);
}
}

2. Cglib 动态代理

通过字节码底层继承要代理类来实现

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

private CglibProxyTest() {
}

public static <T extends Target> Target newProxyInstance(Class<T> targetInstanceClazz) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(targetInstanceClazz);
enhancer.setCallback(new CglibProxyTest());
return (Target) enhancer.create();
}

@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}

}

测试

1
2
3
java复制代码Target targetImpl = new TargetImpl();
Target dynamicProxy = JdkDynamicProxyTest.newProxyInstance(targetImpl);
Target cglibProxy = CglibProxyTest.newProxyInstance(TargetImpl.class);

七、Spring Bean 生命周期

  1. 反射调用无参构造实例化 Bean
  2. 使用反射设置属性值
  3. 如果 Bean 实现了 BeanNameAware 接口,则调用 setBeanName() 方法传入当前 Bean 的 ID 值
  4. 如果 Bean 实现了 BeanFactoryAware 接口,则调用 setBeanFactory() 方法传入当前工厂实例的引用
  5. 如果 Bean 实现了 ApplicationContextAware 接口,则调用 setApplictaionContext() 方法传入当前 ApplicationContext 实例的引用
  6. 如果 Bean 和 BeanPostProcessor 关联,则调用 postProcessBeforeInitialization() 方法,对 Bean 进行加工 - Spring 的 AOP 在此实现
  7. 如果 Bean 实现了 InitializingBean 接口,则调用 afterPropertiesSet() 方法
  8. 如果配置文档中指定了 init-method 属性,则调用该属性指定的方法
  9. 如果 Bean 和 BeanPostProcessor 关联,则调用 postProcessAfterInitialization() 方法 - 此时 Bean 已可在应用中使用
  10. 如果 Bean 的作用范围为 singleton,则将 Bean 放入 IOC 容器中;如果作用范围是 prototype,则将 Bean 交给调用者
  11. 如果 Bean 实现了 DisposableBean 接口,销毁 Bean 时将调用 destory() 方法 - 如果配置文档中设置 destory-method 属性,则调用该属性指定的方法

八、Spring Bean 循环依赖

流程

  1. SpringBean A 实例化,将自己放入 三级缓存 中,在实例化过程中,发现依赖 SpringBean B
  2. SpringBean B 实例化,将自己放入 三级缓存 中,在实例化过程中,发现依赖 SpringBean A
  3. SpringBean B 到 三级缓存 中获取尚未成形的 SpringBean A
  4. 将 SpringBean A 升级到 二级缓存,并进行一些扩展操作
  5. SpringBean B 创建完成后将自己放入 一级缓存
  6. SpringBean A 从 一级缓存 中获取 SpringBean B

无法处理场景

  • 单例 Bean 构造器循环依赖
  • 多例 Bean 循环依赖

本文转载自: 掘金

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

读spring Async的源码让我收获了什么?

发表于 2020-11-05

对于从事后端开发的同学来说,为了提升系统性能异步是必须要使用的技术之一。通常我们可以通过:线程、线程池、定时任务 和 回调等方法来实现异步,其中用得最多的可能是线程和线程池。
但创建线程需要实现Runnable接口或继承Thread类,为了避免单继承问题,我们优先使用实现Runnable接口的方式创建线程,在run方法中执行我们自己的业务逻辑。此外,使用线程池,我们也需要一个类去实现Runnable或Callable接口,然后将该类的实例提交到线程池中,如果该类实现的是Runnable接口,则在run方法中执行我们自己的业务逻辑,并且没有返回值,也获取不到异常信息。如果该类实现的是Callable接口,则在call方法中执行我们自己的业务逻辑,并且能获取返回值,也能捕获异常信息。
大家有没有发现有部分代码有冗余?spring的开发者们考虑到异步是一种思想,不应该拘泥于实现Runnable接口或Callable接口,在run方法或call方法中实现业务逻辑,它将线程的创建细节封装起来,只需少许的注解,就可以实现异步的功能,让我们把更多时间花在业务方法上。让我们一起看看spring是怎么做的?

一、spring异步的使用
1.在springboot的启动类上面加上@EnableAsync注解

2.在需要执行异步调用的业务方法加上@Async注解

3.在controller方法中调用这个业务方法

调用category/add接口后打印信息如下:

其中的add在end之后打印,说明确实是异步调用,spring的异步任务使用起来就是这么简单,不用怀疑,只需要在springboot的启动类加上@EnableAsync注解,然后在业务方法上加上@Async注解就可以搞定,so easy。
那么,它的底层是如何实现的呢?

二、源码分析
先从@EnableAsync注解开始,因为它是一切的开始

该注解还是非常简单的,关键是Import了AsyncConfigurationSelector类。

知识点:其实EnableXXX开头的注解,在springboot中使用非常多,它更像一个开关,使用该注解就开启了相关功能,说白了,就是通过@Import注解引入相关的功能类。

真正核心的内容在Import的类中。

AsyncConfigurationSelector类的代码也非常简单,它会根据不同的adviceMode通知模式引入不同的配置类。

知识点:看到这里明白了为什么上一步@EnableAsync注解Import的AsyncConfigurationSelector类,而不是直接引入配置类,因为根据不同的adviceMode通知模式引入不同的配置类,不能单独只引入一个Configuration配置类。selectImports方法是在BeanPostProcessor解析Configuration配置类的时候调用的,import的类有三种:Configuration配置类、实现了ImportSelector接口的类 和 实现了ImportBeanDefinitionRegistrar接口的类。后面两种都可以根据不同的条件返回不同的配置类,有什么区别呢?最大的区别是ImportBeanDefinitionRegistrar接口除了可以获取到注解元数据之后,还可以获取到ImportBeanDefinitionRegistrar类,这个类可以获取到所有注册的BeanDefinition实例。

好了,接下来,我们一起看看ProxyAsyncConfiguration配置类。

该类是一个配置类,里面创建了AsyncAnnotationBeanPostProcessor实例,并将@EnableAsync注解中的属性赋值到该实例对象上。

AsyncAnnotationBeanPostProcessor实现了BeanPostProcessor和BeanFactoryAware接口,其实BeanPostProcessor是后置处理器,我们知道AOP的入口类就是后置处理器。而实现了BeanFactoryAware接口,就意味着要重写setBeanFacotory方法,该方法是核心代码:

知识点:我们在项目中如果想根据bean的名称获取bean实例该怎么办呢?以前我们的做法是new一个ClassPathXmlApplicationContext对象applicationContext,使用这个对象根据bean的名称获取bean实例。现在可以通过定义一个类实现:BeanFactoryAware、ApplicationContextAware 和 ApplicationListener ,从重写的方法入参中可以获取到spring容器对象,用该容器对象就能根据bean的名称获取bean实例。

上面的setBeanFacotory方法创建了一个切面AsyncAnnotationAdvisor,切面有两个要素:通知和切入点,我们一起看看它是怎么玩的?

先将@Async和javax.ejb.Asynchronous类添加到set集合中,然后使用buildAdvice方法创建通知,使用buildPointcut方法创建切入点。

buildAdvice方法里面只创建了一个拦截器AnnotationAsyncExecutionInterceptor实例,spring 异步任务的主要逻辑就在这个拦截器中实现的。

该方法的逻辑:

1.根据invocation对象先找到targetClass类

2.再根据invocation.getMethod()和targetClass类校验目标方法的访问权限,然后找到真正的目标方法。

3.根据目标方法找到任务执行器

4.创建一个Callable匿名类,在它的call方法中执行目标方法,如果是Future类型则返回数据。

5.将Callable匿名类实例提交到任务,返回这个方法的数据。


这个方法会先从缓存中根据method查询AsyncTaskExecutor,如果不为空,则直接返回。

1
2
3
4
5
bash复制代码        ![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ae48dbe77478456fafc8e9a0f078e8ac~tplv-k3u1fbpfcp-watermark.image)![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e2768516396d47f6afea4473c37d5ee0~tplv-k3u1fbpfcp-watermark.image)

知识点:缓存其实就是一个ConcurrentHashMap对象,这种用法在spring中随处可见,比如bean实例放在singletonObjects对象中,该对象也是一个ConcurrentHashMap,这样做的好处是为了提升性能,不用每次都new AsyncTaskExecutor对象的实例。

如果缓存中没有,则通过getExecutorQualifier方法找出method中定义的任务执行器的名字

1
sql复制代码 该方法先从method上面找@Async注解,如果有则使用方法上定义的执行器名称,如果没有则用该方法所在类上定义的执行器名称,所以要特别注意一下,@Async注解既可以使用在方法上,又可以使用在类上面,如果方法和类上面都定义了,优先使用方法上定义的执行器名称。

再看看获取任务执行器的方法:

它最终会根据名称和类型从容器中获取相应的bean实例,即AsyncTaskExecutor对象实例。

那么问题来了:什么时候可以获取到AsyncTaskExecutor对象实例?

就是在创建了ThreadPoolTaskExecutor线程池的时候,但是不只这一个线程池,只要实现了AsyncTaskExecutor接口的线程池都可以。

回到上面代码,如果既没有从缓存中获取到syncTaskExecutor对象实例,又没有定义过线程池,则创建一个默认的任务执行器:SimpleAsyncTaskExecutor对象。

最后将创建的任务执行器放入缓存中,然后任务执行器。

有意思的是这段代码:

使用了双重检查锁

并且defaultExecutor对象被定义成了volatile的,为什么要这样定义?

是为了解决指令重排问题,比如:一个new Object();代码,看起来简单,其底层其实分了三步:分配内存,初始化,将引用指向分配的对象。这三步在synchronized同步代码块中是可能发生指令重排的,如果指令重排了可能会出现先分配内存和将引用指向分配的对象,还没来得及初始化,另外一个线程调用这个对象时就会报错。所以,这里使用volatile,防止指令重排。如果有些朋友想进一步了解volatile原理,可以看看《天天在用volatile,你知道它的底层原理吗?》。

那么,为什么说它有意思?
因为它跟一般的双重检查锁不一样,它使用了targetExecutor局部变量保存defaultExecutor对象的值。

为什么要这样设计?
普通的双重检查锁加volatile关键字,虽说可以解决指令重排问题,但是需要消耗一定的性能,因为volatile的底层是通过内存屏障命令来处理的,内存屏障会增加额外的开销。第一个为空的判断,完全没有必要使用内存屏障,第二个为空的判断才需要,即实例化任务执行器的时候,可以缩小内存屏障使用范围。

最后,看一下invoke方法中的doSubmit方法

这个方法可以说是spring异步的核心,根据不同的返回值类型,使用不同的AsyncTaskExecutor任务执行器,执行不同的操作:

CompletableFuture类型使用CompletableFuture异步执行,返回数据

ListenableFuture类型使用AsyncListenableTaskExecutor提交任务,返回数据

Future类型使用AsyncTaskExecutor提交任务,返回数据

其他的使用AsyncTaskExecutor提交任务,返回空

知识点:
AsyncListenableTaskExecutor和AsyncTaskExecutor有什么区别?
区别是AsyncListenableTaskExecutor返回ListenableFuture类型的数据,而AsyncTaskExecutor返回Future类型的数据,而ListenableFuture是对Future的增强,我们知道Future表示一个异步计算任务,当任务完成时可以得到计算结果。如果我们希望一旦计算完成就拿到结果展示给用户或者做另外的计算,就必须使用另一个线程不断的查询计算状态。这样做,代码复杂,而且效率低下。使用ListenableFuture Guava帮我们检测Future是否完成了,如果完成就自动调用回调函数,这样可以减少并发程序的复杂度。

三、收获
使用EnableXXX开头的注解,配合@import注解一起提供一个开关的功能。

@import注解中的类包含:Configuration配置类、实现了ImportSelector接口的类 和 实现了ImportBeanDefinitionRegistrar接口的类 三种。

AOP的入口是BeanPostProcessor接口的实现类,我们可以在该类中定义切面来实现异步的功能,切面的两个要素:切入点 和 通知。

实现了BeanFactoryAware、ApplicationContextAware 和 ApplicationListener接口的类,可以获取到spring容器。

可以使用ConcurrentHashMap做缓存,bean实例放的singletonObjects对象就是ConcurrentHashMap类型

使用synchronized和volatile实现双重检查锁时,使用局部变量性能更好

使用ListenableFuture可以拿到计算完成的结果,而Future只能拿到整个任务完成的结果。

通过阅读spring异步的源码,收获还是蛮多的,关键是要多思考。

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,或者点赞、转发、在看。在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多大厂的前辈交流和学习。坚持原创不易,你的支持是我坚持的最大动力,谢谢啦。

苏三说技术 发起了一个读者讨论
通过CompletableFuture获取返回数据,跟通过Callable的Future获取返回数据 有什么区别?

本文转载自: 掘金

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

Dubbo整合Seata,教你轻松实现TTC模式分布式事务!

发表于 2020-11-05

整理了一份Java、JVM、多线程、MySQL、Redis、Kafka、Docker、RocketMQ、Nginx、MQ队列、数据结构、并发编程、并发压测、秒杀架构等技术知识点PDF,如果你有需要的话,可_click here_领取

Seata

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

Seata的事务模式 Seata针对不同的业务场景提供了四种不同的事务模式,具体如下

  • AT模式: AT 模式的一阶段、二阶段提交和回滚(借助undo_log表来实现)均由 Seata 框架自动生成,用户只需编写“业务SQL”,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。
  • TTC模式: 相对于 AT 模式,TCC 模式对业务代码有一定的侵入性,但是 TCC 模式无 AT 模式的全局行锁,TCC 性能会比 AT模式高很多。( 适用于核心系统等对性能有很高要求的场景。)
  • SAGA模式:Sage 是长事务解决方案,事务驱动,使用那种存在流程审核的业务场景,如: 金融行业,需要层层审核。
  • XA模式: XA模式是分布式强一致性的解决方案,但性能低而使用较少。
![](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/b3f56ca13e013a33369b9c4175bf9c8ba7a59e216cf226be189e81d006066a29)

TCC模式

tcc模式主要可以分为三个阶段:

  • Try:做业务检查和资源预留
  • Confirm:确认提交
  • Cancel:业务执行错误需要回滚的状态下执行分支事务的业务取消,预留资源释放

TCC模式下常见的三种异常

1.空回滚

空回滚就是对于一个分布式事务,在没有调用 TCC 资源 Try 方法的情况下(如机器宕机、网络异常),调用了二阶段的 Cancel 方法,Cancel 方法需要识别出这是一个空回滚,然后直接返回成功。

解决方案

需要一张额外的事务控制表,其中有分布式事务 ID 和分支事务 ID,第一阶段 Try 方法里会插入一条记录,表示一阶段执行了。Cancel 接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存在,则是空回滚。

2.幂等

幂等就是对于同一个分布式事务的同一个分支事务,重复去调用该分支事务的第二阶段接口,因此,要求 TCC 的二阶段 Confirm 和 Cancel 接口保证幂等,不会重复使用或者释放资源。如果幂等控制没有做好,很有可能导致资损等严重问题。

解决方案

记录每个分支事务的执行状态。在执行前状态,如果已执行,那就不再执行;否则,正常执行。前面在讲空回滚的时候,已经有一张事务控制表了,事务控制表的每条记录关联一个分支事务,那我们完全可以在这张事务控制表上加一个状态字段,用来记录每个分支事务的执行状态。

3.悬挂

悬挂就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。因为允许空回滚的原因,Cancel 接口认为 Try 接口没执行,空回滚直接返回成功,对于 Seata 框架来说,认为分布式事务的二阶段接口已经执行成功,整个分布式事务就结束了。

解决方案

二阶段执行时插入一条事务控制记录,状态为已回滚,这样当一阶段执行时,先读取该记录,如果记录存在,就认为二阶段已经执行;否则二阶段没执行。

Seata Server安装

第一步:下载安装服务端,并解压到指定位置

1
python复制代码unzip  seata-server-1.1.0.zip

seata的目录结构:

  • bin:存放各系统的启动脚本
  • conf:存放seata server启动时所需要配置信息、数据库模式下所需要的建表语句
  • lib:运行seata server所需要的的依赖包

第二步:配置seata

seata的配置文件(conf目录下)

  • file.conf: 该文件用于配置存储方式、透传事务信息的NIO等信息,默认对应registry.conf中file配置方式
  • registry.conf:seata server核心配置文件,可以通过该文件配置服务注册方式、配置读取方式。

注册方式目前支持file、nacos、eureka、redis、zk、consul、etcd3、sofa等方式,默认为file,对应读取file.conf内的注册方式信息。 读取配置信息的方式支持file、nacos、apollo、zk、consul、etcd3等方式,默认为file,对应读取file.conf文件内的配置。

修改registry.conf

  • 注册中心使用Nacos
  • 配置中心使用file进行配置

注册中心配置,用于TC,TM,RM的相互服务发现

1
2
3
4
5
6
7
8
ini复制代码registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
consul {
cluster = "seata"
serverAddr = "127.0.0.1:8848"
}
}

配置中心配置,用于读取TC的相关配置

1
2
3
4
5
6
7
ini复制代码config {
# file、nacos 、apollo、zk、consul、etcd3
type = "file"
file {
name = "file.conf"
}
}

修改file.conf,配置中心配置,用于读取TC的相关配置

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
ini复制代码store {
## store mode: file?.b
mode = "file"

## file store property
file {
## store location dir
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
maxBranchSessionSize = 16384
# globe session size , if exceeded throws exceptions
maxGlobalSessionSize = 512
# file buffer size , if exceeded allocate new buffer
fileWriteBufferCacheSize = 16384
# when recover batch read size
sessionReloadReadSize = 100
# async, sync
flushDiskMode = async
}

## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
registry.confuser = "mysql"
password = "mysql"
minConn = 1
maxConn = 10
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
}
}

第三步:创建seata数据库、以及所需的三张表

  • global_table: 存储全局事务session数据的表
  • branch_table:存储分支事务session数据的表
  • lockTable:存储分布式锁数据的表

– ——————————– The script used when storeMode is ‘db’ ——————————–
– the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS global_table
(
xid VARCHAR(128) NOT NULL,
transaction_id BIGINT,
status TINYINT NOT NULL,
application_id VARCHAR(32),
transaction_service_group VARCHAR(32),
transaction_name VARCHAR(128),
timeout INT,
begin_time BIGINT,
application_data VARCHAR(2000),
gmt_create DATETIME,
gmt_modified DATETIME,
PRIMARY KEY (xid),
KEY idx_gmt_modified_status (gmt_modified, status),
KEY idx_transaction_id (transaction_id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
– the table to store BranchSession data
CREATE TABLE IF NOT EXISTS branch_table
(
branch_id BIGINT NOT NULL,
xid VARCHAR(128) NOT NULL,
transaction_id BIGINT,
resource_group_id VARCHAR(32),
resource_id VARCHAR(256),
branch_type VARCHAR(8),
status TINYINT,
client_id VARCHAR(64),
application_data VARCHAR(2000),
gmt_create DATETIME,
gmt_modified DATETIME,
PRIMARY KEY (branch_id),
KEY idx_xid (xid)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
– the table to store lock data
CREATE TABLE IF NOT EXISTS lock_table
(
row_key VARCHAR(128) NOT NULL,
xid VARCHAR(96),
transaction_id BIGINT,
branch_id BIGINT NOT NULL,
resource_id VARCHAR(256),
table_name VARCHAR(32),
pk VARCHAR(36),
gmt_create DATETIME,
gmt_modified DATETIME,
PRIMARY KEY (row_key),
KEY idx_branch_id (branch_id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;

第四步:启动seata server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sql复制代码sh seata-server.sh -p 8091 -h 0.0.0.0 -m file 

Options:

--host, -h
The host to bind.
Default: 0.0.0.0

--port, -p
The port to listen.
Default: 8091

--storeMode, -m log store mode : file、db
Default: file

--help

补充:

  • 外网访问:如果需要外网访问 需要将0.0.0.0转成外网IP
  • 后台启动: nohup sh seata-server.sh -p 8091 -h 127.0.0.1 -m file > catalina.out 2>&1 &

在nacos中看到seata的注册信息

Dubbo整合Seata实现AT模式

引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>2.1.0.RELEASE</version>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.1.0</version>
</dependency>

application.properties 配置文件

1
2
3
4
5
6
7
8
ini复制代码#mysql
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/dubbodemo
spring.datasource.username=mysql
spring.datasource.password=mysql

spring.cloud.alibaba.seata.tx-service-group=springcloud-alibaba-producer-test

resoures目录下新建 file.conf ,配置如下

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
ini复制代码transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
# the client batch send request enable
enableClientBatchSendRequest = false
#thread factory for netty
threadFactory {
bossThreadPrefix = "NettyBoss"
workerThreadPrefix = "NettyServerNIOWorker"
serverExecutorThreadPrefix = "NettyServerBizHandler"
shareBossWorker = false
clientSelectorThreadPrefix = "NettyClientSelector"
clientSelectorThreadSize = 1
clientWorkerThreadPrefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
bossThreadSize = 1
#auto default pin or 8
workerThreadSize = "default"
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
# service configuration, only used in client side
service {
#transaction service group mapping
vgroupMapping.springcloud-alibaba-producer-test = "seata"
seata.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}
#client transaction configuration, only used in client side
client {
rm {
asyncCommitBufferLimit = 10000
lock {
retryInterval = 10
retryTimes = 30
retryPolicyBranchRollbackOnConflict = true
}
reportRetryCount = 5
tableMetaCheckEnable = false
reportSuccessEnable = false
sqlParserType = druid
}
tm {
commitRetryCount = 5
rollbackRetryCount = 5
}
undo {
dataValidation = true
logSerialization = "jackson"
logTable = "undo_log"
}
log {
exceptionRate = 100
}
}

resoures目录下新建registry.conf 配置如下:

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
ini复制代码registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"

nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
cluster = "seata"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = "0"
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}

config {
# file、nacos 、apollo、zk、consul、etcd3
type = "file"

nacos {
serverAddr = "localhost"
namespace = ""
group = "SEATA_GROUP"
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
app.id = "seata-server"
apollo.meta = "http://192.168.1.204:8801"
namespace = "application"
}
zk {
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}

DataSourceProxyConfig数据源加载

  • SEATA是基于数据源拦截来实现的分布式事务, 需要排除掉SpringBoot默认自动注入DataSourceAutoConfigurationBean,自定义配置数据源。

在启动类上添加:@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) ,并添加以下配置类

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复制代码@Configuration
public class DataSourceProxyConfig {

@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}

@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}

@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:/mapper/*Mapper.xml"));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}

@Bean public GlobalTransactionScanner globalTransactionScanner(){ return new GlobalTransactionScanner("account-gts-seata-example", "account-service-seata-service-group"); }

}

创建业务表

1
2
3
4
5
6
7
8
9
sql复制代码# 业务数据表
CREATE TABLE `sys_user` (
`id` varchar(36) NOT NULL,
`name` varchar(100) NOT NULL,
`msg` varchar(500) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `sys_user` (`id`, `name`, `msg`) VALUES ('1', '小王', '初始化数据');

TTC事务接口

1
2
3
4
5
6
7
8
9
less复制代码public interface IUserTccService {

@TwoPhaseBusinessAction(name = "IUserTccService",commitMethod = "commit",rollbackMethod = "rollback")
boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "userPojo") UserPojo userPojo);

boolean commit(BusinessActionContext actionContext);

boolean rollback(BusinessActionContext actionContext);
}

业务接口

1
2
3
arduino复制代码public interface IUserService {
public String ceshi(String input);
}

TTC事务实现类

  • 初步操作 Try:完成所有业务检查,预留必须的业务资源,本身也有数据操作。(支付场景:冻结预扣款30元)
  • 确认操作 Confirm:真正执行的业务逻辑,不做任何业务检查,只使用 Try 阶段预留的业务资源。因此,只要 Try操作成功,Confirm 必须能成功。另外,Confirm 操作需满足幂等性,保证一笔分布式事务能且只能成功一次。(支付场景:扣除预付款)
  • 取消操作 Cancel:释放 Try 阶段预留的业务资源,来回滚Try的数据操作。同样的,Cancel操作也需要满足幂等性。(支付场景:释放预付款)
    @Service
    public class UserTccServiceImpl implements IUserTccService {
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
typescript复制代码@Autowired
UserPojoMapper userPojoMapper;

@Override
public boolean prepare(BusinessActionContext actionContext, UserPojo userPojo) {
System.out.println("actionContext获取Xid commit>>> "+ RootContext.getXID());
int storage =userPojoMapper.updateByPrimaryKey(userPojo);
if (storage > 0){
return true;
}
return false;
}

@Override
public boolean commit(BusinessActionContext actionContext) {
System.out.println("actionContext获取Xid commit>>> "+actionContext.getXid());
return true;
}

@Override
public boolean rollback(BusinessActionContext actionContext) {
System.out.println("actionContext获取Xid rollback>>> "+actionContext.getXid());
UserPojo userPojo = JSONObject.toJavaObject((JSONObject)actionContext.getActionContext("userPojo"),UserPojo.class);
userPojo.setName("姓名被回滾了");
int storage = userPojoMapper.updateByPrimaryKey(userPojo);
if (storage > 0){
return true;
}
return false;
}

}

业务实现类

  • 没有涉及RPC调用的话只是一个分支事务,并不会触发rollback,并且分支事务共享同一个全局事务ID(即XID)。分支事务本身也具有原子性,可以确保本地事务的原子性(可以理解为prepare、commit方法虽然执行了,但是事务还是没执行,会在RM和TC协调下顺序执行 )
  • TC(事务协调者):会使得全部try执行成功后,才开始执行confirm,如果存在一个try执行失败(或者网络问题RPC调用失败),会开启回滚分布式事务,这一点满足了分布式事务的原子性。
  • GlobalTransactionScanner 会同时启动 RM 和 TM client。
    @Service
    public class UserServiceImpl implements IUserService{
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typescript复制代码@Autowired
UserPojoMapper userPojoMapper;

@Autowired
UserTccServiceImpl userTccService;

private static Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);

@Override
@GlobalTransactional
public String ceshi(String input) {
logger.info("全局XID:{}", RootContext.getXID());
UserPojo userPojo = userPojoMapper.selectByPrimaryKey("1");
// userPojo.setId("11111111");
userPojo.setName("正常提交");
if (userTccService.prepare(null,userPojo)){
return "Hello World,"+input+"! ,I am "+ userPojo.getName();
}
return "失敗";
}

}

调用服务,进行验证

日志打印:分布式事务信息

访问127.0.0.1:8081/hello 页面

本文转载自: 掘金

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

探索Java8:(二)Function接口的使用

发表于 2020-11-05

Java8 添加了一个新的特性Function,顾名思义这一定是一个函数式的操作。我们知道Java8的最大特性就是函数式接口。所有标注了@FunctionalInterface注解的接口都是函数式接口,具体来说,所有标注了该注解的接口都将能用在lambda表达式上。

标注了@FunctionalInterface的接口有很多,但此篇我们主要讲Function,了解了Function其他的操作也就很容易理解了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
/**
* @return a composed function that first applies the {@code before}
* function and then applies this function
*/
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
/**
* @return a composed function that first applies this function and then
* applies the {@code after} function
*/
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
}

为了方便地阅读源码,我们需要了解一些泛型的知识,如果你对泛型已经很熟悉了,那你可以跳过这段 。

泛型是JDK1.5引入的特性,通过泛型编程可以使编写的代码被很多不同的类型所共享,这可以很好的提高代码的重用性。因为本篇重点不是介绍泛型,所以我们只关注上述Function源码需要用到的泛型含义。

1. 泛型类

泛型类使用<T>来表示该类为泛型类,其内部成员变量和函数的返回值都可以为泛型<T> ,Function源码的标识为<T,R>,也就是两个泛型参数,此处不再赘述,具体泛型类可以看网上的文章。

2. 泛型方法和通配符

在方法修饰符的后面加一个<T>表明该方法为泛型方法,如Function 的源码里的compose方法的<V>。通配符也很好理解,还是compose的例子,我们可以看到compose的参数为一个Function类型,其中Functin的参数指定了其第一个参数必须是V的父类,第二个参数必须继承T,也就是T的子类。

源码解析

1.apply

讲完了上面这些就可以开始研究源码了。

首先我们已经知道了Function是一个泛型类,其中定义了两个泛型参数T和R,在Function中,T代表输入参数,R代表返回的结果。也许你很好奇,为什么跟别的java源码不一样,Function 的源码中并没有具体的逻辑呢?

其实这很容易理解,Function 就是一个函数,其作用类似于数学中函数的定义 ,(x,y)跟<T,R>的作用几乎一致。

y=f(x)y=f(x)y=f(x)
所以Function中没有具体的操作,具体的操作需要我们去为它指定,因此apply具体返回的结果取决于传入的lambda表达式。

1
java复制代码 R apply(T t);

举个例子:

1
2
3
4
5
java复制代码public void test(){
Function<Integer,Integer> test=i->i+1;
test.apply(5);
}
/** print:6*/

我们用lambda表达式定义了一个行为使得i自增1,我们使用参数5执行apply,最后返回6。这跟我们以前看待Java的眼光已经不同了,在函数式编程之前我们定义一组操作首先想到的是定义一个方法,然后指定传入参数,返回我们需要的结果。函数式编程的思想是先不去考虑具体的行为,而是先去考虑参数,具体的方法我们可以后续再设置。

再举个例子:

1
2
3
4
5
6
7
8
9
10
11
java复制代码public void test(){
Function<Integer,Integer> test1=i->i+1;
Function<Integer,Integer> test2=i->i*i;
System.out.println(calculate(test1,5));
System.out.println(calculate(test2,5));
}
public static Integer calculate(Function<Integer,Integer> test,Integer number){
return test.apply(number);
}
/** print:6*/
/** print:25*/

我们通过传入不同的Function,实现了在同一个方法中实现不同的操作。在实际开发中这样可以大大减少很多重复的代码,比如我在实际项目中有个新增用户的功能,但是用户分为VIP和普通用户,且有两种不同的新增逻辑。那么此时我们就可以先写两种不同的逻辑。除此之外,这样还让逻辑与数据分离开来,我们可以实现逻辑的复用。

当然实际开发中的逻辑可能很复杂,比如两个方法F1,F2都需要两个个逻辑AB,但是F1需要A->B,F2方法需要B->A。这样的我们用刚才的方法也可以实现,源码如下:

1
2
3
4
5
6
7
8
java复制代码public void test(){
Function<Integer,Integer> A=i->i+1;
Function<Integer,Integer> B=i->i*i;
System.out.println("F1:"+B.apply(A.apply(5)));
System.out.println("F2:"+A.apply(B.apply(5)));
}
/** F1:36 */
/** F2:26 */

也很简单呢,但是这还不够复杂,假如我们F1,F2需要四个逻辑ABCD,那我们还这样写就会变得很麻烦了。

2.compose和andThen

compose和andThen可以解决我们的问题。先看compose的源码

1
2
3
4
java复制代码  default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}

compose接收一个Function参数,返回时先用传入的逻辑执行apply,然后使用当前Function的apply。

1
2
3
4
java复制代码default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}

andThen跟compose正相反,先执行当前的逻辑,再执行传入的逻辑。

这样说可能不够直观,我可以换个说法给你看看

compose等价于B.apply(A.apply(5)),而andThen等价于A.apply(B.apply(5))。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public void test(){
Function<Integer,Integer> A=i->i+1;
Function<Integer,Integer> B=i->i*i;
System.out.println("F1:"+B.apply(A.apply(5)));
System.out.println("F1:"+B.compose(A).apply(5));
System.out.println("F2:"+A.apply(B.apply(5)));
System.out.println("F2:"+B.andThen(A).apply(5));
}
/** F1:36 */
/** F1:36 */
/** F2:26 */
/** F2:26 */

我们可以看到上述两个方法的返回值都是一个Function,这样我们就可以使用建造者模式的操作来使用。

1
java复制代码B.compose(A).cpmpose(A).andThen(A).apply(5);

这个操作很简单,你可以自己试试。

本文转载自: 掘金

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

23张图!万字详解「链表」,从小白到大佬!

发表于 2020-11-05

链表和数组是数据类型中两个重要又常用的基础数据类型,数组是连续存储在内存中的数据结构,因此它的优势是可以通过下标迅速的找到元素的位置,而它的缺点则是在插入和删除元素时会导致大量元素的被迫移动,为了解决和平衡此问题于是就有了链表这种数据类型。

链表和数组可以形成有效的互补,这样我们就可以根据不同的业务场景选择对应的数据类型了。那么,本文我们就来重点介绍学习一下链表,一是因为它非常重要,二是因为面试面试必考,先来看本文的大纲:
image.png

看过某些抗日神剧我们都知道,某些秘密组织为了防止组织的成员被“一窝端”,通常会采用上下级单线联系的方式来保护其他成员,而这种“行为”则是链表的主要特征。

本文已收录至 Github《小白学算法》系列:github.com/vipstone/al…

简介

链表(Linked List)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。

链表是由数据域和指针域两部分组成的,它的组成结构如下:
image.png

复杂度分析

由于链表无需按顺序存储,因此链表在插入的时可以达到 O(1) 的复杂度,比顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要 O(n) 的时间,而顺序表插入和查询的时间复杂度分别是 O(log n) 和 O(1)。

优缺点分析

使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。

分类

链表通常会分为以下三类:

  • 单向链表
  • 双向链表
  • 循环链表
    • 单循链表
    • 双循环链表

1.单向链表

链表中最简单的一种是单向链表,或叫单链表,它包含两个域,一个数据域和一个指针域,指针域用于指向下一个节点,而最后一个节点则指向一个空值,如下图所示:
image.png
单链表的遍历方向单一,只能从链头一直遍历到链尾。它的缺点是当要查询某一个节点的前一个节点时,只能再次从头进行遍历查询,因此效率比较低,而双向链表的出现恰好解决了这个问题。

接下来,我们用代码来实现一下单向链表的节点:

1
2
3
4
5
6
7
8
9
java复制代码private static class Node<E> {
E item;
Node<E> next;

Node(E element, Node<E> next) {
this.item = element;
this.next = next;
}
}

2.双向链表

双向链表也叫双面链表,它的每个节点由三部分组成:prev 指针指向前置节点,此节点的数据和 next 指针指向后置节点,如下图所示:

image.png

接下来,我们用代码来实现一下双向链表的节点:

1
2
3
4
5
6
7
8
9
10
11
java复制代码private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;

Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}

3.循环链表

循环链表又分为单循环链表和双循环链表,也就是将单向链表或双向链表的首尾节点进行连接,这样就实现了单循环链表或双循环链表了,如下图所示:
image.png
image.png

Java中的链表

学习了链表的基础知识之后,我们来思考一个问题:Java 中的链表 LinkedList 是属于哪种类型的链表呢?单向链表还是双向链表?

要回答这个问题,首先我们要来看 JDK 中的源码,如下所示:

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
java复制代码package java.util;

import java.util.function.Consumer;

public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
// 链表大小
transient int size = 0;

// 链表头部
transient Node<E> first;

// 链表尾部
transient Node<E> last;

public LinkedList() {
}

public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}

// 获取头部元素
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}

// 获取尾部元素
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}

// 删除头部元素
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}

// 删除尾部元素
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}

// 添加头部元素
public void addFirst(E e) {
linkFirst(e);
}

// 添加头部元素的具体执行方法
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}

// 添加尾部元素
public void addLast(E e) {
linkLast(e);
}

// 添加尾部元素的具体方法
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}

// 查询链表个数
public int size() {
return size;
}

// 清空链表
public void clear() {
for (Node<E> x = first; x != null; ) {
Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
first = last = null;
size = 0;
modCount++;
}

// 根据下标获取元素
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}

private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;

Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
// 忽略其他方法......
}

从上述节点 Node 的定义可以看出:LinkedList 其实是一个双向链表,因为它定义了两个指针 next 和 prev 分别用来指向自己的下一个和上一个节点。

链表常用方法

LinkedList 的设计还是很巧妙的,了解了它的实现代码之后,下面我们来看看它是如何使用的?或者说它的常用方法有哪些。

1.增加

接下来我们来演示一下增加方法的使用:

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class LinkedListTest {
public static void main(String[] a) {
LinkedList list = new LinkedList();
list.add("Java");
list.add("中文");
list.add("社群");
list.addFirst("头部添加"); // 添加元素到头部
list.addLast("尾部添加"); // 添加元素到最后
System.out.println(list);
}
}

以上代码的执行结果为:

[头部添加, Java, 中文, 社群, 尾部添加]

出来以上的 3 个增加方法之外,LinkedList 还包含了其他的添加方法,如下所示:

  • add(int index, E element):向指定位置插入元素;
  • offer(E e):向链表末尾添加元素,返回是否成功;
  • offerFirst(E e):头部插入元素,返回是否成功;
  • offerLast(E e):尾部插入元素,返回是否成功。

add 和 offer 的区别

它们的区别主要体现在以下两点:

  • offer 方法属于 Deque 接口,add 方法属于 Collection 的接口;
  • 当队列添加失败时,如果使用 add 方法会报错,而 offer 方法会返回 false。

2.删除

删除功能的演示代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码import java.util.LinkedList;

public class LinkedListTest {
public static void main(String[] a) {
LinkedList list = new LinkedList();
list.offer("头部");
list.offer("中间");
list.offer("尾部");

list.removeFirst(); // 删除头部元素
list.removeLast(); // 删除尾部元素

System.out.println(list);
}
}

以上代码的执行结果为:

[中间]

除了以上删除方法之外,更多的删除方法如下所示:

  • clear():清空链表;
  • removeFirst():删除并返回第一个元素;
  • removeLast():删除并返回最后一个元素;
  • remove(Object o):删除某一元素,返回是否成功;
  • remove(int index):删除指定位置的元素;
  • poll():删除并返回第一个元素;
  • remove():删除并返回第一个元素。

3.修改

修改方法的演示代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码import java.util.LinkedList;

public class LinkedListTest {
public static void main(String[] a) {
LinkedList list = new LinkedList();
list.offer("Java");
list.offer("MySQL");
list.offer("DB");

// 修改
list.set(2, "Oracle");

System.out.println(list);
}
}

以上代码的执行结果为:

[Java, MySQL, Oracle]

4.查询

查询方法的演示代码如下:

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复制代码import java.util.LinkedList;

public class LinkedListTest {
public static void main(String[] a) {
LinkedList list = new LinkedList();
list.offer("Java");
list.offer("MySQL");
list.offer("DB");

// --- getXXX() 获取 ---
// 获取最后一个
System.out.println(list.getLast());
// 获取首个
System.out.println(list.getFirst());
// 根据下标获取
System.out.println(list.get(1));

// peekXXX() 获取
System.out.println("--- peek() ---");
// 获取最后一个
System.out.println(list.peekLast());
// 获取首个
System.out.println(list.peekFirst());
// 根据首个
System.out.println(list.peek());
}
}

以上代码的执行结果为:

DB

Java

MySQL

— peek() —

DB

Java

Java

5.遍历

LinkedList 的遍历方法包含以下三种。

遍历方法一:

1
2
3
java复制代码for (int size = linkedList.size(), i = 0; i < size; i++) {
System.out.println(linkedList.get(i));
}

遍历方法二:

1
2
3
java复制代码for (String str: linkedList) {
System.out.println(str);
}

遍历方法三:

1
2
3
4
java复制代码Iterator iter = linkedList.iterator();
while (iter.hasNext()) {
System.out.println(iter.next());
}

链表应用:队列 & 栈

1.用链表实现栈

接下来我们用链表来实现一个先进先出的“队列”,实现代码如下:

1
2
3
4
5
6
7
8
9
10
java复制代码LinkedList list = new LinkedList();
// 元素入列
list.add("Java");
list.add("中文");
list.add("社群");

while (!list.isEmpty()) {
// 打印并移除队头元素
System.out.println(list.poll());
}

以上程序的执行结果如下:

Java

中文

社群

image.png

2.用链表实现队列

然后我们用链表来实现一个后进先出的“栈”,实现代码如下:

1
2
3
4
5
6
7
8
9
10
java复制代码LinkedList list = new LinkedList();
// 元素入栈
list.add("Java");
list.add("中文");
list.add("社群");

while (!list.isEmpty()) {
// 打印并移除栈顶元素
System.out.println(list.pollLast());
}

以上程序的执行结果如下:

社群

中文

Java

image.png

链表使用场景

链表作为一种基本的物理结构,常被用来构建许多其它的逻辑结构,如堆栈、队列都可以基于链表实现。

所谓的物理结构是指可以将数据存储在物理空间中,比如数组和链表都属于物理数据结构;而逻辑结构则是用于描述数据间的逻辑关系的,它可以由多种不同的物理结构来实现,比如队列和栈都属于逻辑结构。

链表常见笔试题

链表最常见的笔试题就是链表的反转了,之前的文章《链表反转的两种实现方法,后一种击败了100%的用户!》我们提供了 2 种链表反转的方法,而本文我们再来扩充一下,提供 3 种链表反转的方法。

实现方法 1:Stack

我们先用图解的方式来演示一下,使用栈实现链表反转的具体过程,如下图所示。

image.png

全部入栈:
image.png
因为栈是先进后出的数据结构,因此它的执行过程如下图所示:
image.png
image.png
image.png
最终的执行结果如下图所示:
image.png
实现代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public ListNode reverseList(ListNode head) {
if (head == null) return null;
Stack<ListNode> stack = new Stack<>();
stack.push(head); // 存入第一个节点
while (head.next != null) {
stack.push(head.next); // 存入其他节点
head = head.next; // 指针移动的下一位
}
// 反转链表
ListNode listNode = stack.pop(); // 反转第一个元素
ListNode lastNode = listNode; // 临时节点,在下面的 while 中记录上一个节点
while (!stack.isEmpty()) {
ListNode item = stack.pop(); // 当前节点
lastNode.next = item;
lastNode = item;
}
lastNode.next = null; // 最后一个节点赋为null(不然会造成死循环)
return listNode;
}

LeetCode 验证结果如下图所示:
image.png
可以看出使用栈的方式来实现链表的反转执行的效率比较低。

实现方法 2:递归

同样的,我们先用图解的方式来演示一下,此方法实现的具体过程,如下图所示。

image.png

image.png
image.png
image.png
image.png
实现代码如下所示:

1
2
3
4
5
6
7
8
java复制代码public static ListNode reverseList(ListNode head) {
if (head == null || head.next == null) return head;
// 从下一个节点开始递归
ListNode reverse = reverseList(head.next);
head.next.next = head; // 设置下一个节点的 next 为当前节点
head.next = null; // 把当前节点的 next 赋值为 null,避免循环引用
return reverse;
}

LeetCode 验证结果如下图所示:
image.png
可以看出这种实现方法在执行效率方面已经满足我们的需求了,性能还是很高的。

实现方法 3:循环

我们也可以通过循环的方式来实现链表反转,只是这种方法无需重复调用自身方法,只需要一个循环就搞定了,实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码class Solution {
public ListNode reverseList(ListNode head) {
if (head == null) return null;
// 最终排序的倒序链表
ListNode prev = null;
while (head != null) {
// 循环的下个节点
ListNode next = head.next;
// 反转节点操作
head.next = prev;
// 存储下个节点的上个节点
prev = head;
// 移动指针到下一个循环
head = next;
}
return prev;
}
}

LeetCode 验证结果如下图所示:
image.png
从上述图片可以看出,使用此方法在时间复杂度和空间复杂度上都是目前的最优解,比之前的两种方法更加理想。

总结

本文我们讲了链表的定义,它是由数据域和指针域两部分组成的。链表可分为:单向链表、双向链表和循环链表,其中循环链表又可以分为单循链表和双循环链表。通过 JDK 的源码可知,Java 中的 LinkedList 其实是双向链表,我们可以使用它来实现队列或者栈,最后我们讲了反转链表的 3 种实现方法,希望本文的内容对你有帮助。

文末福利:搜索公众号「Java中文社群」发送“面试”,领取最新的面试资料。

本文转载自: 掘金

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

Java中的微信支付(3):API V3对微信服务器响应进行

发表于 2020-11-04

  1. 前言

牢记一句话:公钥加密,私钥解密;私钥加签,公钥验签。

微信支付V3版本前两篇分别讲了如何对请求做签名和如何获取并刷新微信平台公钥,本篇将继续展开如何对微信支付响应结果的验签。

  1. 为什么要对响应验签

微信支付会在回调的HTTP头部中包括回调报文的签名。商户必须验证响应的签名,保证响应确实来自微信支付服务器,避免中间人攻击。而验证响应签名除了需要微信平台的公钥外还需要从请求头的其它参数。

假设以下就是微信支付服务器的响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
http复制代码HTTP/1.1 200 OK
Server: nginx
Date: Tue, 02 Apr 2019 12:59:40 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2204
Connection: keep-alive
Keep-Alive: timeout=8
Content-Language: zh-CN
Request-ID: e2762b10-b6b9-5108-a42c-16fe2422fc8a
Wechatpay-Nonce: c5ac7061fccab6bf3e254dcf98995b8c
Wechatpay-Signature: CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==
Wechatpay-Timestamp: 1554209980
Wechatpay-Serial: 5157F09EFDC096DE15EBE81A47057A7232F1B8E1
Cache-Control: no-cache, must-revalidate

{"prepay_id":"wx2922034726858082fbd40b511c67630000"}

检查平台证书序列号

微信支付响应的时候会携带一个微信平台证书序列号,从响应头中的Wechatpay-Serial字段中获取值,用来提示我们要使用该序列号的证书来进行验签,如果不存在就需要我们刷新证书,而上一文我们将平台证书序列号和证书以键值对存在HashMap中,我们只需要检查是否存在即可,不存在就刷新。

构造验签名串

从响应结果中获取对应下面方法的三个参数就可以构造出验签名串。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码/**
* 构造验签名串.
*
* @param wechatpayTimestamp HTTP头 Wechatpay-Timestamp 中的应答时间戳。
* @param wechatpayNonce HTTP头 Wechatpay-Nonce 中的应答随机串
* @param body 响应体
* @return the string
*/
public String responseSign(String wechatpayTimestamp, String wechatpayNonce, String body) {
return Stream.of(wechatpayTimestamp, wechatpayNonce, body)
.collect(Collectors.joining("\n", "", "\n"));
}

验证签名

待验证的签名从响应头中的Wechatpay-Signature字段中获取,我们使用微信支付平台公钥对验签名串和签名进行SHA256 with RSA签名验证。

1
2
3
4
5
6
7
8
9
10
java复制代码   // 构造验签名串  
final String signatureStr = responseSign(wechatpayTimestamp, wechatpayNonce, body);
// 加载SHA256withRSA签名器
Signature signer = Signature.getInstance("SHA256withRSA");
// 用微信平台公钥对签名器进行初始化
signer.initVerify(certificate);
// 把我们构造的验签名串更新到签名器中
signer.update(signatureStr.getBytes(StandardCharsets.UTF_8));
// 把请求头中微信服务器返回的签名用Base64解码 并使用签名器进行验证
boolean result = signer.verify(Base64Utils.decodeFromString(wechatpaySignature));

完整的验签代码

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
java复制代码/**
* 我方对响应验签,和应答签名做比较,使用微信平台证书.
*
* @param wechatpaySerial response.headers['Wechatpay-Serial'] 当前使用的微信平台证书序列号
* @param wechatpaySignature response.headers['Wechatpay-Signature'] 微信平台签名
* @param wechatpayTimestamp response.headers['Wechatpay-Timestamp'] 微信服务器的时间戳
* @param wechatpayNonce response.headers['Wechatpay-Nonce'] 微信服务器提供的随机串
* @param body response.body 微信服务器的响应体
* @return the boolean
*/
@SneakyThrows
public boolean responseSignVerify(String wechatpaySerial, String wechatpaySignature, String wechatpayTimestamp, String wechatpayNonce, String body) {

if (CERTIFICATE_MAP.isEmpty() || !CERTIFICATE_MAP.containsKey(wechatpaySerial)) {
refreshCertificate();
}
Certificate certificate = CERTIFICATE_MAP.get(wechatpaySerial);

final String signatureStr = createSign(wechatpayTimestamp, wechatpayNonce, body);
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initVerify(certificate);
signer.update(signatureStr.getBytes(StandardCharsets.UTF_8));

return signer.verify(Base64Utils.decodeFromString(wechatpaySignature));
}

CERTIFICATE_MAP 平台证书容器可参考上一篇文章。

  1. 总结

验签通过就说明我们请求的响应来自微信服务器就可以针对结果进行对应的逻辑处理了,微信支付API无论是V2还是V3都包含了使用Api证书对请求进行加签,对响应结果进行验签的流程,十分考验对密码摘要算法的使用,其它就是组织参数调用Http请求。如果你能够掌握这一能力就会在面试中和工作中占到优势。好了今天分享就到这里,多多关注: 码农小胖哥 获取更多实用的编程干货。

本文转载自: 掘金

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

API接口防止参数篡改和重放攻击

发表于 2020-11-03

 API重放攻击(Replay Attacks)又称为重播攻击、回放攻击。它的原理就是把之前窃听到的数据原封不动的重新发送给接收方。HTTPS并不能防止这种攻击,虽然传输的数据都是经过加密的,窃听者无法得到数据的准确定义,但是可以从请求的接收方地址分析这些数据的作用。比如用户登录请求时攻击者虽然无法窃听密码,但是却可以截获加密后的口令然后将其重放,从而利用这种方式进行有效的攻击。

所谓重放攻击就是攻击者发送一个目的主机已经接收过的包,来达到欺骗系统的目的,主要用于身份认证过程,重放攻击是计算机世界黑客常用的攻击方式之一。

一次HTTP请求,从请求方到接收方中间要经过很多路由器和交换机,攻击者可以在中途截获请求的数据。假设在一个网上存款系统中,一条消息表示用户支取了一笔存款,攻击者完全可以多次发送这条消息而偷窃存款。

重放是二次请求,如果API接口没有做到对应的安全防护,将可能造成很严重的后果。

API接口常见的安全防护要做到主要有以下几点:

  • 防止sql注入
  • 防止xss攻击
  • 防止请求参数被篡改
  • 防止重放攻击

主要防御措施可以归纳为两点:

  • 对请求的合法性进行校验
  • 对请求的数据进行校验

防止重放攻击必须要保证请求仅一次有效。需要通过在请求体重携带当前请求的唯一标识,并且进行签名防止被篡改。所以防止重放攻击需要建立在防止签名被篡改的基础之上。

请求参数防篡改

采用https协议可以将传输的明文进行加密,但是黑客仍然可以截获传输的数据包,进一步伪造请求进行重放攻击。如果黑客使用特殊手段让请求方设备使用了伪造的证书进行通信,那么https加密的内容也会被解密。

在API接口中我们除了使用https协议进行通信外,还需要有自己的一套加解密机制,对请求的参数进行保护,防止被篡改。

过程如下:

  1. 客户端使用约定好的秘钥对传输的参数进行加密,得到签名值signature,并且将签名值也放入请求的参数中,发送请求给服务端
  2. 服务端接收到客户端的请求,然后使用约定好的秘钥对请求的参数(除了signature以外)再次进行签名,得到签名值autograph。
  3. 服务端比对signature和autograph的值,如果对比一致,认定为合法请求。如果对比不一致,说明参数被篡改,认定是合法请求。

因为黑客不知道签名的秘钥,所以即使截获到请求数据,对请求参数进行篡改,但是却无法对参数进行前面,无法得到修改后参数的签名值signature。
签名的秘钥我们可以使用很多方案,可以采用对称加密或者非对称加密

防止重放攻击

基于timestamp的方案

每次HTTP请求,都需要加上timestamp参数,然后把timestamp和其他参数一起进行数字签名。因为一次正常的HTTP氢气u,从发出到达服务器一般都不会超过60s,所以服务器收到HTTP请求之后,首先判断时间戳参数与当前时间比较,是否超过了60s,如果超过了则认为是非法请求。

一般情况下,黑客从抓包重放请求耗时远远超过了60s,所以此时请求中的timestamp参数已经失效了。
如果黑客修改timestamp参数为当前的时间戳,则signature参数对应的数字签名就会失效,因为黑客不知道签名秘钥,没有办法生成新的数字签名。

但是这种方式的漏洞也是显而易见,如果在60s之内进行重放攻击,那就没办法了,所以这种方式不能保证请求仅一次有效。

基于nonce的方案

nonce的意思是仅一次有效的随机字符串,要求每次请求时,该参数要保证不同,所以该参数一般与时间戳有关,我们这里为了方便起见,直接使用时间戳的16进制,实际使用客户加上客户端的ip地址,mac地址等信息做个哈希之后,作为nonce参数。

我们每次将请求的nonce参数存储到一个”集合“中,可以json格式存储到数据库或缓存。

每次处理HTTP请求时,首先判断该请求的nonce是否在该集合中,如果存在则认为是非法请求。

nonce参数在首次请求时,已经被存储到了服务器上的集合中,再次请求会被识别并拒绝。

nonce参数作为数组签名的一部分,是无法篡改的,因为黑客不清楚token,所以不能生成新的sign。

这种方式也有很大的问题,那就是存储nonce参数的集合会越来越大,验证nonce是否在集合中的耗时会越来越长。我们不能让nonce集合无限大,所以需要定期清理该集合,但是一旦该集合被清理,我们就无法验证被清理的nonce参数了。也就是说,假设该集合平均1天清理一次的话,我们抓取到的该url虽然当时无法进行重放攻击,但是我们还是可以每隔一天进行一次重复刚攻击的。而且存储24小时内,所有请求的nonce参数,也是一笔不小的开销。

基于timestamp和nonce的方案

nonce的一次性可以解决timestamp参数60s的问题,timestamp可以解决nonce参数集合越来越大的问题。防止重放攻击一般和防止请求参数被篡改一起做。请求的Headers数据如下图所示。

我们在timestamp方案的基础上,加上nonce参数,因为timestamp参数对于超过60s的请求,都认为是非法请求,所以我们只需要存储60s的nonce参数集合即可。

API接口验证流程:

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
ini复制代码String token = request.getHeader("token");
String timestamp = request.getHeader("timestamp");
String nonceStr = request.getHeader("nonceStr");

String url = request.getHeader("url");

String signature = request.getHeader("signature");


if(StringUtil.isBlank(token) || StringUtil.isBlank(timestamp) || StringUtil.isBlank(nonceStr) || StringUtil.isBlank(url)
|| StringUtil.isBlank(signature))
{
return;
}

UserTokenInfo userTokenInfo = TokenUtil.getUserTokenInfo(token);

if(userTokenInfo == null){
return;
}

if(!request.getRequestURI().equal(url)){
return;
}

if(DateUtil.getSecond()-DateUtil.toSecond(timestamp) > 60){
return;
}

if(RedisUtils.haveNonceStr(userTokenInfo,nonceStr)){
return;
}

String stringB = SignUtil.signature(token, timestamp, nonceStr, url, request);
if(!signature.equals(stringB)){
return;
}
RedisUtils.saveNonceStr(userTokenInfo,nonceStr,60);

微信公众号如何保证消息不会被重放攻击

使用微信公众平台的接口需要在微信公众平台设置token。这里假设token是不会被攻击者知道的,相当于一个PSK(Pre Shared Key)
微信发消息会有三个参数:signature、timestamp和nonce。

验证是否为微信发送的消息流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码$signature = $_GET["signature"];
$timestamp = $_GET["timestamp"];
$nonce = $_GET["nonce"];
$token = TOKEN;
// 按照$token,$timestamp,$nonce的顺序组成数组
$tmpArr = array($token, $timestamp, $nonce);
// 按照字典序排序
sort($tmpArr, SORT_STRING);
// 将排序后的数组串成字符串
$tmpStr = implode( $tmpArr );
// 用sha1计算签名
$tmpStr = sha1( $tmpStr );
// 校验签名
if( $tmpStr == $signature ){
return true;
}else{
return false;
}

这里在使用的时候,这里针对消息的重放攻击的防护应该检查nonce值是否已经存在,如果已经存在可能为非法通知,按道理来讲也应该检查这里timestamp和当前时间比较是否已经过了一定的时间,但是这里在微信公众号开发文档中并没有说明,这里其实应该是需要验证消息是否是否在同一个时间戳内的,如果接收到的消息距离发送的时间已经超过1s就可以直接丢弃了,当然在网络不好的情况下会造成丢消息。

另外一个这里针对重试或者幂等性的要求上要做到解密后消息中去,比如发起重试使用同样的消息id即可。虽然每次的nonce值是不一样的,但是消息的id是一样的,就可以区分出来是否为同一个消息的重试发送。

这里一般的做法是抽出一个前置网关,做消息的解密和加密,后端服务进行消息的处理,消息的处理上对重试带来的幂等性问题做处理就可以了。

本文转载自: 掘金

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

面试时说Redis是单线程的,被喷惨了! Reactor模式

发表于 2020-11-03

Redis是单线程的,这话搁以前,是横着走的,谁都知道的真理。现在不一样,Redis 变了。再说这句话,多少得有质疑的语气来跟你辩驳一番。意志不坚定的,可能就缴械投降,顺着别人走了。

到底是什么样的,各位看官请跟小莱一起往下:

图注:思维导图

Reactor模式

反应器模式,你可能不太认识,如果看过上篇文章的话应该会有点印象。涉及到 Redis 线程它是一个绕不过去的话题。

1、传统阻塞IO模型

在讲反应器模式前,这里有必要提一下传统阻塞IO模型的处理方式。

在传统阻塞IO模型中,由一个独立的 Acceptor 线程来监听客户端的连接,每当有客户端请求过来时,它就会为客户端分配一个新的线程来进行处理。当同时有多个请求过来,服务端对应的就会分配相应数量的线程。这就会导致CPU频繁切换,浪费资源。

有的连接请求过来不做任何事情,但服务端还会分配对应的线程,这样就会造成不必要的线程开销。这就好比你去餐厅吃饭,你拿着菜单看了半天发现真他娘的贵,然后你就走人了。这段时间等你点菜的服务员就相当于一个对应的线程,你要点菜可以看作一个连接请求。

同时,每次建立连接后,当线程调用读写方法时,线程会被阻塞,直到有数据可读可写, 在此期间线程不能做其它事情。还是上边餐厅吃饭的例子,你出去转了一圈发现还是这家性价比最高。回到这家餐厅又拿着菜单看了半天,服务员也在旁边等你点完菜为止。这个过程中服务员什么也不能做,只能这么干等着,这个过程相当于阻塞。

你看这样的方式,每来一个请求就要分配一个线程,并且还得阻塞地等线程处理完。有的请求还只是过来连接下,什么操作也不干,还得为它分配一个线程,对服务器资源要求那得多高啊。遇到高并发场景,不敢想象。对于连接数目比较小的的固定架构倒是可以考虑。

2、伪异步IO模型

你可能了解过一种通过线程池优化的解决方案,采用线程池和任务队列的方式。这种被称作伪异步IO模型。

当有客户端接入时,将客户端的请求封装成一个 task 投递到后端线程池中来处理。线程池维护一个消息队列和多个活跃线程,对消息队列中的任务进行处理。

这种解决方案,避免了为每个请求创建一个线程导致的线程资源耗尽问题。但是底层仍然是同步阻塞模型。如果线程池内的所有线程都阻塞了,那么对于更多请求就无法响应了。因此这种模式会限制最大连接数,并不能从根本上解决问题。

我们继续用上边的餐厅来举例,餐厅老板在经营了一段时间后,顾客多了起来,原本店里的5个服务员一对一服务的话根本对付不过来。于是老板采用5个人线程池的方式。服务员服务完一个客人后立刻去服务另一个。

这时问题出现了,有的客人点菜特别慢,服务员就得等待很长时间,直到客人点完为止。如果5个客人都点的特别慢的话,这5个服务员就得一直等下去,就会导致其余的顾客没有人服务的状态。这就是我们上边所说的线程池所有线程都被阻塞的情况。

那么这种问题该如何解决呢?别急, Reactor 模式就要出场了。

3、Reactor设计模式

Reactor 模式的基本设计思想是基于I/O复用模型来实现的。

这里说下I/O复用模型。和传统IO多线程阻塞不同,I/O复用模型中多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待。当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。

什么意思呢?餐厅老板也发现了顾客点餐慢的问题,于是他采用了一种大胆的方式,只留了一个服务员。当客人点餐的时候,这个服务员就去招待别的客人,客人点好餐后直接喊服务员来进行服务。这里的顾客和服务员可以分别看作多个连接和一个线程。服务员阻塞在一个顾客那里,当有别的顾客点好餐后,她就立刻去服务其他的顾客。

了解了 reactor 的设计思想后,我们再来看下今天的主角单 reactor 单线程的实现方案:

Reactor 通过 I/O复用程序监控客户端请求事件,收到事件后通过任务分派器进行分发。

针对建立连接请求事件,通过 Acceptor 处理,并建立对应的 handler 负责后续业务处理。

针对非连接事件,Reactor 会调用对应的 handler 完成 read->业务处理->write 处理流程,并将结果返回给客户端。

整个过程都在一个线程里完成。

单线程时代

了解了 Reactor 模式后,你可能会有一个疑问,这个和我们今天的主题有什么关系呢。可能你不知道的是,Redis 是基于 Reactor 单线程模式来实现的。

IO多路复用程序接收到用户的请求后,全部推送到一个队列里,交给文件分派器。对于后续的操作,和在 reactor 单线程实现方案里看到的一样,整个过程都在一个线程里完成,因此 Redis 被称为是单线程的操作。

对于单线程的 Redis 来说,基于内存,且命令操作时间复杂度低,因此读写速率是非常快的。

多线程时代

Redis6 版本中引入了多线程。上边已经提到过 Redis 单线程处理有着很快的速度,那为什么还要引入多线程呢?单线程的瓶颈在什么地方?

我们先来看第二个问题,在 Redis 中,单线程的性能瓶颈主要在网络IO操作上。也就是在读写网络 read/write 系统调用执行期间会占用大部分 CPU 时间。如果你要对一些大的键值对进行删除操作的话,在短时间内是删不完的,那么对于单线程来说就会阻塞后边的操作。

回想下上边讲得 Reactor 模式中单线程的处理方式。针对非连接事件,Reactor 会调用对应的 handler 完成 read->业务处理->write 处理流程,也就是说这一步会造成性能上的瓶颈。

Redis 在设计上采用将网络数据读写和协议解析通过多线程的方式来处理,对于命令执行来说,仍然使用单线程操作。

总结

Reactor模式

  • 传统阻塞IO模型客户端与服务端线程1:1分配,不利于进行扩展。
  • 伪异步IO模型采用线程池方式,但是底层仍然使用同步阻塞方式,限制了最大连接数。
  • Reactor 通过 I/O复用程序监控客户端请求事件,通过任务分派器进行分发。

单线程时代

  • 基于 Reactor 单线程模式实现,通过IO多路复用程序接收到用户的请求后,全部推送到一个队列里,交给文件分派器进行处理。

多线程时代

  • 单线程性能瓶颈主要在网络IO上。
  • 将网络数据读写和协议解析通过多线程的方式来处理 ,对于命令执行来说,仍然使用单线程操作。

关于作者

作者:大家好,我是莱乌,BAT搬砖工一枚。从小公司进入大厂,一路走来收获良多,想将这些经验分享给有需要的人,因此创建了公众号「IT界农民工」。定时更新,希望能帮助到你。

本文转载自: 掘金

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

基于MidwayJs开发前后端分离的权限管理系统(开源)

发表于 2020-11-02

sf-midway-admin

基于MidwayJs + TypeScript + TypeORM + Redis + MySql + Vue + Element-UI编写的一款简单高效的前后端分离的权限管理系统。希望这个项目在全栈的路上能够帮助到你。

前端项目地址:传送门
后端项目地址:传送门

演示地址

  • opensource.admin.si-yee.com

演示环境账号密码:

账号 密码 权限
openadmin 123456 仅只有各个功能的查询权限
monitoradmin 123456 系统监控页面及按钮权限

本地部署账号密码:

账号 密码 权限
rootadmin 123456 超级管理员

系统模块

1
2
3
4
5
6
7
8
9
10
11
bash复制代码├─系统管理
│ ├─用户管理
│ ├─角色管理
│ ├─菜单管理
├─系统监控
│ ├─在线用户
│ ├─登录日志
│ ├─请求追踪
├─任务调度
│ ├─定时任务
│ └─任务日志

系统特点

  • 前后端请求参数校验
  • JWT 认证
  • 基于 MidwayJs 框架,内置了基础的中间件支持(用户认证、访问日志、请求追踪等)
  • 用户权限动态刷新
  • 代码简单,结构清晰

技术选型

后端

  • MidwayJs + TypeScript
  • TypeORM(MYSQL)
  • ioredis(Redis)
  • bull(队列)

前端

  • Vue、Vue-Router、VueX
  • Element-UI

本地开发

初始化数据库,以及服务启动

新建数据库并导入数据库脚本,文件位于 sql/init.sql,确保MySql版本>=5.7

修改数据库配置信息,在src/config/config.${env}.ts目录下更改对应模式下的配置

内置swagger文档,启动运行项目后访问:http://127.0.0.1:7001/swagger-ui/index.html即可

参考对应配置请参考:config.local.ts

运行项目

1
2
3
4
bash复制代码$ git clone https://github.com/hackycy/sf-midway-admin.git
$ cd sf-midway-admin
$ npm i
$ npm run dev

系统截图

1.png

2.png

3.png

4.png

5.png

项目部署

执行

1
2
arduino复制代码$ npm run build
$ npm run start

反向代理配置示例

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
conf复制代码server
{
# ... 省略

# 请添加以下配置
location / {
try_files $uri $uri/ /index.html;
}

location /api/
{
proxy_pass http://127.0.0.1:7001/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;

#缓存相关配置
#proxy_cache cache_one;
#proxy_cache_key $host$request_uri$is_args$args;
#proxy_cache_valid 200 304 301 302 1h;

#持久化连接相关配置
proxy_connect_timeout 3000s;
proxy_read_timeout 86400s;
proxy_send_timeout 3000s;
#proxy_http_version 1.1;
#proxy_set_header Upgrade $http_upgrade;
#proxy_set_header Connection "upgrade";

add_header X-Cache $upstream_cache_status;

#expires 12h;
}

# ... 省略
}

环境要求

  • Node.js 12.x+
  • Typescript 2.8+
  • MYSQL 5.7+
  • Redis 6.0+

欢迎Star && PR

如果项目有帮助到你可以点个Star支持下。有更好的实现欢迎PR。

致谢

  • vue-element-admin

本文转载自: 掘金

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

Arthas-Java 线上问题排查利器

发表于 2020-11-02

前言

  • 工欲善其事,必先利其器。而手握利器,工作便可游刃有余。
  • 线上问题大家都不希望遇到,但是它又总是无法避免的。而由于线上环境有诸多限制,比如传统的添加日志代码重新发布程序一般是不允许的。此时,最痛苦的莫过于因为缺少关键的日志信息导致问题排查受阻。
  • 下面分享一个阿里开源的 Java 诊断工具——Arthas。相信认识它之后,大家会有相见恨晚的感觉。

Arthas 的故事

  • 首先,附上 Arthas 的GitHub 地址:github.com/alibaba/art…
  • 博主初次遇到 Arthas,应该是在去年的春天,而博主在夏天写了一份回忆录:传送门
  • 最近,有朋友说靓仔就应该来掘金写博主,所以博主来了。恰巧,今天又用到了 Arthas,所以就有了这篇博客。
  • 博主近期遇到一个问题,有一个线上系统突然间无法发送微信模板消息。曾一度怀疑是因为有人修改了微信公众号的配置信息,但经过多次校验后却定位到了一个函数。为了验证猜想,引入了 arthas 来观察这个函数的调用情况:入参、调用者、返回结果。

借助 Arthas 排查线上问题

  • 在官网下载程序包: arthas-boot.jar
  • 上传到生产服务器,启动 arthas:
    • java -jar arthas-boot.jar 123178
    • 这里的 123178 是指进程 PID
  • 不知道如何使用 arthas 也不需要担心,输入命令:help
    • 即可看到 arthas 的命令说明列表
    • 由于当前的需求是观察某个函数(方法)的调用情况,所以应该选择 watch 命令
  • 不知道如何使用 watch 也不需要担心,输入命令:watch --help查看帮助文档
    • 从帮助文档中,可以看到很多使用示例:
    • 当前的需求可以参考画圈的部分示例
  • 观察获取微信公众平台 token 的函数调用情况:
    • 果然,和猜想的一样。这个获取 token 的确实有问题。而由于这里的 token 获取失败,影响到所有需要 token 作为身份校验的微信公众号接口的调用。

小结

  • 在生产环境,很多平常非常好用的可视化工具由于诸多限制没有办法使用,此时能帮助我们的反而是平常被忽视的小巧玲珑的命令行工具,比如 arthas ,希望小伙伴们也喜欢它。

本文转载自: 掘金

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

1…769770771…956

开发者博客

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