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

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


  • 首页

  • 归档

  • 搜索

表妹,OAuth2 vs JWT,到底怎么选?

发表于 2021-11-17

本文会详细描述两种通用的保证API安全性的方法:OAuth2和JSON Web Token (JWT)

假设:

  • 你已经或者正在实现API;
  • 你正在考虑选择一个合适的方法保证API的安全性;

JWT和OAuth2比较?

要比较JWT和OAuth2?首先要明白一点就是,这两个根本没有可比性,是两个完全不同的东西。

  • JWT是一种认证协议 JWT提供了一种用于发布接入令牌(Access Token),并对发布的签名接入令牌进行验证的方法。令牌(Token)本身包含了一系列声明,应用程序可以根据这些声明限制用户对资源的访问。
  • OAuth2是一种授权框架 另一方面,OAuth2是一种授权框架,提供了一套详细的授权机制(指导)。用户或应用可以通过公开的或私有的设置,授权第三方应用访问特定资源。

既然JWT和OAuth2没有可比性,为什么还要把这两个放在一起说呢?实际中确实会有很多人拿JWT和OAuth2作比较。标题里把这两个放在一起,确实有误导的意思。很多情况下,在讨论OAuth2的实现时,会把JSON Web Token作为一种认证机制使用。这也是为什么他们会经常一起出现。

先来搞清楚JWT和OAuth2究竟是干什么的~

JSON Web Token (JWT)

JWT在标准中是这么定义的:

JSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is digitally signed using JSON Web Signature (JWS). -RFC7519 tools.ietf.org/html/rfc751…

JWT是一种安全标准。基本思路就是用户提供用户名和密码给认证服务器,服务器验证用户提交信息信息的合法性;如果验证成功,会产生并返回一个Token(令牌),用户可以使用这个token访问服务器上受保护的资源。

一个token的例子:

1
复制代码eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

一个token包含三部分:

1
css复制代码header.claims.signature

了安全的在url中使用,所有部分都 base64 URL-safe进行编码处理。

Header头部分头部分简单声明了类型(JWT)以及产生签名所使用的算法。

1
json复制代码{  "alg" : "AES256",  "typ" : "JWT"}

Claims声明

声明部分是整个token的核心,表示要发送的用户详细信息。有些情况下,我们很可能要在一个服务器上实现认证,然后访问另一台服务器上的资源;或者,通过单独的接口来生成token,token被保存在应用程序客户端(比如浏览器)使用。一个简单的声明(claim)的例子:

1
json复制代码{  "sub": "1234567890",  "name": "John Doe",  "admin": true}

Signature签名

签名的目的是为了保证上边两部分信息不被篡改。如果尝试使用Bas64对解码后的token进行修改,签名信息就会失效。一般使用一个私钥(private key)通过特定算法对Header和Claims进行混淆产生签名信息,所以只有原始的token才能于签名信息匹配。

这里有一个重要的实现细节。只有获取了私钥的应用程序(比如服务器端应用)才能完全认证token包含声明信息的合法性。所以,永远不要把私钥信息放在客户端(比如浏览器)。

OAuth2是什么?

相反,OAuth2不是一个标准协议,而是一个安全的授权框架。它详细描述了系统中不同角色、用户、服务前端应用(比如API),以及客户端(比如网站或移动App)之间怎么实现相互认证。

The OAuth 2.0 authorization framework enables a third-party application to obtain limited access to an HTTP service, either on behalf of a resource owner by orchestrating an approval interaction between the resource owner and the HTTP service, or by allowing the third-party application to obtain access on its own behalf. -RFC6749 tools.ietf.org/html/rfc674…

这里简单说一下涉及到的基本概念。

Roles角色应用程序或者用户都可以是下边的任何一种角色:

  • 资源拥有者
  • 资源服务器
  • 客户端应用
  • 认证服务器

Client Types客户端类型这里的客户端主要指API的使用者。它可以是的类型:

  • 私有的
  • 公开的

Client Profile客户端描述OAuth2框架也指定了集中客户端描述,用来表示应用程序的类型:

  • Web应用
  • 用户代理
  • 原声应用

Authorization Grants认证授权认证授权代表资源拥有者授权给客户端应用程序的一组权限,可以是下边几种形式:

  • 授权码
  • 隐式授权
  • 资源拥有者密码证书
  • 客户端证书
  • Endpoints终端

OAuth2框架需要下边几种终端:

  • 认证终端
  • Token终端
  • 重定向终端

从上边这些应该可以看出,OAuth2定义了一组相当复杂的规范。

使用HTTPS保护用户密码

在进一步讨论OAuth2和JWT的实现之前,有必要说一下,两种方案都需要SSL安全保护,也就是对要传输的数据进行加密编码。

安全地传输用户提供的私密信息,在任何一个安全的系统里都是必要的。否则任何人都可以通过侵入私人wifi,在用户登录的时候窃取用户的用户名和密码等信息。

一些重要的实施考虑在做选择之前,参考一下下边提到的几点。

时间投入OAuth2是一个安全框架,描述了在各种不同场景下,多个应用之间的授权问题。有海量的资料需要学习,要完全理解需要花费大量时间。甚至对于一些有经验的开发工程师来说,也会需要大概一个月的时间来深入理解OAuth2。这是个很大的时间投入。相反,JWT是一个相对轻量级的概念。可能花一天时间深入学习一下标准规范,就可以很容易地开始具体实施。

出现错误的风险OAuth2不像JWT一样是一个严格的标准协议,因此在实施过程中更容易出错。尽管有很多现有的库,但是每个库的成熟度也不尽相同,同样很容易引入各种错误。在常用的库中也很容易发现一些安全漏洞。当然,如果有相当成熟、强大的开发团队来持续OAuth2实施和维护,可以一定成都上避免这些风险。

社交登录的好处在很多情况下,使用用户在大型社交网站的已有账户来认证会方便。如果期望你的用户可以直接使用Facebook或者Gmail之类的账户,使用现有的库会方便得多。

结论

做结论前,我们先来列举一下 JWT和OAuth2的主要使用场景。

JWT使用场景

无状态的分布式API

JWT的主要优势在于使用无状态、可扩展的方式处理应用中的用户会话。服务端可以通过内嵌的声明信息,很容易地获取用户的会话信息,而不需要去访问用户或会话的数据库。

在一个分布式的面向服务的框架中,这一点非常有用。 但是,如果系统中需要使用黑名单实现长期有效的token刷新机制,这种无状态的优势就不明显了。

优势

  • 快速开发
  • 不需要cookie
  • JSON在移动端的广泛应用
  • 不依赖于社交登录
  • 相对简单的概念理解

限制

  • Token有长度限制
  • Token不能撤销
  • 需要token有失效时间限制(exp)
  • OAuth2使用场景

在作者看来两种比较有必要使用OAuth2的场景:

外包认证服务器

上边已经讨论过,如果不介意API的使用依赖于外部的第三方认证提供者,你可以简单地把认证工作留给认证服务商去做。 也就是常见的,去认证服务商(比如facebook)那里注册你的应用,然后设置需要访问的用户信息,比如电子邮箱、姓名等。

当用户访问站点的注册页面时,会看到连接到第三方提供商的入口。用户点击以后被重定向到对应的认证服务商网站,获得用户的授权后就可以访问到需要的信息,然后重定向回来。

推荐一个 Spring Boot 基础教程及实战示例:

github.com/javastacks/…

优势

  • 快速开发
  • 实施代码量小
  • 维护工作减少
大型企业解决方案

如果设计的API要被不同的App使用,并且每个App使用的方式也不一样,使用OAuth2是个不错的选择。

考虑到工作量,可能需要单独的团队,针对各种应用开发完善、灵活的安全策略。当然需要的工作量也比较大!这一点,OAuth2的作者也指出过:

To be clear, OAuth 2.0 at the hand of a developer with deep understanding of web security will likely result is a secure implementation. However, at the hands of most developers – as has been the experience from the past two years – 2.0 is likely to produce insecure implementations. hueniverse - OAuth 2.0 and the Road to Hell

优势

  • 灵活的实现方式
  • 可以和JWT同时使用
  • 可针对不同应用扩展

本文转载自: 掘金

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

面渣逆袭:线程池夺命连环十八问 1 什么是线程池? 2

发表于 2021-11-17

大家好,我是老三,很高兴又和大家见面。线程池是面试必问的知识点,这节我们来对线面试官,搞透线程池。

  1. 什么是线程池?

线程池: 简单理解,它就是一个管理线程的池子。

  • 它帮我们管理线程,避免增加创建线程和销毁线程的资源损耗。因为线程其实也是一个对象,创建一个对象,需要经过类加载过程,销毁一个对象,需要走GC垃圾回收流程,都是需要资源开销的。
  • 提高响应速度。 如果任务到达了,相对于从线程池拿线程,重新去创建一条线程执行,速度肯定慢很多。
  • 重复利用。 线程用完,再放回池子,可以达到重复利用的效果,节省资源。
  1. 能说说工作中线程池的应用吗?

之前我们有一个和第三方对接的需求,需要向第三方推送数据,引入了多线程来提升数据推送的效率,其中用到了线程池来管理线程。

业务示例

主要代码如下:

主要代码

完整可运行代码地址:gitee.com/fighter3/th…

线程池的参数如下:

  • corePoolSize:线程核心参数选择了CPU数×2
  • maximumPoolSize:最大线程数选择了和核心线程数相同
  • keepAliveTime:非核心闲置线程存活时间直接置为0
  • unit:非核心线程保持存活的时间选择了 TimeUnit.SECONDS 秒
  • workQueue:线程池等待队列,使用 LinkedBlockingQueue阻塞队列

同时还用了synchronized 来加锁,保证数据不会被重复推送:

1
java复制代码  synchronized (PushProcessServiceImpl.class) {}

ps:这个例子只是简单地进行了数据推送,实际上还可以结合其他的业务,像什么数据清洗啊、数据统计啊,都可以套用。

3.能简单说一下线程池的工作流程吗?

用一个通俗的比喻:

有一个营业厅,总共有六个窗口,现在开放了三个窗口,现在有三个窗口坐着三个营业员小姐姐在营业。

老三去办业务,可能会遇到什么情况呢?

  1. 老三发现有空间的在营业的窗口,直接去找小姐姐办理业务。

直接办理

  1. 老三发现没有空闲的窗口,就在排队区排队等。

排队等待

  1. 老三发现没有空闲的窗口,等待区也满了,蚌埠住了,经理一看,就让休息的小姐姐赶紧回来上班,等待区号靠前的赶紧去新窗口办,老三去排队区排队。小姐姐比较辛苦,假如一段时间发现他们可以不用接着营业,经理就让她们接着休息。

排队区满

  1. 老三一看,六个窗口都满了,等待区也没位置了。老三急了,要闹,经理赶紧出来了,经理该怎么办呢?

等待区,排队区都满

  1. 我们银行系统已经瘫痪
  2. 谁叫你来办的你找谁去
  3. 看你比较急,去队里加个塞
  4. 今天没办法,不行你看改一天

上面的这个流程几乎就跟 JDK 线程池的大致流程类似,

  1. 营业中的 3个窗口对应核心线程池数:corePoolSize
  2. 总的营业窗口数6对应:maximumPoolSize
  3. 打开的临时窗口在多少时间内无人办理则关闭对应:unit
  4. 排队区就是等待队列:workQueue
  5. 无法办理的时候银行给出的解决方法对应:RejectedExecutionHandler
  6. threadFactory 该参数在 JDK 中是 线程工厂,用来创建线程对象,一般不会动。

所以我们线程池的工作流程也比较好理解了:

  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
  • 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
  • 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
  • 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
  • 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会根据拒绝策略来对应处理。

线程池执行流程

  1. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  2. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

4.线程池主要参数有哪些?

  1. corePoolSize

此值是用来初始化线程池中核心线程数,当线程池中线程池数< corePoolSize时,系统默认是添加一个任务才创建一个线程池。当线程数 = corePoolSize时,新任务会追加到workQueue中。

  1. maximumPoolSize

maximumPoolSize表示允许的最大线程数 = (非核心线程数+核心线程数),当BlockingQueue也满了,但线程池中总线程数 < maximumPoolSize时候就会再次创建新的线程。

  1. keepAliveTime

非核心线程 =(maximumPoolSize - corePoolSize ) ,非核心线程闲置下来不干活最多存活时间。

  1. unit

线程池中非核心线程保持存活的时间的单位

  • TimeUnit.DAYS; 天
  • TimeUnit.HOURS; 小时
  • TimeUnit.MINUTES; 分钟
  • TimeUnit.SECONDS; 秒
  • TimeUnit.MILLISECONDS; 毫秒
  • TimeUnit.MICROSECONDS; 微秒
  • TimeUnit.NANOSECONDS; 纳秒
  1. workQueue

线程池等待队列,维护着等待执行的Runnable对象。当运行当线程数= corePoolSize时,新的任务会被添加到workQueue中,如果workQueue也满了则尝试用非核心线程执行任务,等待队列应该尽量用有界的。

  1. threadFactory

创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等。

  1. handler

corePoolSize、workQueue、maximumPoolSize都不可用的时候执行的饱和策略。

5.线程池的拒绝策略有哪些?

类比前面的例子,无法办理业务时的处理方式,帮助记忆:

四种策略

  • AbortPolicy :直接抛出异常,默认使用此策略
  • CallerRunsPolicy:用调用者所在的线程来执行任务
  • DiscardOldestPolicy:丢弃阻塞队列里最老的任务,也就是队列里靠前的任务
  • DiscardPolicy :当前任务直接丢弃

想实现自己的拒绝策略,实现RejectedExecutionHandler接口即可。

6.线程池有哪几种工作队列?

常用的阻塞队列主要有以下几种:

  • ArrayBlockingQueue:ArrayBlockingQueue(有界队列)是一个用数组实现的有界阻塞队列,按FIFO排序量。
  • LinkedBlockingQueue:LinkedBlockingQueue(可设置容量队列)是基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool线程池使用了这个队列
  • DelayQueue:DelayQueue(延迟队列)是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。
  • PriorityBlockingQueue:PriorityBlockingQueue(优先级队列)是具有优先级的无界阻塞队列
  • SynchronousQueue:SynchronousQueue(同步队列)是一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool线程池使用了这个队列。

7.线程池提交execute和submit有什么区别?

  1. execute 用于提交不需要返回值的任务
1
2
3
4
java复制代码threadsPool.execute(new Runnable() { 
@Override public void run() {
// TODO Auto-generated method stub }
});
  1. submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个 future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值
1
2
3
4
5
6
7
8
java复制代码Future<Object> future = executor.submit(harReturnValuetask); 
try { Object s = future.get(); } catch (InterruptedException e) {
// 处理中断异常
} catch (ExecutionException e) {
// 处理无法执行任务异常
} finally {
// 关闭线程池 executor.shutdown();
}

8.线程池怎么关闭知道吗?

可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。

  1. shutdown() 将线程池状态置为shutdown,并不会立即停止:
  1. 停止接收外部submit的任务
  2. 内部正在跑的任务和队列里等待的任务,会执行完
  3. 等到第二步完成后,才真正停止
  1. shutdownNow() 将线程池状态置为stop。一般会立即停止,事实上不一定:
  1. 和shutdown()一样,先停止接收外部提交的任务
  2. 忽略队列里等待的任务
  3. 尝试将正在跑的任务interrupt中断
  4. 返回未执行的任务列表

shutdown 和shutdownnow简单来说区别如下:

shutdownNow()能立即停止线程池,正在跑的和正在等待的任务都停下了。这样做立即生效,但是风险也比较大。shutdown()只是关闭了提交通道,用submit()是无效的;而内部的任务该怎么跑还是怎么跑,跑完再彻底停止线程池。

9.线程池的线程数应该怎么配置?

线程在Java中属于稀缺资源,线程池不是越大越好也不是越小越好。任务分为计算密集型、IO密集型、混合型。

  1. 计算密集型:大部分都在用CPU跟内存,加密,逻辑操作业务处理等。
  2. IO密集型:数据库链接,网络通讯传输等。
  1. 计算密集型一般推荐线程池不要过大,一般是CPU数 + 1,+1是因为可能存在页缺失(就是可能存在有些数据在硬盘中需要多来一个线程将数据读入内存)。如果线程池数太大,可能会频繁的 进行线程上下文切换跟任务调度。获得当前CPU核心数代码如下:
1
java复制代码Runtime.getRuntime().availableProcessors();
  1. IO密集型:线程数适当大一点,机器的Cpu核心数*2。
  2. 混合型:可以考虑根绝情况将它拆分成CPU密集型和IO密集型任务,如果执行时间相差不大,拆分可以提升吞吐量,反之没有必要。

当然,实际应用中没有固定的公式,需要结合测试和监控来进行调整。

10.有哪几种常见的线程池?

主要有四种,都是通过工具类Excutors创建出来的,阿里巴巴《Java开发手册》里禁止使用这种方式来创建线程池。

  • newFixedThreadPool (固定数目线程的线程池)
  • newCachedThreadPool (可缓存线程的线程池)
  • newSingleThreadExecutor (单线程的线程池)
  • newScheduledThreadPool (定时及周期执行的线程池)

11.能说一下四种常见线程池的原理吗?

前三种线程池的构造直接调用ThreadPoolExecutor的构造方法。

newSingleThreadExecutor

1
2
3
4
5
6
7
java复制代码  public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}

线程池特点

  • 核心线程数为1
  • 最大线程数也为1
  • 阻塞队列是无界队列LinkedBlockingQueue,可能会导致OOM
  • keepAliveTime为0

SingleThreadExecutor运行流程

工作流程:

  • 提交任务
  • 线程池是否有一条线程在,如果没有,新建线程执行任务
  • 如果有,将任务加到阻塞队列
  • 当前的唯一线程,从队列取任务,执行完一个,再继续取,一个线程执行任务。

适用场景

适用于串行执行任务的场景,一个任务一个任务地执行。

newFixedThreadPool

1
2
3
4
5
6
java复制代码  public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}

线程池特点:

  • 核心线程数和最大线程数大小一样
  • 没有所谓的非空闲时间,即keepAliveTime为0
  • 阻塞队列为无界队列LinkedBlockingQueue,可能会导致OOM

FixedThreadPool

工作流程:

  • 提交任务
  • 如果线程数少于核心线程,创建核心线程执行任务
  • 如果线程数等于核心线程,把任务添加到LinkedBlockingQueue阻塞队列
  • 如果线程执行完任务,去阻塞队列取任务,继续执行。

使用场景

FixedThreadPool 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。

newCachedThreadPool

1
2
3
4
5
6
java复制代码   public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}

线程池特点:

  • 核心线程数为0
  • 最大线程数为Integer.MAX_VALUE,即无限大,可能会因为无限创建线程,导致OOM
  • 阻塞队列是SynchronousQueue
  • 非核心线程空闲存活时间为60秒

当提交任务的速度大于处理任务的速度时,每次提交一个任务,就必然会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。

CachedThreadPool执行流程

工作流程:

  • 提交任务
  • 因为没有核心线程,所以任务直接加到SynchronousQueue队列。
  • 判断是否有空闲线程,如果有,就去取出任务执行。
  • 如果没有空闲线程,就新建一个线程执行。
  • 执行完任务的线程,还可以存活60秒,如果在这期间,接到任务,可以继续活下去;否则,被销毁。

适用场景

用于并发执行大量短期的小任务。

newScheduledThreadPool

1
2
3
4
java复制代码    public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}

线程池特点

  • 最大线程数为Integer.MAX_VALUE,也有OOM的风险
  • 阻塞队列是DelayedWorkQueue
  • keepAliveTime为0
  • scheduleAtFixedRate() :按某种速率周期执行
  • scheduleWithFixedDelay():在某个延迟后执行

ScheduledThreadPool执行流程

工作机制

  • 线程从DelayQueue中获取已到期的ScheduledFutureTask(DelayQueue.take())。到期任务是指ScheduledFutureTask的time大于等于当前时间。
  • 线程执行这个ScheduledFutureTask。
  • 线程修改ScheduledFutureTask的time变量为下次将要被执行的时间。
  • 线程把这个修改time之后的ScheduledFutureTask放回DelayQueue中(DelayQueue.add())。

ScheduledThreadPoolExecutor执行流程

使用场景

周期性执行任务的场景,需要限制线程数量的场景

12.使用无界队列的线程池会导致什么问题吗?

例如newFixedThreadPool使用了无界的阻塞队列LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长,会导致队列的任务越积越多,导致机器内存使用不停飙升,最终导致OOM。

13.线程池异常怎么处理知道吗?

在使用线程池处理任务的时候,任务代码可能抛出RuntimeException,抛出异常后,线程池可能捕获它,也可能创建一个新的线程来代替异常的线程,我们可能无法感知任务出现了异常,因此我们需要考虑线程池异常情况。

常见的异常处理方式:

线程池异常处理

14.能说一下线程池有几种状态吗?

线程池有这几个状态:RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED。

1
2
3
4
5
6
java复制代码   //线程池状态
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;

线程池各个状态切换图:

线程池状态切换图

RUNNING

  • 该状态的线程池会接收新任务,并处理阻塞队列中的任务;
  • 调用线程池的shutdown()方法,可以切换到SHUTDOWN状态;
  • 调用线程池的shutdownNow()方法,可以切换到STOP状态;

SHUTDOWN

  • 该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
  • 队列为空,并且线程池中执行的任务也为空,进入TIDYING状态;

STOP

  • 该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
  • 线程池中执行的任务为空,进入TIDYING状态;

TIDYING

  • 该状态表明所有的任务已经运行终止,记录的任务数量为0。
  • terminated()执行完毕,进入TERMINATED状态

TERMINATED

  • 该状态表示线程池彻底终止

15.线程池如何实现参数的动态修改?

线程池提供了几个 setter方法来设置线程池的参数。

JDK 线程池参数设置接口来源参考[7]

这里主要有两个思路:

动态修改线程池参数

  • 在我们微服务的架构下,可以利用配置中心如Nacos、Apollo等等,也可以自己开发配置中心。业务服务读取线程池配置,获取相应的线程池实例来修改线程池的参数。
  • 如果限制了配置中心的使用,也可以自己去扩展ThreadPoolExecutor,重写方法,监听线程池参数变化,来动态修改线程池参数。

16.线程池调优了解吗?

线程池配置没有固定的公式,通常事前会对线程池进行一定评估,常见的评估方案如下:

线程池评估方案 来源参考[7]

上线之前也要进行充分的测试,上线之后要建立完善的线程池监控机制。

事中结合监控告警机制,分析线程池的问题,或者可优化点,结合线程池动态参数配置机制来调整配置。

事后要注意仔细观察,随时调整。

线程池调优

具体的调优案例可以查看参考[7]美团技术博客。

17.你能设计实现一个线程池吗?

⭐这道题在阿里的面试中出现频率比较高

线程池实现原理可以查看 要是以前有人这么讲线程池,我早就该明白了! ,当然,我们自己实现, 只需要抓住线程池的核心流程-参考[6]:

线程池主要实现流程

我们自己的实现就是完成这个核心流程:

  • 线程池中有N个工作线程
  • 把任务提交给线程池运行
  • 如果线程池已满,把任务放入队列
  • 最后当有空闲时,获取队列中任务来执行

实现代码[6]:

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

//记录线程池中线程数量
private final AtomicInteger ctl = new AtomicInteger(0);

//核心线程数
private volatile int corePoolSize;
//最大线程数
private volatile int maximumPoolSize;

//阻塞队列
private final BlockingQueue<Runnable> workQueue;

public MyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, BlockingQueue<Runnable> workQueue) {
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
}

/**
* 执行
*
* @param command
*/
@Override
public void execute(Runnable command) {
//工作线程数
int c = ctl.get();
//小于核心线程数
if (c < corePoolSize) {
//添加任务失败
if (!addWorker(command)) {
//执行拒绝策略
reject();
}
return;
}
//任务队列添加任务
if (!workQueue.offer(command)) {
//任务队列满,尝试启动线程添加任务
if (!addWorker(command)) {
reject();
}
}
}

/**
* 饱和拒绝
*/
private void reject() {
//直接抛出异常
throw new RuntimeException("Can not execute!ctl.count:"
+ ctl.get() + "workQueue size:" + workQueue.size());
}

/**
* 添加任务
*
* @param firstTask
* @return
*/
private boolean addWorker(Runnable firstTask) {
if (ctl.get() >= maximumPoolSize) return false;
Worker worker = new Worker(firstTask);
//启动线程
worker.thread.start();
ctl.incrementAndGet();
return true;
}

/**
* 线程池工作线程包装类
*/
private final class Worker implements Runnable {
final Thread thread;
Runnable firstTask;

public Worker(Runnable firstTask) {
this.thread = new Thread(this);
this.firstTask = firstTask;
}

@Override
public void run() {
Runnable task = firstTask;
try {
//执行任务
while (task != null || (task = getTask()) != null) {
task.run();
//线程池已满,跳出循环
if (ctl.get() > maximumPoolSize) {
break;
}
task = null;
}
} finally {
//工作线程数增加
ctl.decrementAndGet();
}
}

/**
* 从队列中获取任务
*
* @return
*/
private Runnable getTask() {
for (; ; ) {
try {
System.out.println("workQueue size:" + workQueue.size());
return workQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

//测试
public static void main(String[] args) {
MyThreadPoolExecutor myThreadPoolExecutor = new MyThreadPoolExecutor(2, 2,
new ArrayBlockingQueue<Runnable>(10));
for (int i = 0; i < 10; i++) {
int taskNum = i;
myThreadPoolExecutor.execute(() -> {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务编号:" + taskNum);
});
}
}
}

这样,一个实现了线程池主要流程的类就完成了。

18.单机线程池执行断电了应该怎么处理?

我们可以对正在处理和阻塞队列的任务做事务管理或者对阻塞队列中的任务持久化处理,并且当断电或者系统崩溃,操作无法继续下去的时候,可以通过回溯日志的方式来撤销正在处理的已经执行成功的操作。然后重新执行整个阻塞队列。

也就是:阻塞队列持久化;正在处理任务事务控制;断电之后正在处理任务的回滚,通过日志恢复该次操作;服务器重启后阻塞队列中的数据再加载。


参考:

[1]. 《Java并发编程的艺术》

[2]. 《Java发编程实战》

[3]. 讲真 这次绝对让你轻松学习线程池

[4]. 面试必备:Java线程池解析

[5]. 面试官问:“在项目中用过多线程吗?”你就把这个案例讲给他听!

[6]. 小傅哥 《Java面经手册》

[7]. Java线程池实现原理及其在美团业务中的实践


文章首首发个人公众号三分恶,欢迎关注!

本文转载自: 掘金

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

SSIS学习使用十:高级事件行为 翻译参考 关于SSIS任务

发表于 2021-11-17

这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战

翻译参考

本文主要参考翻译自 The Stairway to Integration Services 系列文章的 Advanced Event Behavior – Level 10 of the Stairway to Integration Services,目的在于对 SSIS 有一个全面清晰的认识,所有内容在原文的基础上进行实操,由于版本差异、个人疑问等多种原因,未采用完全翻译的原则,同时也会对文中内容进行适当修改,希望最终可以更有利于学习和了解 SSIS,

感谢支持!


本部分我们关注下事件行为。我们分享两种方法,来管理事件传输的默认行为(冒泡——bubbing),并介绍 父子模式(Parent-Child pattern) 以及其中的 事件行为

关于SSIS任务事件(SSIS Task Events)

打开 “Precedence.dtsx” 包,目前控制流应该如下所示:

上一部分,我们在 “Script Task 4” 和 “序列容器1” 上创建了OnError事件处理程序 —— 称之为”侦听器”。每个 OnError事件处理程序 中我们添加一个 脚本任务 显示包含如下 SSIS变量值 的消息框:

  • System::ErrorCode
  • System::ErrorDescription
  • System::SourceName

在测试之前,让我们验证之前 “Precedence.dtsx” 包构建的一些设置更改。首先,点击 “序列容器1”,然后按”F4”键显示属性,确认 ForceExecutionResult 属性仍旧设置为 “Success”,然后点击 “Script Task 4”,并按”F4”显示属性。修改 MaximumErrorCount 属性为1。

事件和执行状态(Events and Execution Status)

上面设置 “序列容器1” 的 ForceExecutionResult 属性为 “Success”,并且没有改为 默认值(“None”)。这样的意图是演示 “序列容器1” 的 ForceExecutionResult 属性和 “序列容器1” 对错误事件响应之间的相互关系。当 ForceExecutionResult 属性覆盖了 “序列容器1” 的执行状态,它并没有干扰(not interfere)”序列容器1”侦听和响应错误事件的能力。

当 ForceExecutionResult 设置为 “Success”,”序列容器1” 可能会忽略错误事件,但是这是不确定的,我们只是证明了这一点。请注意,有忽略事件处理程序的专门的方式,在本篇文章末尾我们将演示该功能。

ForceExecutionResult属性 和 事件处理程序 之间的相互作用为数据集成开发者提供了灵活性。这一点很重要:在不使控制流程失败的情况下,错误可以发生并被侦听到(并响应)。

阻止事件冒泡

缺口处理(事件冒泡的”缺陷”) —— 阻止事件冒泡

重要的是要记住,事件冒泡将导致Error事件持续沿执行堆栈向上传输。在我们的示例中,作用域的下一层是 “Precedence.dtsx” 包控制流,它代表 SSIS 包本身。

由于我们尚未更改 “Precedence.dtsx” 包的 MaximumErrorCount 或 ForceExecutionResult 属性,因此当错误冒泡到包容器时,该包将失败。

这可能是,也或者不是预期的行为。

我们面临一个设计决定:

  • A —— 在我们想要捕获错误的任务之上的每个作用域级别上增加容错能力,从而为整个SSIS包增加容错能力(无论是否需要)。
  • B —— 我们可以在”Script Task 4”中中断错误事件(Error event)默认的冒泡行为。

让我们看下选项B。

返回 “Script Task 4” 的OnError事件处理程序,点击顶部的SSIS下拉菜单并点击”变量”(Variables)显示变量窗口。”网格选项”中设置显示”系统变量”。

“System::Propagate” 变量是一个 Boolean 变量,默认为True。“System::Propagate”是事件冒泡的控制变量。

点击 “值”(Value) 列改变默认值 True 为 False。

调试器中执行 “Precedence.dtsx” SSIS包,出现 “Script Task 4” 的提示框时点击 “否”(No)。”Script Task 4” 的 OnError事件处理程序 执行:

随后,”序列容器1” 的 OnError事件处理程序 没有执行,并且 “Script Task 3” 和 “序列容器1” 执行完成。

“Script Task 4” 引发的错误事件被 “Script Task 4” 的OnError事件处理程序听到,但是并没有冒泡到 “序列容器1” 的OnError事件处理程序。

编程控制(Programmatic Control)

我们可以在脚本任务中编程控制事件冒泡。

在 “Script Task 4” 的 OnError事件处理程序 中打开脚本任务编辑器,在 “ReadWriteVariables” 属性中添加 “System::Propagate” 变量。

点击”编辑脚本”按钮打开.Net脚本编辑器,在 Main 函数中添加如下的if语句:

根据 “System::ErrorCode” 变量的值,添加条件逻辑控制”System::Propagate”变量的值。

在执行 “Precedence.dtsx” SSIS包测试之前,修改 “Script Task 4” 的OnError事件处理程序中 “System::Propagate” 变量的默认为原来的True。

通过按”F5”,在调试器中运行 “Precedence.dtsx” 包,在 “Script Task 4” 的提示框中点击”否”。可以观察到 “Script Task 4” 的OnError事件处理程序的 “脚本任务” 执行并显示错误事件的详细信息。

后面同样执行完 “Script Task 3” 和 “序列容器1”,错误事件并没有向上冒泡。

事件冒泡已经通过编程进行管理,我们已经在SSIS中实现了动态容错(dynamic fault tolerance)。

自定义错误

我们可以通过更改 “Script Task 4” 引发的 ErrorCode 值来进行测试。

在“控制流”上,打开 “Script Task 4” 编辑器,然后单击“编辑脚本”按钮打开脚本编辑器。在 if 代码块中编辑代码以响应消息框按钮的单击,如下图所示,可以看到 Dts.Events.FireError 方法的智能提示。

注释掉 Dts.TaskResult = (int)ScriptResults.Failure;

添加:

1
cs复制代码Dts.Events.FireError(-1001, sTaskName, "Script Task 4执行失败!", "", 0);

当前代码修改用户点击 “Script Task 4” 询问成功的提示框中 “否”按钮 时的响应,为其生成一个 自定义的错误代码和错误描述(custom error code and error description) —— -1001 和 Script Task 4 failed!。

Dts.Events.FireError 方法的参数列表分别是:ErrorCode, SubComponent, Description, HelpFile 和 HelpContext。

调试运行 “Precedence.dtsx” 包。在 “Script Task 4” 的消息提示框中点击”否”,”Script Task 4” 的OnError事件处理程序中显示消息框变为如下所示:

因为 “Script Task 4” 的 OnError事件处理程序 中 “脚本任务” 的代码仅仅当 “System::ErrorCode”变量值 为 6 时才会设置 “System::Propagate” 为False。所以,当前事件仍会冒泡到”序列容器1”的事件处理程序。

此处演示仅仅在 if 条件语句中隔离单个ErrorCode值。实际中可以根据需要使用其他条件语句,如 C# 中的 Switch 或 VB 中的 Select Case。甚至可以使用SSIS工具箱中的其他选项。

冒泡++

事件冒泡可以扩展到单个SSIS包的范围之外。为了检查事件的这种行为,我们必须花一点时间介绍“父子”SSIS设计模式(Parent-Child SSIS design pattern)

父子SSIS设计模式——Parent-Child SSIS Design Pattern

SSIS执行包任务 用于从 另一个SSIS包 调用 SSIS包。

当一个包调用另一个包时,调用的包(包含”执行包任务”(Execute Package Task)的包)称为父包(parent package),而被”执行包任务”调用的包称为子包(child package)。事实证明,这种描述不仅仅是语义的,也很好地描述了程序包之间的一些有趣行为和交互。

设计父子包

在 “FirstSSIS” 项目解决方案中添加一个新的 SSIS 包。

在解决方案资源管理器中,右击 “SSIS包” 虚拟文件夹,点击 “新建SSIS包”(New SSIS Package)。

一个名为 “Package1.dtsx” 的新包被创建添加到解决方案,右击该包并点击 “重命名”,修改新包的名字为”Parent.dtsx”。

打开 “Parent.dtsx” 包,拖拽一个 “执行包任务”(Execute Package Task) 到控制流中。

双击 “执行包任务” 打开执行包任务编辑器。

“执行包任务”(Execute Package Task) 用来执行 另一个SSIS包。另一个SSIS包可以文件形式存储,也可以存储在 SQL Server 中。在”执行包任务编辑器”中需要配置”子包”的运行方式,指定如何引用子包及哪个子包(子包的位置)等。

执行包任务编辑器中添加子包有两种引用方式:项目引用(Project Reference)和外部引用(External Reference)。项目引用可以很方便的引用当前解决方案下的其他包。外部引用用来引用存储在其他位置(文件系统File system 或 SQL Server)的包

因为我们要启动当前解决方案下的另一个SSIS包,因此在”项目引用”下,”PackageNameFromProjectReference” 属性选择 “Precedence.dtsx” 包。

点击确定,关闭“执行包任务编辑器”。当在调试器中执行 “Parent.dtsx” 包时,它将会调用 “Precedence.dtsx” 包。

SSIS的布局调整父子包一起显示

在调试器中执行 SSIS 包之前,我想向您展示一个方便的技巧(a handy trick),以测试参与 父子SSIS设计模式 的SSIS包。

确保 Parent.dtsx 和 Precedence.dtsx 都打开。左击并抓住 “Precedence.dtsx” 标签,然后向右拖动使其离开原有标签,然后拖动到显示出来的布局线到如下图中的位置,释放鼠标左键。

最后,使父子模式中的两个包显示如下:

执行包及父子间的错误事件处理

点击 “Parent.dtsx” 包的控制流的空白区域,使 “Parent.dtsx” 选中。按”F5”键执行 “Parent.dtsx” 包 —— 它将会调用 “Precedence.dtsx”。

“Script Task 2” 提示框中点 “是”,”Script Task 4”提示框中点击”否”。确认 “Script Task 4” 的 OnError事件处理程序 和 “序列容器1” 的 事件处理程序 中”脚本任务”生成的 OnError事件处理消息框,确认”Script Task 3”的完成提示。

执行完成后应该如下图所示:

在 “Precedence.dtsx” 中,”Script Task 4”失败 和 “序列容器1”成功。”Parent.dtsx” 中执行包任务失败。这种情况是因为”序列容器1”的ForceExecutionResult属性保留着”Success”设置,一个错误事件冒泡穿过”序列容器1”。

如果没有更多的开发尝试,我们将无法观察到发生的其他情况 —— 比如 “Precedence.dtsx” 包执行失败。下面,向 “Precedence.dtsx” 包添加一个 OnError事件处理程序 进行演示。

复制 “序列容器1” 的 OnError事件处理程序 的 “脚本任务”,导航到 “Precedence”包 的 OnError事件处理程序,点击”链接”创建它,然后粘贴”脚本任务”到这。

打开”脚本任务编辑器”,然后点击”编辑脚本”按钮。找到 “sSubComponent” 定义的位置。修改为如下代码:

1
cs复制代码var sSubComponent = "Precedence Package OnError Event Handler";

点击 “Precedence.dtsx” 的控制流的空白区域,确保它被选择。按 “F5” 键执行 “Precedence.dtsx” 包,”Script Task 4” 提示框中点击 “No”,当 OnError事件处理程序 执行和显示消息框时,观察 “Precedence” 的 OnError事件处理程序,如下:

复制 Precedence 的 OnError事件处理程序 中的 “脚本任务”,导航到 “Parent” 包的 OnError事件处理程序,创建 事件处理程序 并粘贴 “脚本任务”。

同样,打开脚本任务编辑器,打开”编辑脚本”按钮,修改 “sSubComponent” 的初始化。

1
cs复制代码var sSubComponent = "Parent Package OnError Event Handler";

点击 “Parent.dtsx” 包控制流的空白区域,确保 “Parent.dtsx” 被选中。按”F5”键执行 “Parent.dtsx” 包。和之前的操作一样,”Script Task 4”提示框中点击”否”。在确认 “Script Task 4”、”序列容器1”、”Precedence”包 的 OnError事件处理程序 的消息框后,”Parent”包的OnError事件处理程序接下来会执行。

这里有几点有趣的事需要指出:

首先,在 父子SSIS设计模式 中,错误事件从 “Precedence.dtsx” SSIS包的 “底部”(Script Task 4),冒泡到”Parent.dtsx” SSIS包的 “顶部”。

Precedence.dtsx包就好像在Parent.dtsx包的“执行包任务”的“作用域内”一样。

第二,最初由 “Script Task 4” 生成的错误事件(Error event)仍然配置为最初的变量值。当 Error事件 冒泡时,ErrorCode,ErrorDescription和SourceName SSIS变量的值保持静态 —— 即使它从子包冒泡到父包。

这种行为适用于所有的SSIS任务事件,不仅仅OnError事件。

最后一件事(DisableEventHandlers属性)

你可能会考虑,在父子SSIS设计模式中,我们需要在子级水平管理事件吗?我们可以仅仅在父级水平管理所有事件吗?

答案是:它取决于是否适合你公司业务的事件管理设计,即使你允许大多数事件冒泡到父包的顶部,也存在有效的用例,可以在子包范围内捕获单个SSIS任务和容器事件。我相信,关键是你现在对配置这些事件的灵活性有了更多的了解。你现在有了可选项,并且所有的选择都很好。

我想讨论的最后一件事是 DisableEventHandlers 属性。

SSIS中的可执行文件 —— 换句话说是 SSIS任务和容器 —— 都有 DisableEventHandlers 属性。

DisableEventHandlers属性由任务或容器范围内的可执行文件继承。其默认值为False。不是所有的属性都是true,这一点很重要。另外 MaximumErrorCount 和 ForceExecutionResult 是不继承的。

点击 “Precedence.dtsx” 控制流,按”F4”显示包属性,修改 DisableEventHandlers 属性为True。

点击 “Parent.dtsx” 包控制流的空白区域,确保”Parent.dtsx”被选中。按”F5”执行”Parent.dtsx”包。同样, “Script Task 4” 提示框中点击”否”按钮。然后,下一个显示的是 “Parent” 的 OnError事件处理程序 的消息框。

为什么?因为 “Precedence.dtsx” SSIS包的事件处理程序现在是禁用的。

同样,你可能希望在 子包事件处理程序内隔离某些事件并停止冒泡。

总结

在本文中,我们关注于事件的行为。并演示了两种方法来更改事件冒泡的默认行为:

  • 使用 DisableEventHandlers 属性禁用事件处理程序;
  • 使用脚本任务在事件处理程序内操纵 System::Propagate SSIS变量的值,取消事件冒泡。

同时,介绍了简单的自定义错误,以及父子模式,检查了父子模式内的事件行为。

注:以上 “Precedence.dtsx” 的包名 “Package1”,可通过包属性中的 Name 属性进行修改,使其名字和”Precedence”对应。

本文转载自: 掘金

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

mysql 基本知识及常用命令汇总(2)

发表于 2021-11-17

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

继续上文……

union 操作符

UNION 操作符用于连接两个以上的 SELECT 语句的结果组合到一个结果集合中。多个 SELECT 语句会删除重复的数据。

1
2
3
4
5
6
7
sql复制代码SELECT expression1, expression2, ... expression_n
FROM tables
[WHERE conditions]
UNION [ALL | DISTINCT]
SELECT expression1, expression2, ... expression_n
FROM tables
[WHERE conditions];
参数
  • expression1, expression2, … expression_n: 要检索的列。
  • tables: 要检索的数据表。
  • WHERE conditions: 可选, 检索条件。
  • DISTINCT: 可选,删除结果集中重复的数据。默认情况下 UNION 操作符已经删除了重复数据,所以 DISTINCT 修饰符对结果没啥影响。
  • ALL: 可选,返回所有结果集,包含重复数据。

排序 order by

ORDER BY 子句将查询数据排序后再返回数据:

1
2
sql复制代码SELECT field1, field2,...fieldN FROM table_name1, table_name2...
ORDER BY field1 [ASC [DESC][默认 ASC]], [field2...] [ASC [DESC][默认 ASC]]
  • 你可以使用任何字段来作为排序的条件,从而返回排序后的查询结果。
  • 你可以设定多个字段来排序。
  • 你可以使用 ASC 或 DESC 关键字来设置查询结果是按升序或降序排列。 默认情况下,它是按升序排列。
  • 你可以添加 WHERE…LIKE 子句来设置条件。

分组 group by

GROUP BY 语句根据一个或多个列对结果集进行分组。

在分组的列上我们可以使用 COUNT, SUM, AVG,等函数。

1
2
3
4
vbnet复制代码SELECT column_name, function(column_name)
FROM table_name
WHERE column_name operator value
GROUP BY column_name;

实例:

1
sql复制代码SELECT name, COUNT(*) FROM   employee_tbl GROUP BY name;

WITH ROLLUP

WITH ROLLUP 可以实现在分组统计数据基础上再进行相同的统计(SUM,AVG,COUNT…)。

例如我们将以上的数据表按名字进行分组,再统计每个人登录的次数:

1
sql复制代码SELECT name, SUM(signin) as signin_count FROM  employee_tbl GROUP BY name WITH ROLLUP;

联表查询

  • INNER JOIN(内连接,或等值连接) :获取两个表中字段匹配关系的记录。
1
css复制代码SELECT a.runoob_id, a.runoob_author, b.runoob_count FROM runoob_tbl a INNER JOIN tcount_tbl b ON a.runoob_author = b.runoob_author;
  • LEFT JOIN(左连接): 获取左表所有记录,即使右表没有对应匹配的记录。
1
css复制代码SELECT a.runoob_id, a.runoob_author, b.runoob_count FROM runoob_tbl a LEFT JOIN tcount_tbl b ON a.runoob_author = b.runoob_author;
  • RIGHT JOIN(右连接): 与 LEFT JOIN 相反,用于获取右表所有记录,即使左表没有对应匹配的记录。
1
css复制代码SELECT a.runoob_id, a.runoob_author, b.runoob_count FROM runoob_tbl a RIGHT JOIN tcount_tbl b ON a.runoob_author = b.runoob_author;

正则表达式

模式 描述
^ 匹配输入字符串的开始位置。如果设置了 RegExp 对象的 Multiline 属性,^ 也匹配 ‘\n’ 或 ‘\r’ 之后的位置。
$ 匹配输入字符串的结束位置。如果设置了RegExp 对象的 Multiline 属性,$ 也匹配 ‘\n’ 或 ‘\r’ 之前的位置。
. 匹配除 “\n” 之外的任何单个字符。要匹配包括 ‘\n’ 在内的任何字符,请使用像 ‘[.\n]’ 的模式。
[…] 字符集合。匹配所包含的任意一个字符。例如, ‘[abc]’ 可以匹配 “plain” 中的 ‘a’。
[^…] 负值字符集合。匹配未包含的任意字符。例如, ‘[^abc]’ 可以匹配 “plain” 中的’p’。
p1 p2
* 匹配前面的子表达式零次或多次。例如,zo* 能匹配 “z” 以及 “zoo”。* 等价于{0,}。
+ 匹配前面的子表达式一次或多次。例如,’zo+’ 能匹配 “zo” 以及 “zoo”,但不能匹配 “z”。+ 等价于 {1,}。
{n} n 是一个非负整数。匹配确定的 n 次。例如,’o{2}’ 不能匹配 “Bob” 中的 ‘o’,但是能匹配 “food” 中的两个 o。
{n,m} m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。
1
sql复制代码SELECT name FROM person_tbl WHERE name REGEXP '^st';

本文转载自: 掘金

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

第一次访问网站时,连接和ssl握手慢

发表于 2021-11-17

我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

优化了各种配置,还是无效。

换证书解决。

客户端验证证书时耗时太久,应该是国外的证书引起的。

SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。SSL协议可分为两层: SSL记录协议(SSL Record Protocol):它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。 SSL握手协议(SSL Handshake Protocol):它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。

SSL握手详细过程:

一、客户端发出加密通信请求ClientHello

提供:

1,协议版本(如TSL1.0)

2,随机数1(用于生成对话密钥)

3,支持的加密方法(如RSA公钥加密)

4,支持的压缩方法

二、服务器回应SeverHello

回应内容:

1,确认使用的加密通信协议版本(TSL1.0)

2,随机数2(用于生成对话密钥)

3,确认加密方法(RSA)

4,服务器证书(包含非对称加密的公钥)

5,(可选)要求客户端提供证书的请求

三、客户端验证证书

如果证书不是可信机构颁布,或证书域名与实际域名不符,或者证书已经过期,就会向访问者显示一个警告,是否继续通信

四、客户端回应

证书没有问题,就会取出证书中的服务器公钥

然后发送:

1,随机数3(pre-master key,此随机数用服务器公钥加密,防止被窃听)

2,编码改变通知(表示随后的信息都将用双方商定的方法和密钥发送)

3,客户端握手结束通知

五、双方生成会话密钥

双方同时有了三个随机数,接着就用事先商定的加密方法,各自生成同一把“会话密钥”

服务器端用自己的私钥(非对称加密的)获取第三个随机数,会计算生成本次所用的会话密钥(对称加密的密钥),如果前一步要求客户端证书,会在这一步验证

六、服务器最后响应

服务器生成会话密钥后,向客户端发送:

1,编码改变通知(后面的信息都用双方的加密方法和密钥来发送)

2,服务器握手结束通知

本文转载自: 掘金

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

Flask 入门系列之 上下文!

发表于 2021-11-17

这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战。

上下文概念

上下文,说白了就是所谓的语境,就是语言环境。比如单独拎出来一篇文章的某一句话,我们可能不能理解它的意思,但是通过这句话所处的语言环境,再结合它前后的语句,就能很轻易的理解。

我们可以把上下文理解为当前环境的快照,是一个用来保存状态的对象。在代码执行的某个时刻,根据上下文的代码逻辑,可以决定在当前时刻下使用到的环境变量等。

Flask中的上下文

Flask 中有两种上下文,应用上下文(application context)和请求上下文(request context):

  • application:指的是调用app = Flask(__name__)创建的 Flask 对象
  • request:指的是每次 HTTP 请求发生时,在 Flask 对象内部创建的 Request 对象

请求上下文

在Flask中处理请求时,应用会生成“请求上下文”对象,保存当前请求的相关数据信息,整个请求的处理过程,都会在这个上下文对象中进行,保证请求的处理过程独立不受干扰。

请求上下文对象有:request和session,下面以request为例具体讲解。

在上篇文章Flask 入门系列之 请求钩子!我们说过,在 Flask 中有四种常用的请求钩子,分别是:before_first_request、before_request、after_request和teardown_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
python复制代码@app.before_first_request
def before_first_request():
print(request.url)
print('before_first_request')


@app.before_request
def before_request():
print(request.url)
print('before_request')


@app.after_request
def after_request(response):
print(request.url)
print('after_request')
return response


@app.teardown_request
def teardown_request(e):
print(request.url)
print('teardown_request')


@app.route('/test')
def test():
print(request.url)
return 'test'

image.png

通过请求我们发现,在每个请求钩子装饰的处理函数中,我们都可以直接访问 request 对象。而且,在其他普通函数内,无法访问 request 对象,说明 request 对象并不是真正的全局变量,只是在请求上下文的生命周期内可以访问,离开了请求的生命周期,就无法访问了。上面的请求钩子装饰的处理函数,在请求处理的不同阶段执行,自然其内部也可以访问 request 对象。

应用上下文

上面说请求上下文是和请求相关,请求上下文对象保存的是请求的相关数据信息,下面说一下应用上下文,所谓应用上下文,就是和当前应用相关的,应用上下文对象是包含当前应用相关的信息。

应用上下文对象有:current_app和g。

我们了解到,每个请求,都有一个 request 对象和视图函数对应,可以理解为当前请求(current request), 而程序也会有多个实例的情况,为了能获取对应的程序实例,而不是固定的某一个程序实例,我们就需要使用 current_app变量。

1
2
3
4
5
6
7
8
python复制代码from flask import Flask, current_app

app = Flask("tigeriaf_app")


@app.route('/')
def index():
return 'Hello, {}!'.format(current_app.name)

current_app是一个本地代理,它的类型是werkzeug.local.LocalProxy,它所代理的即是app对象,也就是说current_app == LocalProxy(app)。所以通过current_app.name可以获取当前应用的名称,也就是tigeriaf_app,使用current_app是因为它也是一个ThreadLocal变量,对它的改动不会影响到其他线程。我们可以通过current_app._get_current_object()方法来获取app对象。也可以在current_app中存储一些自定义的变量。

current_app只在请求线程内存在,它的生命周期就是在应用上下文里。离开了应用上下文,current_app一样无法使用。

g对象是 Flask 程序全局的一个临时变量,充当中间媒介的作用。我们可以通过它传递一些数据,g保存的是当前请求的全局变量,每次请求都会重设这个值,我们通常会使用它结合请求钩子来保存每个请求处理前所需要的全局变量,比如当前登入的用户对象,数据库连接等。
比如,使用g对象保存请求的 token,在视图函数中就可以直接使用g.name获取对应的值了。

1
2
3
4
5
python复制代码from flask import g

@app.before_request
def get_token():
g.name = request.headers.get("token")

原创不易,如果小伙伴们觉得有帮助,麻烦点个赞再走呗~

最后,感谢女朋友在工作和生活中的包容、理解与支持 !

本文转载自: 掘金

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

冲刺大厂每日算法&面试题,动态规划21天——第十九天 导读

发表于 2021-11-17

「这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战」

导读

在这里插入图片描述

肥友们为了更好的去帮助新同学适应算法和面试题,最近我们开始进行专项突击一步一步来。我们先来搞一下让大家最头疼的一类算法题,动态规划我们将进行为时21天的养成计划。还在等什么快来一起肥学进行动态规划21天挑战吧!!

21天动态规划入门

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,”ace”是”abcde”的一个子序列,而”aec”不是)。

进阶:

如果有大量输入的 S,称作 S1, S2, … , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T
的子序列。在这种情况下,你会怎样改变代码?

1
2
3
4
java复制代码示例 1:

输入:s = "abc", t = "ahbgdc"
输出:true
1
2
3
4
java复制代码示例 2:

输入:s = "axc", t = "ahbgdc"
输出:false
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复制代码class Solution {
public boolean isSubsequence(String s, String t) {
int n = s.length(), m = t.length();

int[][] f = new int[m + 1][26];
for (int i = 0; i < 26; i++) {
f[m][i] = m;
}

for (int i = m - 1; i >= 0; i--) {
for (int j = 0; j < 26; j++) {
if (t.charAt(i) == j + 'a')
f[i][j] = i;
else
f[i][j] = f[i + 1][j];
}
}
int add = 0;
for (int i = 0; i < n; i++) {
if (f[add][s.charAt(i) - 'a'] == m) {
return false;
}
add = f[add][s.charAt(i) - 'a'] + 1;
}
return true;
}
}

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列
是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,”ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。 两个字符串的 公共子序列
是这两个字符串所共同拥有的子序列。

1
2
3
4
5
java复制代码示例 1:

输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。
1
2
3
4
5
java复制代码示例 2:

输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。
1
2
3
4
5
java复制代码示例 3:

输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(), n = text2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
char c1 = text1.charAt(i - 1);
for (int j = 1; j <= n; j++) {
char c2 = text2.charAt(j - 1);
if (c1 == c2) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
}

面试题

继续二叉树的内容:
今天来讲怎么判断是否为完全二叉树,首先回顾一下什么叫完全二叉树。为了增强大家的记忆能力,我们将完全二叉树和满二叉树对比记忆:
在这里插入图片描述

在这里插入图片描述
所以简单来说:

完全二叉树:设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数, 第 h 层所有的结点都连续集中在最左边

满二叉树:深度为k且有2^k-1个结点的二叉树称为满二叉树

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复制代码package tree;

import java.util.LinkedList;
import java.util.Queue;

public class completeTree {
//判断是否为完全二叉树
public boolean isCompleteTree(node root) {
if(root==null)return false;
Queue<node> queue=new LinkedList<node>();
queue.add(root);
boolean NoChild=false,result=true;
while(!queue.isEmpty()) {
node current=queue.remove();
if(NoChild) {
if(current.left!=null||current.right!=null) {
result=false;
break;
}
}else {
if(current.left!=null&&current.right!=null) {
queue.add(current.left);
NoChild=true;

}else if(current.left!=null&&current.right==null) {
queue.add(current.left);
NoChild=true;

}else if(current.left==null&&current.right!=null) {
result=false;
break;
}else {
NoChild=true;
}
}
}
return result;
}

}

本文转载自: 掘金

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

动态规划攻略之:用最小花费爬楼梯

发表于 2021-11-17

「这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战」

题目

数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。

每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。

请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。

示例1:

1
2
3
ini复制代码输入:cost = [10, 15, 20]
输出:15
解释:最低花费是从 cost[1] 开始,然后走两步即可到阶梯顶,一共花费 15 。

示例2:

1
2
3
ini复制代码输入:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出:6
解释:最低花费方式是从 cost[0] 开始,逐个经过那些 1 ,跳过 cost[3] ,一共花费 6 。

解题思路

根据题目要求,我们可以利用动态规划的思路来解题。

首先,到达第 i 级台阶的阶梯顶部的最小花费,有俩个选择:

  • 先付出最小总花 minCost[i-1] 到达第i级台阶(第i-1级台阶的阶梯顶部),踏上第i级台阶需要再花费 cost[i],再迈一步到达第i级台阶的阶梯顶部,最小总花费为minCost[i-1] + cost[i]);
  • 先付出最小总花费 minCost[i-2] 到达第i-1级台阶(第i-2级台阶的阶梯顶部),踏上第i-1级台阶需要再花费 cost[i-1],再迈两步跨过第i级台阶直接到达第i级台阶的阶梯顶部,最小总花费为minCost[i-2] + cost[i-1]);

则minCost[i]是上面这两个最小总花费中的最小值。

因此状态转移方程如下:

minCost[i] = min(minCost[i-1] + cost[i], minCost[i-2] + cost[i-1])。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码class Solution {
public int minCostClimbingStairs(int[] cost) {
int size = cost.length;
int[] minCost = new int[size];
minCost[0] = 0;
minCost[1] = Math.min(cost[0], cost[1]);
for (int i = 2; i < size; i++) {
minCost[i] = Math.min(minCost[i - 1] + cost[i], minCost[i - 2] + cost[i - 1]);
}
return minCost[size - 1];
}
}

最后

时间复杂度为:O(N)

空间复杂度为:O(N)

往期文章:

  • 二叉树刷题总结:二叉搜索树的属性
  • 二叉树总结:二叉树的属性
  • 二叉树总结:二叉树的修改与构造
  • StoreKit2 有这么香?嗯,我试过了,真香
  • 看完这篇文章,再也不怕面试官问我如何构造二叉树啦!
  • 那帮做游戏的又想让大家氪金,太坏了!
  • 手把手带你撸一个网易云音乐首页 | 适配篇
  • 手把手带你撸一个网易云音乐首页(三)
  • 手把手带你撸一个网易云音乐首页(二)
  • 手把手带你撸一个网易云音乐首页(一)
  • 代码要写注释吗?写你就输了
  • Codable发布这么久我就不学,摸鱼爽歪歪,哎~就是玩儿
  • iOS 优雅的处理网络数据,你真的会吗?不如看看这篇
  • UICollectionView 自定义布局!看这篇就够了

请你喝杯 ☕️ 点赞 + 关注哦~

  1. 阅读完记得给我点个赞哦,有👍 有动力
  2. 关注公众号— HelloWorld杰少,第一时间推送新姿势

最后,创作不易,如果对大家有所帮助,希望大家点赞支持,有什么问题也可以在评论区里讨论😄~

本文转载自: 掘金

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

回调方法探索

发表于 2021-11-17

「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战」

前言

作为一名开发人员,你是否经常看到下面这张图中的场景?

image.png

那你在看到这种图的时候是否会有这样的疑惑:为什么这个 res 对象不是 null, 是谁给你赋的值呢,这个命名是我掉的方法啊?我没有在这边给它传值啊?下面我们就来以前端 axios 组件为引,深入探讨一下回调方法。

什么是回调

在计算机程序设计中,回调:是指通过函数参数将某一块可执行代码的引用传递到其他函数,供其他函数调用的编程思想。

其目的是允许 底层代码 调用在 高层定义的子程序 。

通俗的来讲就是:A 类中调用 B 类中的某个方法 C,然后 B 类中方法 C 在反过来调用 A 类中的方法 D,在这里面 D 就是回调方法。

代码示例:

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
Java复制代码package com.aha.commons.callback;

import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;

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

/**
* 模拟前端调用接口组件来讲解回调的过程
*
* @author WT
* date 2021/11/10
*/

interface CallbackInterface {

// 远程调用接口之后回调的方法
void callback(RPCResponse rpcResponse);

}

/**
* 调用接口之后返回的对象
*/
@Data
@Accessors(chain = true)
class RPCResponse {

private Integer code;
private String message;
private Map<String, Object> data;

}

/**
* 远程调用组件
*/
@Slf4j
public class RPCModule {

public void call (String url, Map<String,String> params, CallbackInterface callbackInterface) {
log.info("远程调用接口地址为:{},参数为:{}",url,params);
log.info("处理远程调用接口响应数据封装响应体...");
HashMap<String, Object> data = new HashMap<>();
data.put("name", "aha");
data.put("age", 24);
RPCResponse response = new RPCResponse().setCode(200).setMessage("请求接口成功").setData(data);
log.info("调用接口完成,执行回调方法...");
// 重点: 这边调用的是子类实现的 callback 方法
callbackInterface.callback(response);
}

}

@Slf4j
class CustomFrontPage {

public static void main(String[] args) {
HashMap<String, String> params = new HashMap<>();
params.put("name", "aha");
// 模拟前端远程调用的过程 - 查询名字叫 aha 的用户
new RPCModule().call("/api/users", params, res -> {
// 重点: 这边是 RPCModule 调用上游获取响应信息之后, 回调这个方法的, res 之所以有值是 RPCModule 调用时传递过来的
if (res.getCode().equals(200)) {
log.info("请求后端接口成功,执行成功回调,获得的响应信息为:{},获得的响应数据为:{}", res.getMessage(), res.getData());
} else {
log.error("请求后端接口成功,执行失败回调,错误的响应码为:{}", res.getCode());
}
});
}

}

过程讲解:上面的示例是模拟了前端使用 Ajax 调用后端接口的过程。

关注 CustomFrontPage 类中的 main 方法,此方法的作用便是构建请求参数,发起远程调用接口。这个过程便是 CustomFrontPage 类的 main 去调用 RPCModule 的 call 方法,而 RPCModule 的 call 方法执行之后回调 CustomFrontPage 类中匿名内部类重写的 callback 方法的过程。

在这个过程中,RPCModule 就是底层代码,CustomFrontPage 类中匿名内部类重写的 callback 方法 就是高层定义的子程序。

实现回调的核心便是子类对象引用的传递。上面示例中使用的是匿名内部类的方式将引用传递给底层代码了。

回调的分类

  1. 同步回调:阻塞当前线程,回调方法执行完之后,继续执行下面的代码。
  2. 异步回调:不阻塞当前线程,可以继续执行下面的代码。

同步回调的实现方式就是上面那种方式,这边不在进行赘述,其实一般远程请求接口都应该是异步的方式。

异步回调的实现

异步回调的话,最容易想到的方案便是多线程的方式了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
Java复制代码package com.aha.commons.callback;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;

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


/**
* 远程调用组件
*/
@Slf4j
public class ASyncRPCModule {

public void call (String url, Map<String,String> params, CallbackInterface callbackInterface) {

log.info("远程调用接口地址为:{},参数为:{}",url,params);
log.info("处理远程调用接口响应数据封装响应体...");
HashMap<String, Object> data = new HashMap<>();
data.put("name", "aha");
data.put("age", 24);
RPCResponse response = new RPCResponse().setCode(200).setMessage("请求接口成功").setData(data);
log.info("调用接口完成,执行回调方法...");
// 重点: 这边调用的是子类实现的 callback 方法
callbackInterface.callback(response);

}

}

@Slf4j
class ASyncCustomFrontPage {

public static void main(String[] args) {
HashMap<String, String> params = new HashMap<>();
params.put("name", "aha");
// 异步回调 -> 最简单的方式便是通过多线程的方式
new Thread(() ->
// 模拟前端远程调用的过程 - 查询名字叫 aha 的用户
new ASyncRPCModule().call("/api/users", params, res -> {

if (ObjectUtils.isEmpty(res) || ObjectUtils.isEmpty(res.getCode())) {
log.error("请求后端接口异常, 响应对象为 null");
return;
}

// 重点: 这边是 RPCModule 调用上游获取响应信息之后, 回调这个方法的, res 之所以有值是 RPCModule 调用时传递过来的
if (res.getCode().equals(200)) {
log.info("请求后端接口成功,执行成功回调,获得的响应信息为:{},获得的响应数据为:{}", res.getMessage(), res.getData());
} else {
log.error("请求后端接口成功,执行失败回调,错误的响应码为:{}", res.getCode());
}

})
).start();

log.info("因为是异步执行,主线程没有阻塞,我在调用接口的下面,但是依然可能是我先执行");

}

}

本文转载自: 掘金

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

Ambari274+HDP314 离线安装(2) 4

发表于 2021-11-17

「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战」

大家好,我是怀瑾握瑜,一只大数据萌新,家有两只吞金兽,嘉与嘉,上能code下能teach的全能奶爸

如果您喜欢我的文章,可以[关注⭐]+[点赞👍]+[评论📃],您的三连是我前进的动力,期待与您共同成长~


  1. 实现离线安装,更换yum源

4.1. 文件目录展示

4.1.1. http服务方式

1
2
3
4
csharp复制代码[root@master ~]# yum -y install httpd
[root@master ~]# service httpd restart
Redirecting to /bin/systemctl restart httpd.service
[root@master ~]# chkconfig httpd on

安装完成后,会生成/var/www/html目录(相当于Tomcat的webapps目录)

将之前下的Ambari、HDP、HDP-UTILS三个包放到 /var/www/html 下

1
2
csharp复制代码[root@master ~]# cd /var/www/html/
[root@master html]# mkdir ambari

拷贝文件到ambari下面

1
2
3
4
5
6
7
8
9
10
csharp复制代码[root@master html]# cd ambari/
[root@master ambari]# ls
ambari-2.7.4.0-centos7.tar.gz HDP-3.1.4.0-centos7-rpm.tar.gz HDP-UTILS-1.1.0.22-centos7.tar.gz
[root@master ambari]# tar -zxvf ambari-2.7.4.0-centos7.tar.gz
[root@master ambari]# tar -zxvf HDP-3.1.4.0-centos7-rpm.tar.gz
[root@master ambari]# mkdir HDP-UTILS
[root@master ambari]# tar -zxvf HDP-UTILS-1.1.0.22-centos7.tar.gz -C HDP-UTILS
[root@master ambari]# rm -rf ambari--2.7.4.0-centos7.tar.gz HDP-2.6.3.0-centos7-rpm.tar.gz HDP-UTILS-1.1.0.22-centos7.tar.gz
[root@master ambari]# ls
ambari HDP HDP-UTILS

4.1.2. nginx服务方式

找到nginx的配置, 在nginx server中的location中增加:

1
2
3
4
5
6
7
8
9
ini复制代码server {
listen 8001;
location / {
root /www//html/ambari/;
autoindex on;
autoindex_localtime on;
autoindex_exact_size off;
}
}

参数说明:

root /data/image/

你需要开启浏览的目录,放访问

http://IP 时候显示的就是/data/image目录下的内容

autoindex_localtime on;

默认为off,显示的文件时间为GMT时间。

改为on后,显示的文件时间为文件的服务器时间

autoindex_exact_size off;

默认为on,显示出文件的确切大小,单位是bytes。

改为off后,显示出文件的大概大小,单位是kB或者MB或者GB

listen 8001;

访问端口号

1
2
csharp复制代码[root@master ~]# cd /var/www/
[root@master html]# mkdir ambari

拷贝文件到ambari下面

1
2
3
4
5
6
7
8
9
10
csharp复制代码[root@master html]# cd ambari/
[root@master ambari]# ls
ambari-2.7.4.0-centos7.tar.gz HDP-3.1.4.0-centos7-rpm.tar.gz HDP-UTILS-1.1.0.22-centos7.tar.gz
[root@master ambari]# tar -zxvf ambari-2.7.4.0-centos7.tar.gz
[root@master ambari]# tar -zxvf HDP-3.1.4.0-centos7-rpm.tar.gz
[root@master ambari]# mkdir HDP-UTILS
[root@master ambari]# tar -zxvf HDP-UTILS-1.1.0.22-centos7.tar.gz -C HDP-UTILS
[root@master ambari]# rm -rf ambari--2.7.4.0-centos7.tar.gz HDP-2.6.3.0-centos7-rpm.tar.gz HDP-UTILS-1.1.0.22-centos7.tar.gz
[root@master ambari]# ls
ambari HDP HDP-UTILS

访问http://172.29.30.61/ambari/ 看是否能访问

4.2. 制作本地源

4.2.1. 安装本地源制作相关工具(主节点)

1
2
bash复制代码# yum install yum-utils createrepo yum-plugin-priorities -y
# createrepo ./

4.2.2. 修改文件里面的源地址(主节点)

注意文件路径,以自己为准

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
ini复制代码[root@master ambari]# vi ambari/centos7/2.7.4.0-118/ambari.repo
#VERSION_NUMBER=2.7.4.0-118
[ambari-2.7.4.0]
#json.url = http://public-repo-1.hortonworks.com/HDP/hdp_urlinfo.json
name=ambari Version - ambari-2.7.4.0
baseurl=http://172.29.30.61:8001/ambari/centos7/2.7.4.0-118
gpgcheck=1
gpgkey=http://172.29.30.61:8001/ambari/centos7/2.7.4.0-118/RPM-GPG-KEY/RPM-GPG-KEY-Jenkins
enabled=1
priority=1
[root@master ambari]# cp ambari/centos7/2.7.4.0-118/ambari.repo /etc/yum.repos.d/
[root@master ambari]# vi HDP/centos7/3.1.4.0-315/hdp.repo
#VERSION_NUMBER=3.1.4.0-315
[HDP-3.1.4.0]
name=HDP Version - HDP-3.1.4.0
baseurl=http://172.29.30.61:8001/HDP/centos7/3.1.4.0-315
gpgcheck=1
gpgkey=http://172.29.30.61:8001/HDP/centos7/3.1.4.0-315/RPM-GPG-KEY/RPM-GPG-KEY-Jenkins
enabled=1
priority=1



[HDP-UTILS-1.1.0.22]
name=HDP-UTILS Version - HDP-UTILS-1.1.0.22
baseurl=http://172.29.30.61:8001/HDP-UTILS/HDP-UTILS/centos7/1.1.0.22
gpgcheck=1
gpgkey=http://172.29.30.61:8001/HDP-UTILS/HDP-UTILS/centos7/1.1.0.22/RPM-GPG-KEY/RPM-GPG-KEY-Jenkins
enabled=1
priority=1
[root@master ambari]# cp HDP/centos7/3.1.4.0-315/hdp.repo /etc/yum.repos.d/

以上就已经创建好了

使用yum的命令清一下缓存

1
2
3
arduino复制代码# yum clean all
# yum makecache
# yum repolist

4.2.3. 将创建好的源文件拷贝到子节点(主节点)

1
2
3
4
5
6
7
8
9
shell复制代码#cd /etc/yum.repos.d
# scp ambari.repo xxx2.hadoop.com:/etc/yum.repos.d/
# scp ambari.repo xxx3.hadoop.com:/etc/yum.repos.d/
# scp ambari.repo xxx4.hadoop.com:/etc/yum.repos.d/
# scp ambari.repo xxx5.hadoop.com:/etc/yum.repos.d/
# scp hdp.repo xxx2.hadoop.com:/etc/yum.repos.d/
# scp hdp.repo xxx3.hadoop.com:/etc/yum.repos.d/
# scp hdp.repo xxx4.hadoop.com:/etc/yum.repos.d/
# scp hdp.repo xxx5.hadoop.com:/etc/yum.repos.d/
  1. 安装ambari-server

这里介绍两种模式,一种是默认postgresql数据库的安装方式,还有就是mysql方式,这里我们用到的是mysql,大家根据自身选一种即可

无论是用哪种,首先都要安装ambari-server

1
arduino复制代码# yum -y install ambari-server

5.1. 默认数据库PostgreSQL安装方式(主节点)

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
erlang复制代码[root@master yum.repos.d]# ambari-server setup
Using python /usr/bin/python
Setup ambari-server
Checking SELinux...
SELinux status is 'disabled'
Customize user account for ambari-server daemon [y/n] (n)? n
Adjusting ambari-server permissions and ownership...
Checking firewall status...
Checking JDK...
[1] Oracle JDK 1.8 + Java Cryptography Extension (JCE) Policy Files 8
[2] Custom JDK
Enter choice (1): 2
WARNING: JDK must be installed on all hosts and JAVA_HOME must be valid on all hosts.
WARNING: JCE Policy files are required for configuring Kerberos security. If you plan to use Kerberos,please make sure JCE Unlimited Strength Jurisdiction Policy Files are valid on all hosts.
Path to JAVA_HOME: /app/tools/java/jdk1.8.0_201
Validating JDK on Ambari Server...done.
Completing setup...
Configuring database...
Enter advanced database configuration [y/n] (n)? n
Configuring database...
Default properties detected. Using built-in database.
Configuring ambari database...
Checking PostgreSQL...
Running initdb: This may take up to a minute.
Initializing database ... OK

About to start PostgreSQL
Configuring local database...
Configuring PostgreSQL...
Restarting PostgreSQL
Creating schema and user...
done.
Creating tables...
done.
Extracting system views...
ambari-admin-2.7.4.0-118.jar
...........
Adjusting ambari-server permissions and ownership...
Ambari Server 'setup' completed successfully.

启动ambari
[root@master yum.repos.d]# ambari-server start
Using python /usr/bin/python
Starting ambari-server
Ambari Server running with administrator privileges.
Organizing resource files at /var/lib/ambari-server/resources...
Ambari database consistency check started...
Server PID at: /var/run/ambari-server/ambari-server.pid
Server out at: /var/log/ambari-server/ambari-server.out
Server log at: /var/log/ambari-server/ambari-server.log
Waiting for server start...........................................
Server started listening on 8080

DB configs consistency check: no errors and warnings were found.
Ambari Server 'start' completed successfully.

成功启动后在浏览器输入Ambari地址:

http://172.29.30.61:8080 即可看到页面

5.2. MySql安装方式(主节点)

5.2.1. 安装MySql

1
2
3
csharp复制代码[root@master ~]# wget http://dev.mysql.com/get/mysql-community-release-el7-5.noarch.rpm
[root@master ~]# rpm -ivh mysql-community-release-el7-5.noarch.rpm
[root@master ~]# yum install mysql-community-serve

5.2.2. 启动mysql,设置开机启动

1
2
3
csharp复制代码[root@master ~]# service mysqld start
[root@master ~]# vi /etc/rc.local
#添加service mysqld start

5.2.3. 登录进mysql,初始化设置root 密码

1
2
3
4
5
6
7
8
9
csharp复制代码[root@master ~]# mysql -uroot 
设置登录密码
mysql> set password for 'root'@'localhost' = password('yourPassword');

添加远程登录用户
mysql> grant all privileges on *.* to 'root'@'%' identified by 'yourPassword';

远程登录
#mysql -uroot -h ip(远程的ip地址) -p

5.2.4. 登录mysql,执行以下语句

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
sql复制代码CREATE DATABASE ambari;  
use ambari;
CREATE USER 'ambari'@'%' IDENTIFIED BY 'ambari';
GRANT ALL PRIVILEGES ON *.* TO 'ambari'@'%';
CREATE USER 'ambari'@'localhost' IDENTIFIED BY 'ambar';
GRANT ALL PRIVILEGES ON *.* TO 'ambari'@'localhost';
CREATE USER 'ambari'@'master' IDENTIFIED BY 'ambari';
GRANT ALL PRIVILEGES ON *.* TO 'ambari'@'master';
FLUSH PRIVILEGES;
source /var/lib/ambari-server/resources/Ambari-DDL-MySQL-CREATE.sql
show tables;
use mysql;
select Host User Password from user where user='ambari';
CREATE DATABASE hive;
use hive;
CREATE USER 'hive'@'%' IDENTIFIED BY 'hive';
GRANT ALL PRIVILEGES ON *.* TO 'hive'@'%';
CREATE USER 'hive'@'localhost' IDENTIFIED BY 'hive';
GRANT ALL PRIVILEGES ON *.* TO 'hive'@'localhost';
CREATE USER 'hive'@'master' IDENTIFIED BY 'hive';
GRANT ALL PRIVILEGES ON *.* TO 'hive'@'master';
FLUSH PRIVILEGES;
CREATE DATABASE oozie;
use oozie;
CREATE USER 'oozie'@'%' IDENTIFIED BY 'oozie';
GRANT ALL PRIVILEGES ON *.* TO 'oozie'@'%';
CREATE USER 'oozie'@'localhost' IDENTIFIED BY 'oozie';
GRANT ALL PRIVILEGES ON *.* TO 'oozie'@'localhost';
CREATE USER 'oozie'@'master' IDENTIFIED BY 'oozie';
GRANT ALL PRIVILEGES ON *.* TO 'oozie'@'master';
FLUSH PRIVILEGES;

5.2.5. 建立mysql与ambari-server的jdbc连接

1
2
3
4
5
ini复制代码[root@master yum.repos.d]# yum install mysql-connector-java
[root@master yum.repos.d]# ls /usr/share/java
[root@master yum.repos.d]# cp /usr/share/java/mysql-connector-java.jar /var/lib/ambari-server/resources/mysql-jdbc-driver.jar
[root@master yum.repos.d]# vi /etc/ambari-server/conf/ambari.properties
添加server.jdbc.driver.path=/usr/share/java/mysql-connector-java.jar

5.2.6. 设置ambari-server

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
vbnet复制代码[root@master yum.repos.d]# ambari-server setup
Using python /usr/bin/python
Setup ambari-server
Checking SELinux...
SELinux status is 'enabled'
SELinux mode is 'permissive'
WARNING: SELinux is set to 'permissive' mode and temporarily disabled.
OK to continue [y/n] (y)? y
Customize user account for ambari-server daemon [y/n] (n)? y
Enter user account for ambari-server daemon (root):ambari #ambari-server 账号。如果直接回车就是默认选择root用户
如果输入已经创建的用户就会显示:
Adjusting ambari-server permissions and ownership...
Checking firewall status...
WARNING: iptables is running. Confirm the necessary Ambari ports are accessible. Refer to the Ambari documentation for more details on ports.
OK to continue [y/n] (y)? y
Checking JDK... #设置JDK。输入:2
[1] Oracle JDK 1.8 + Java Cryptography Extension (JCE) Policy Files 8
[2] Custom JDK
Enter choice (1): 2 #输入你自己的jdk位置/usr/java/jdk1.8.0_201-amd64
WARNING: JDK must be installed on all hosts and JAVA_HOME must be valid on all hosts.
WARNING: JCE Policy files are required for configuring Kerberos security. If you plan to use Kerberos,please make sure JCE Unlimited Strength Jurisdiction Policy Files are valid on all hosts.
Path to JAVA_HOME: /usr/java/jdk1.8.0_201-amd64
Validating JDK on Ambari Server...done.
Check JDK version for Ambari Server...
JDK version found: 8
Minimum JDK version is 8 for Ambari. Skipping to setup different JDK for Ambari Server.
Checking GPL software agreement...
GPL License for LZO: https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html
Enable Ambari Server to download and install GPL Licensed LZO packages [y/n] (n)? n
Completing setup...
Configuring database...
Enter advanced database configuration [y/n] (n)? y #数据库配置。选择:y
Configuring database...
Choose one of the following options:
[1] - PostgreSQL (Embedded)
[2] - Oracle
[3] - MySQL / MariaDB
[4] - PostgreSQL
[5] - Microsoft SQL Server (Tech Preview)
[6] - SQL Anywhere
[7] - BDB
Enter choice (1): 3 #选择mysql数据库类型。输入:3
# 设置数据库的具体配置信息,根据实际情况输入,如果和括号内相同,则可以直接回车。
Hostname (localhost):
Port (3306):
Database name (ambari):
Username (ambari): ambari
Enter Database Password (bigdata): ambari
Re-enter password: ambari
Configuring ambari database...
Configuring remote database connection properties...
WARNING: Before starting Ambari Server, you must run the following DDL directly from the database shell to create the schema: /var/lib/ambari-server/resources/Ambari-DDL-MySQL-CREATE.sql
Proceed with configuring remote database connection properties [y/n] (y)? y
Extracting system views...
ambari-admin-2.7.4.0.118.jar
....
Ambari repo file contains latest json url http://public-repo-1.hortonworks.com/HDP/hdp_urlinfo.json, updating stacks repoinfos with it...
Adjusting ambari-server permissions and ownership...
Ambari Server 'setup' completed successfully.
[root@master yum.repos.d]# ambari-server start
Using python /usr/bin/python
Starting ambari-server
Ambari Server running with administrator privileges.
Organizing resource files at /var/lib/ambari-server/resources...
Ambari database consistency check started...
Server PID at: /var/run/ambari-server/ambari-server.pid
Server out at: /var/log/ambari-server/ambari-server.out
Server log at: /var/log/ambari-server/ambari-server.log
Waiting for server start...........................................
Server started listening on 8080


DB configs consistency check: no errors and warnings were found.
Ambari Server 'start' completed successfully.

如果启动失败建议查看

日志在/var/log/ambari-server/ambari-server.log里面

重置ambari-server

1
2
3
csharp复制代码[root@master ~]# ambari-server stop
[root@master ~]# ambari-server reset
[root@master ~]# ambari-server setup

如果选择的是mysql方式,就需要先执行上面的语句,然后手动将mysql里面创建的数据库进行删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sql复制代码[root@master ~]# mysql -uroot -p
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| ambari |
| hive |
| oozie |
| performance_schema |
+--------------------+
rows in set (0.00 sec)
mysql> drop database ambari;
mysql> drop database hive;
mysql> drop database oozie;

如果在安装的过程中出现了错误,又想重新安装,可以在ambari-server开启的情况下,执行下面的语句来移除已安装的包,然后再通过不同的情况选择上面两种方式的一种对ambari-server进行重置

1
bash复制代码python /usr/lib/python2.6/site-packages/ambari_agent/HostCleanup.py --silent

5.3. Mysql驱动检查

为了后续安装HDP时hive可以正常安装,此处需要检查mysql驱动是否符合要求

第一:数据库、用户名、密码是否正确,安装驱动

1
复制代码yum install mysql-connector-java

第二:mysql驱动是否存在

  1. /usr/share/java/mysql-connector-java.jar 是否有mysql驱动
  2. vim /etc/ambari-server/conf/ambari.properties

配置文件添加server.jdbc.driver.path=/usr/share/java/mysql-connector-java.jar

第三:设置ambari的mysql驱动

在master主机命令行界面执行即可(前提是上面第二条件都满足)

1
css复制代码ambari-server setup --jdbc-db=mysql --jdbc-driver=/usr/share/java/mysql-connector-java.jar
  1. 安装配置部署HDP集群

6.1. 登录过程

如果你以上安装成功

输入主机ip:8080则会看到如下界面

账户:admin 密码:admin

6.2. 安装向导

6.2.1. 配置集群名字

6.2.2. 选择版本并修改为本地源地址

Centos的选择rathat7,HDP和DHP-UTILS的url地址填上之前改好的

使用当前安装包需要修改HDP,HDP-3.1.4.0

ambari确认过了就next继续了

6.2.3. 安装配置

上传之前存好的秘钥文件id_rsa

6.2.4. 确认安装ambari的agent

确认安装ambari的agent,并检查出现的问题,这个地方经常出现错误

如图就出现了错误,点击Failed的查看错误日志

我这里的错误是ambari的8040等端口无法访问的问题,我放开了8000-9000的端口就可以了,我之前也遇到很多其他的问题,具体问题具体分析,多查谷歌,百度,国外的网站更容易解决问题

检查主机可能会发现之前漏下的问题,比如说我这里防火墙没关他就会出现提示

检查无误,NEXT→通过即可

如果这个步骤失败了错误,记得多看日志,多找问题,如果还不行的话,回档咯

1
2
3
4
csharp复制代码[root@master ~]# # ambari-server stop    #停止命令
[root@master ~]# # ambari-server reset #重置命令
[root@master ~]# # ambari-server setup #重新设置
[root@master ~]# # ambari-server start #启动命令

6.2.5. 大数据服务组件安装

勾选你所需要的

6.2.6. 节点分配

6.2.7. 分配主从

6.2.8. 安装配置

hive和oozie的数据库用户密码填上之前创建好的

如果安装了hive,ooize等,需要修改成我们本地建好的库,jdbc-mysql也要配置好

6.2.9. 概况部署

警告这里我这就忽略掉了,后期我们再修复

  1. 集群配置

7.1. hive配置更新

修改hive配置,关闭acid

1
2
3
ini复制代码hive.strict.managed.tables=false 
hive.create.as.insert.only=false
metastore.create.as.acid=false

新增hive-site配置,关闭客户端校验

1
ini复制代码hive.metastore.client.capability.check = false

设置数据库路径访问权限(KUDU使用)

1
bash复制代码hdfs dfs -chmod -R 777  /warehouse/tablespace/managed/hive

7.2. hdfs配置更新

增加配置项,方便查看文件,新增默认用户

1
ini复制代码hadoop.http.staticuser.user=hdfs

7.3. Spark配置更新

关闭hiveacid后,访问metastore值需要配置,该配置项为spark度metastore文件默认配置,原值为spark,读自己的

1
ini复制代码metastore.catalog.default=hive

结束语

如果您喜欢我的文章,可以[关注⭐]+[点赞👍]+[评论📃],您的三连是我前进的动力,期待与您共同成长~

可关注企鹅号【怀瑾握瑜的嘉与嘉】,获取资源下载方式

本文转载自: 掘金

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

1…314315316…956

开发者博客

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