morethink

morethink 查看完整档案

重庆编辑  |  填写毕业院校  |  填写所在公司/组织 www.morethink.cn 编辑
编辑

经史子集,了然于胸。谈笑之间,代码写就。

个人动态

morethink 赞了文章 · 3月16日

【JAVA】AES加密 简单实现 AES-128/ECB/PKCS5Padding

AES加密

AES 是一种可逆加密算法,对用户的敏感信息加密处理。

本文暂不深入AES原理,仅关注JAVA代码实现AES加解密。

JAVA代码实现

在用JAVA实现AES加密前,先浏览一下该网站:
http://tool.chacuo.net/cryptaes

这是一个在线AES加密网站。从页面上我们可以看到如下几点:
AES加密模式ECB/CBC/CTR/OFB/CFB
填充pkcs5padding/pkcs7padding/zeropadding/iso10126/ansix923
数据块128位/192位/256位
密码:【设置加解密的密码,JAVA中有效密码为16位/24位/32位,
其中24位/32位需要JCE(Java 密码扩展无限制权限策略文件,
每个JDK版本对应一个JCE,百度即可找到)】
偏移量:【iv偏移量,ECB不用设置】
输出:base64/hex
字符集:gb2312/gbk/gb18030/utf8

确保以上元素相互匹配,即可保证AES加解密无误。
JAVA代码实现

注意:建议加密密码为16位,避免密码位数不足补0,导致密码不一致,加解密错误。
IOS可设置任意长度的加密密码,JAVA只支持16位/24位/32位,不知能否实现任意长度,望大佬告之。

package cn.roylion.common.util;

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

/**
 * @Author: Roylion
 * @Description: AES算法封装
 * @Date: Created in 9:46 2018/8/9
 */
public class EncryptUtil{

    /**
     * 加密算法
     */
    private static final String ENCRY_ALGORITHM = "AES";

    /**
     * 加密算法/加密模式/填充类型
     * 本例采用AES加密,ECB加密模式,PKCS5Padding填充
     */
    private static final String CIPHER_MODE = "AES/ECB/PKCS5Padding";

    /**
     * 设置iv偏移量
     * 本例采用ECB加密模式,不需要设置iv偏移量
     */
    private static final String IV_ = null;

    /**
     * 设置加密字符集
     * 本例采用 UTF-8 字符集
     */
    private static final String CHARACTER = "UTF-8";

    /**
     * 设置加密密码处理长度。
     * 不足此长度补0;
     */
    private static final int PWD_SIZE = 16;

    /**
     * 密码处理方法
     * 如果加解密出问题,
     * 请先查看本方法,排除密码长度不足填充0字节,导致密码不一致
     * @param password 待处理的密码
     * @return
     * @throws UnsupportedEncodingException
     */
    private static byte[] pwdHandler(String password) throws UnsupportedEncodingException {
        byte[] data = null;
        if (password != null) {
            byte[] bytes = password.getBytes(CHARACTER);
            if (password.length() < PWD_SIZE) {
                System.arraycopy(bytes, 0, data = new byte[PWD_SIZE], 0, bytes.length);
            } else {
                data = bytes;
            }
        }
        return data;
    }

    //======================>原始加密<======================

    /**
     * 原始加密
     * @param clearTextBytes 明文字节数组,待加密的字节数组
     * @param pwdBytes 加密密码字节数组
     * @return 返回加密后的密文字节数组,加密错误返回null
     */
    public static byte[] encrypt(byte[] clearTextBytes, byte[] pwdBytes) {
        try {
            // 1 获取加密密钥
            SecretKeySpec keySpec = new SecretKeySpec(pwdBytes, ENCRY_ALGORITHM);

            // 2 获取Cipher实例
            Cipher cipher = Cipher.getInstance(CIPHER_MODE);

            // 查看数据块位数 默认为16(byte) * 8 =128 bit
//            System.out.println("数据块位数(byte):" + cipher.getBlockSize());

            // 3 初始化Cipher实例。设置执行模式以及加密密钥
            cipher.init(Cipher.ENCRYPT_MODE, keySpec);

            // 4 执行
            byte[] cipherTextBytes = cipher.doFinal(clearTextBytes);

            // 5 返回密文字符集
            return cipherTextBytes;

        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 原始解密
     * @param cipherTextBytes 密文字节数组,待解密的字节数组
     * @param pwdBytes 解密密码字节数组
     * @return 返回解密后的明文字节数组,解密错误返回null
     */
    public static byte[] decrypt(byte[] cipherTextBytes, byte[] pwdBytes) {

        try {
            // 1 获取解密密钥
            SecretKeySpec keySpec = new SecretKeySpec(pwdBytes, ENCRY_ALGORITHM);

            // 2 获取Cipher实例
            Cipher cipher = Cipher.getInstance(CIPHER_MODE);

            // 查看数据块位数 默认为16(byte) * 8 =128 bit
//            System.out.println("数据块位数(byte):" + cipher.getBlockSize());

            // 3 初始化Cipher实例。设置执行模式以及加密密钥
            cipher.init(Cipher.DECRYPT_MODE, keySpec);

            // 4 执行
            byte[] clearTextBytes = cipher.doFinal(cipherTextBytes);

            // 5 返回明文字符集
            return clearTextBytes;

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 解密错误 返回null
        return null;
    }

    //======================>BASE64<======================

    /**
     * BASE64加密
     * @param clearText 明文,待加密的内容
     * @param password 密码,加密的密码
     * @return 返回密文,加密后得到的内容。加密错误返回null
     */
    public static String encryptBase64(String clearText, String password) {
        try {
            // 1 获取加密密文字节数组
            byte[] cipherTextBytes = encrypt(clearText.getBytes(CHARACTER), pwdHandler(password));

            // 2 对密文字节数组进行BASE64 encoder 得到 BASE6输出的密文
            BASE64Encoder base64Encoder = new BASE64Encoder();
            String cipherText = base64Encoder.encode(cipherTextBytes);

            // 3 返回BASE64输出的密文
            return cipherText;
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 加密错误 返回null
        return null;
    }

    /**
     * BASE64解密
     * @param cipherText 密文,带解密的内容
     * @param password 密码,解密的密码
     * @return 返回明文,解密后得到的内容。解密错误返回null
     */
    public static String decryptBase64(String cipherText, String password) {
        try {
            // 1 对 BASE64输出的密文进行BASE64 decodebuffer 得到密文字节数组
            BASE64Decoder base64Decoder = new BASE64Decoder();
            byte[] cipherTextBytes = base64Decoder.decodeBuffer(cipherText);

            // 2 对密文字节数组进行解密 得到明文字节数组
            byte[] clearTextBytes = decrypt(cipherTextBytes, pwdHandler(password));

            // 3 根据 CHARACTER 转码,返回明文字符串
            return new String(clearTextBytes, CHARACTER);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 解密错误返回null
        return null;
    }

    //======================>HEX<======================

    /**
     * HEX加密
     * @param clearText 明文,待加密的内容
     * @param password 密码,加密的密码
     * @return 返回密文,加密后得到的内容。加密错误返回null
     */
    public static String encryptHex(String clearText, String password) {
        try {
            // 1 获取加密密文字节数组
            byte[] cipherTextBytes = encrypt(clearText.getBytes(CHARACTER), pwdHandler(password));

            // 2 对密文字节数组进行 转换为 HEX输出密文
            String cipherText = byte2hex(cipherTextBytes);

            // 3 返回 HEX输出密文
            return cipherText;
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 加密错误返回null
        return null;
    }

    /**
     * HEX解密
     * @param cipherText 密文,带解密的内容
     * @param password 密码,解密的密码
     * @return 返回明文,解密后得到的内容。解密错误返回null
     */
    public static String decryptHex(String cipherText, String password) {
        try {
            // 1 将HEX输出密文 转为密文字节数组
            byte[] cipherTextBytes = hex2byte(cipherText);

            // 2 将密文字节数组进行解密 得到明文字节数组
            byte[] clearTextBytes = decrypt(cipherTextBytes, pwdHandler(password));

            // 3 根据 CHARACTER 转码,返回明文字符串
            return new String(clearTextBytes, CHARACTER);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 解密错误返回null
        return null;
    }

    /*字节数组转成16进制字符串  */
    public static String byte2hex(byte[] bytes) { // 一个字节的数,
        StringBuffer sb = new StringBuffer(bytes.length * 2);
        String tmp = "";
        for (int n = 0; n < bytes.length; n++) {
            // 整数转成十六进制表示
            tmp = (java.lang.Integer.toHexString(bytes[n] & 0XFF));
            if (tmp.length() == 1) {
                sb.append("0");
            }
            sb.append(tmp);
        }
        return sb.toString().toUpperCase(); // 转成大写
    }

    /*将hex字符串转换成字节数组 */
    private static byte[] hex2byte(String str) {
        if (str == null || str.length() < 2) {
            return new byte[0];
        }
        str = str.toLowerCase();
        int l = str.length() / 2;
        byte[] result = new byte[l];
        for (int i = 0; i < l; ++i) {
            String tmp = str.substring(2 * i, 2 * i + 2);
            result[i] = (byte) (Integer.parseInt(tmp, 16) & 0xFF);
        }
        return result;
    }

    public static void main(String[] args) {
        String test = encryptHex("test", "1234567800000000");
        System.out.println(test);

        System.out.println(decryptHex(test, "1234567800000000"));
    }
}
查看原文

赞 9 收藏 3 评论 0

morethink 赞了回答 · 2020-05-14

我用SpringBoot集合Quartz创建的定时任务为什么有时候不执行呢,也不报错

贴下代码吧

关注 4 回答 4

morethink 赞了文章 · 2020-04-15

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

随着计算机行业的飞速发展,摩尔定律逐渐失效,多核CPU成为主流。使用多线程并行计算逐渐成为开发人员提升服务器性能的基本武器。J.U.C提供的线程池:ThreadPoolExecutor类,帮助开发人员管理线程并方便地执行并行任务。了解并合理使用线程池,是一个开发人员必修的基本功。

本文开篇简述线程池概念和用途,接着结合线程池的源码,帮助读者领略线程池的设计思路,最后回归实践,通过案例讲述使用线程池遇到的问题,并给出了一种动态化线程池解决方案。

一、写在前面

1.1 线程池是什么

线程池(Thread Pool)是一种基于池化思想管理线程的工具,经常出现在多线程服务器中,如MySQL。

线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。

而本文描述线程池是JDK中提供的ThreadPoolExecutor类。

当然,使用线程池可以带来一系列好处:

  • 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
  • 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

1.2 线程池解决的问题是什么

线程池解决的核心问题就是资源管理问题。在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:

  1. 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。
  2. 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
  3. 系统无法合理管理内部的资源分布,会降低系统的稳定性。

为解决资源分配这个问题,线程池采用了“池化”(Pooling)思想。池化,顾名思义,是为了最大化收益并最小化风险,而将资源统一在一起管理的一种思想。

Pooling is the grouping together of resources (assets, equipment, personnel, effort, etc.) for the purposes of maximizing advantage or minimizing risk to the users. The term is used in finance, computing and equipment management.——wikipedia

“池化”思想不仅仅能应用在计算机领域,在金融、设备、人员管理、工作管理等领域也有相关的应用。

在计算机领域中的表现为:统一管理IT资源,包括服务器、存储、和网络资源等等。通过共享资源,使用户在低投入中获益。除去线程池,还有其他比较典型的几种使用策略包括:

  1. 内存池(Memory Pooling):预先申请内存,提升申请内存速度,减少内存碎片。
  2. 连接池(Connection Pooling):预先申请数据库连接,提升申请连接的速度,降低系统的开销。
  3. 实例池(Object Pooling):循环使用对象,减少资源在初始化和释放时的昂贵损耗。

在了解完“是什么”和“为什么”之后,下面我们来一起深入一下线程池的内部实现原理。

二、线程池核心设计与实现

在前文中,我们了解到:线程池是一种通过“池化”思想,帮助我们管理线程而获取并发性的工具,在Java中的体现是ThreadPoolExecutor类。那么它的的详细设计与实现是什么样的呢?我们会在本章进行详细介绍。

2.1 总体设计

Java中的线程池核心实现类是ThreadPoolExecutor,本章基于JDK 1.8的源码来分析Java线程池的核心设计与实现。我们首先来看一下ThreadPoolExecutor的UML类图,了解下ThreadPoolExecutor的继承关系。

图1 ThreadPoolExecutor UML类图

ThreadPoolExecutor实现的顶层接口是Executor,顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。ExecutorService接口增加了一些能力:(1)扩充执行任务的能力,补充可以为一个或一批异步任务生成Future的方法;(2)提供了管控线程池的方法,比如停止线程池的运行。AbstractExecutorService则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。最下层的实现类ThreadPoolExecutor实现最复杂的运行部分,ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。

ThreadPoolExecutor是如何运行,如何同时维护线程和执行任务的呢?其运行机制如下图所示:

图2 ThreadPoolExecutor运行流程

线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:(1)直接申请线程执行该任务;(2)缓冲到队列中等待线程执行;(3)拒绝该任务。线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。

接下来,我们会按照以下三个部分去详细讲解线程池运行机制:

  1. 线程池如何维护自身状态。
  2. 线程池如何管理任务。
  3. 线程池如何管理线程。

2.2 生命周期管理

线程池运行的状态,并不是用户显式设置的,而是伴随着线程池的运行,由内部来维护。线程池内部使用一个变量维护两个值:运行状态(runState)和线程数量 (workerCount)。在具体实现中,线程池将运行状态(runState)、线程数量 (workerCount)两个关键参数的维护放在了一起,如下代码所示:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

ctl这个AtomicInteger类型,是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段, 它同时包含两部分的信息:线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount),高3位保存runState,低29位保存workerCount,两个变量之间互不干扰。用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。通过阅读线程池源代码也可以发现,经常出现要同时判断线程池运行状态和线程数量的情况。线程池也提供了若干方法去供用户获得线程池当前的运行状态、线程个数。这里都使用的是位运算的方式,相比于基本运算,速度也会快很多。

关于内部封装的获取生命周期状态、获取线程池线程数量的计算方法如以下代码所示:

private static int runStateOf(int c)     { return c & ~CAPACITY; } //计算当前运行状态
private static int workerCountOf(int c)  { return c & CAPACITY; }  //计算当前线程数量
private static int ctlOf(int rs, int wc) { return rs | wc; }   //通过状态和线程数生成ctl

ThreadPoolExecutor的运行状态有5种,分别为:

其生命周期转换如下入所示:

图3 线程池生命周期

2.3 任务执行机制

2.3.1 任务调度

任务调度是线程池的主要入口,当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。了解这部分就相当于了解了线程池的核心运行机制。

首先,所有任务的调度都是由execute方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:

  1. 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
  2. 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
  3. 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
  4. 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
  5. 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

其执行流程如下图所示:

图4 任务调度流程

2.3.2 任务缓冲

任务缓冲模块是线程池能够管理任务的核心部分。线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

下图中展示了线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素:

图5 阻塞队列

使用不同的队列可以实现不一样的任务存取策略。在这里,我们可以再介绍下阻塞队列的成员:

2.3.3 任务申请

由上文的任务分配部分可知,任务的执行有两种可能:一种是任务直接由新创建的线程执行。另一种是线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行。第一种情况仅出现在线程初始创建的时候,第二种是线程获取任务绝大多数的情况。

线程需要从任务缓存模块中不断地取任务执行,帮助线程从阻塞队列中获取任务,实现线程管理模块和任务管理模块之间的通信。这部分策略由getTask方法实现,其执行流程如下图所示:

图6 获取任务流程图

getTask这部分进行了多次判断,为的是控制线程的数量,使其符合线程池的状态。如果线程池现在不应该持有那么多线程,则会返回null值。工作线程Worker会不断接收新任务去执行,而当工作线程Worker接收不到任务的时候,就会开始被回收。

2.3.4 任务拒绝

任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。

拒绝策略是一个接口,其设计如下:

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

用户可以通过实现这个接口去定制拒绝策略,也可以选择JDK提供的四种已有拒绝策略,其特点如下:

2.4 Worker线程管理

2.4.1 Worker线程

线程池为了掌握线程的状态并维护线程的生命周期,设计了线程池内的工作线程Worker。我们来看一下它的部分代码:

private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
    final Thread thread;//Worker持有的线程
    Runnable firstTask;//初始化的任务,可以为null
}

Worker这个工作线程,实现了Runnable接口,并持有一个线程thread,一个初始化的任务firstTask。thread是在调用构造方法时通过ThreadFactory来创建的线程,可以用来执行任务;firstTask用它来保存传入的第一个任务,这个任务可以有也可以为null。如果这个值是非空的,那么线程就会在启动初期立即执行这个任务,也就对应核心线程创建时的情况;如果这个值是null,那么就需要创建一个线程去执行任务列表(workQueue)中的任务,也就是非核心线程的创建。

Worker执行任务的模型如下图所示:

图7 Worker执行任务

线程池需要管理线程的生命周期,需要在线程长时间不运行的时候进行回收。线程池使用一张Hash表去持有线程的引用,这样可以通过添加引用、移除引用这样的操作来控制线程的生命周期。这个时候重要的就是如何判断线程是否在运行。

​Worker是通过继承AQS,使用AQS来实现独占锁这个功能。没有使用可重入锁ReentrantLock,而是使用AQS,为的就是实现不可重入的特性去反应线程现在的执行状态。

1.lock方法一旦获取了独占锁,表示当前线程正在执行任务中。
2.如果正在执行任务,则不应该中断线程。
3.如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断。
4.线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态;如果线程是空闲状态则可以安全回收。

在线程回收过程中就使用到了这种特性,回收过程如下图所示:

图8 线程池回收过程

2.4.2 Worker线程增加

增加线程是通过线程池中的addWorker方法,该方法的功能就是增加一个线程,该方法不考虑线程池是在哪个阶段增加的该线程,这个分配线程的策略是在上个步骤完成的,该步骤仅仅完成增加线程,并使它运行,最后返回是否成功这个结果。addWorker方法有两个参数:firstTask、core。firstTask参数用于指定新增的线程执行的第一个任务,该参数可以为空;core参数为true表示在新增线程时会判断当前活动线程数是否少于corePoolSize,false表示新增线程前需要判断当前活动线程数是否少于maximumPoolSize,其执行流程如下图所示:

图9 申请线程执行流程图

2.4.3 Worker线程回收

线程池中线程的销毁依赖JVM自动的回收,线程池做的工作是根据当前线程池的状态维护一定数量的线程引用,防止这部分线程被JVM回收,当线程池决定哪些线程需要回收时,只需要将其引用消除即可。Worker被创建出来后,就会不断地进行轮询,然后获取任务去执行,核心线程可以无限等待获取任务,非核心线程要限时获取任务。当Worker无法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用。

try {
  while (task != null || (task = getTask()) != null) {
    //执行任务
  }
} finally {
  processWorkerExit(w, completedAbruptly);//获取不到任务时,主动回收自己
}

线程回收的工作是在processWorkerExit方法完成的。

图10 线程销毁流程

事实上,在这个方法中,将线程引用移出线程池就已经结束了线程销毁的部分。但由于引起线程销毁的可能性有很多,线程池还要判断是什么引发了这次销毁,是否要改变线程池的现阶段状态,是否要根据新状态,重新分配线程。

2.4.4 Worker线程执行任务

在Worker类中的run方法调用了runWorker方法来执行任务,runWorker方法的执行过程如下:

1.while循环不断地通过getTask()方法获取任务。
2.getTask()方法从阻塞队列中取任务。
3.如果线程池正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态。
4.执行任务。
5.如果getTask结果为null则跳出循环,执行processWorkerExit()方法,销毁线程。

执行流程如下图所示:

图11 执行任务流程

三、线程池在业务中的实践

3.1 业务背景

在当今的互联网业界,为了最大程度利用CPU的多核性能,并行运算的能力是不可或缺的。通过线程池管理线程获取并发性是一个非常基础的操作,让我们来看两个典型的使用线程池获取并发性的场景。

场景1:快速响应用户请求

描述:用户发起的实时请求,服务追求响应时间。比如说用户要查看一个商品的信息,那么我们需要将商品维度的一系列信息如商品的价格、优惠、库存、图片等等聚合起来,展示给用户。

分析:从用户体验角度看,这个结果响应的越快越好,如果一个页面半天都刷不出,用户可能就放弃查看这个商品了。而面向用户的功能聚合通常非常复杂,伴随着调用与调用之间的级联、多级级联等情况,业务开发同学往往会选择使用线程池这种简单的方式,将调用封装成任务并行的执行,缩短总体响应时间。另外,使用线程池也是有考量的,这种场景最重要的就是获取最大的响应速度去满足用户,所以应该不设置队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务。

图12 并行执行任务提升任务响应速度

场景2:快速处理批量任务

描述:离线的大量计算任务,需要快速执行。比如说,统计某个报表,需要计算出全国各个门店中有哪些商品有某种属性,用于后续营销策略的分析,那么我们需要查询全国所有门店中的所有商品,并且记录具有某属性的商品,然后快速生成报表。

分析:这种场景需要执行大量的任务,我们也会希望任务执行的越快越好。这种情况下,也应该使用多线程策略,并行计算。但与响应速度优先的场景区别在于,这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。所以应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。

图13 并行执行任务提升批量任务执行速度

3.2 实际问题及方案思考

线程池使用面临的核心的问题在于:线程池的参数并不好配置。一方面线程池的运行机制不是很好理解,配置合理需要强依赖开发人员的个人经验和知识;另一方面,线程池执行的情况和任务类型相关性较大,IO密集型和CPU密集型的任务运行起来的情况差异非常大,这导致业界并没有一些成熟的经验策略帮助开发人员参考。

关于线程池配置不合理引发的故障,公司内部有较多记录,下面举一些例子:

Case1:2018年XX页面展示接口大量调用降级:

事故描述:XX页面展示接口产生大量调用降级,数量级在几十到上百。

事故原因:该服务展示接口内部逻辑使用线程池做并行计算,由于没有预估好调用的流量,导致最大核心数设置偏小,大量抛出RejectedExecutionException,触发接口降级条件,示意图如下:

图14 线程数核心设置过小引发RejectExecutionException

Case2:2018年XX业务服务不可用S2级故障

事故描述:XX业务提供的服务执行时间过长,作为上游服务整体超时,大量下游服务调用失败。

事故原因:该服务处理请求内部逻辑使用线程池做资源隔离,由于队列设置过长,最大线程数设置失效,导致请求数量增加时,大量任务堆积在队列中,任务执行时间过长,最终导致下游服务的大量调用超时失败。示意图如下:

图15 线程池队列长度设置过长、corePoolSize设置过小导致任务执行速度低

业务中要使用线程池,而使用不当又会导致故障,那么我们怎样才能更好地使用线程池呢?针对这个问题,我们下面延展几个方向:

1. 能否不用线程池?

回到最初的问题,业务使用线程池是为了获取并发性,对于获取并发性,是否可以有什么其他的方案呢替代?我们尝试进行了一些其他方案的调研:

综合考虑,这些新的方案都能在某种情况下提升并行任务的性能,然而本次重点解决的问题是如何更简易、更安全地获得的并发性。另外,Actor模型的应用实际上甚少,只在Scala中使用广泛,协程框架在Java中维护的也不成熟。这三者现阶段都不是足够的易用,也并不能解决业务上现阶段的问题。

2. 追求参数设置合理性?

有没有一种计算公式,能够让开发同学很简易地计算出某种场景中的线程池应该是什么参数呢?

带着这样的疑问,我们调研了业界的一些线程池参数配置方案:

调研了以上业界方案后,我们并没有得出通用的线程池计算方式。并发任务的执行情况和任务类型相关,IO密集型和CPU密集型的任务运行起来的情况差异非常大,但这种占比是较难合理预估的,这导致很难有一个简单有效的通用公式帮我们直接计算出结果。

3. 线程池参数动态化?

尽管经过谨慎的评估,仍然不能够保证一次计算出来合适的参数,那么我们是否可以将修改线程池参数的成本降下来,这样至少可以发生故障的时候可以快速调整从而缩短故障恢复的时间呢?基于这个思考,我们是否可以将线程池的参数从代码中迁移到分布式配置中心上,实现线程池参数可动态配置和即时生效,线程池参数动态化前后的参数修改流程对比如下:

图16 动态修改线程池参数新旧流程对比

基于以上三个方向对比,我们可以看出参数动态化方向简单有效。

3.3 动态化线程池

3.3.1 整体设计

动态化线程池的核心设计包括以下三个方面:

  1. 简化线程池配置:线程池构造参数有8个,但是最核心的是3个:corePoolSize、maximumPoolSize,workQueue,它们最大程度地决定了线程池的任务分配和线程分配策略。考虑到在实际应用中我们获取并发性的场景主要是两种:(1)并行执行子任务,提高响应速度。这种情况下,应该使用同步队列,没有什么任务应该被缓存下来,而是应该立即执行。(2)并行执行大批次任务,提升吞吐量。这种情况下,应该使用有界队列,使用队列去缓冲大批量的任务,队列容量必须声明,防止任务无限制堆积。所以线程池只需要提供这三个关键参数的配置,并且提供两种队列的选择,就可以满足绝大多数的业务需求,Less is More。
  2. 参数可动态修改:为了解决参数不好配,修改参数成本高等问题。在Java线程池留有高扩展性的基础上,封装线程池,允许线程池监听同步外部的消息,根据消息进行修改配置。将线程池的配置放置在平台侧,允许开发同学简单的查看、修改线程池配置。
  3. 增加线程池监控:对某事物缺乏状态的观测,就对其改进无从下手。在线程池执行任务的生命周期添加监控能力,帮助开发同学了解线程池状态。

图17 动态化线程池整体设计

3.3.2 功能架构

动态化线程池提供如下功能:

动态调参:支持线程池参数动态调整、界面化操作;包括修改线程池核心大小、最大核心大小、队列长度等;参数修改后及时生效。
任务监控:支持应用粒度、线程池粒度、任务粒度的Transaction监控;可以看到线程池的任务执行情况、最大任务执行时间、平均任务执行时间、95/99线等。
负载告警:线程池队列任务积压到一定值的时候会通过大象(美团内部通讯工具)告知应用开发负责人;当线程池负载数达到一定阈值的时候会通过大象告知应用开发负责人。
操作监控:创建/修改和删除线程池都会通知到应用的开发负责人。
操作日志:可以查看线程池参数的修改记录,谁在什么时候修改了线程池参数、修改前的参数值是什么。
权限校验:只有应用开发负责人才能够修改应用的线程池参数。

图18 动态化线程池功能架构

参数动态化

JDK原生线程池ThreadPoolExecutor提供了如下几个public的setter方法,如下图所示:

图19 JDK 线程池参数设置接口

JDK允许线程池使用方通过ThreadPoolExecutor的实例来动态设置线程池的核心策略,以setCorePoolSize为方法例,在运行期线程池使用方调用此方法设置corePoolSize之后,线程池会直接覆盖原来的corePoolSize值,并且基于当前值和原始值的比较结果采取不同的处理策略。对于当前值小于当前工作线程数的情况,说明有多余的worker线程,此时会向当前idle的worker线程发起中断请求以实现回收,多余的worker在下次idel的时候也会被回收;对于当前值大于原始值且当前队列中有待执行任务,则线程池会创建新的worker线程来执行队列任务,setCorePoolSize具体流程如下:

图20 setCorePoolSize方法执行流程

线程池内部会处理好当前状态做到平滑修改,其他几个方法限于篇幅,这里不一一介绍。重点是基于这几个public方法,我们只需要维护ThreadPoolExecutor的实例,并且在需要修改的时候拿到实例修改其参数即可。基于以上的思路,我们实现了线程池参数的动态化、线程池参数在管理平台可配置可修改,其效果图如下图所示:

图21 可动态修改线程池参数

用户可以在管理平台上通过线程池的名字找到指定的线程池,然后对其参数进行修改,保存后会实时生效。目前支持的动态参数包括核心数、最大值、队列长度等。除此之外,在界面中,我们还能看到用户可以配置是否开启告警、队列等待任务告警阈值、活跃度告警等等。关于监控和告警,我们下面一节会对齐进行介绍。

线程池监控

除了参数动态化之外,为了更好地使用线程池,我们需要对线程池的运行状况有感知,比如当前线程池的负载是怎么样的?分配的资源够不够用?任务的执行情况是怎么样的?是长任务还是短任务?基于对这些问题的思考,动态化线程池提供了多个维度的监控和告警能力,包括:线程池活跃度、任务的执行Transaction(频率、耗时)、Reject异常、线程池内部统计信息等等,既能帮助用户从多个维度分析线程池的使用情况,又能在出现问题第一时间通知到用户,从而避免故障或加速故障恢复。

1. 负载监控和告警

线程池负载关注的核心问题是:基于当前线程池参数分配的资源够不够。对于这个问题,我们可以从事前和事中两个角度来看。事前,线程池定义了“活跃度”这个概念,来让用户在发生Reject异常之前能够感知线程池负载问题,线程池活跃度计算公式为:线程池活跃度 = activeCount/maximumPoolSize。这个公式代表当活跃线程数趋向于maximumPoolSize的时候,代表线程负载趋高。事中,也可以从两方面来看线程池的过载判定条件,一个是发生了Reject异常,一个是队列中有等待任务(支持定制阈值)。以上两种情况发生了都会触发告警,告警信息会通过大象推送给服务所关联的负责人。

图22 大象告警通知

2. 任务级精细化监控

在传统的线程池应用场景中,线程池中的任务执行情况对于用户来说是透明的。比如在一个具体的业务场景中,业务开发申请了一个线程池同时用于执行两种任务,一个是发消息任务、一个是发短信任务,这两类任务实际执行的频率和时长对于用户来说没有一个直观的感受,很可能这两类任务不适合共享一个线程池,但是由于用户无法感知,因此也无从优化。动态化线程池内部实现了任务级别的埋点,且允许为不同的业务任务指定具有业务含义的名称,线程池内部基于这个名称做Transaction打点,基于这个功能,用户可以看到线程池内部任务级别的执行情况,且区分业务,任务监控示意图如下图所示:

图23 线程池任务执行监控

3. 运行时状态实时查看

用户基于JDK原生线程池ThreadPoolExecutor提供的几个public的getter方法,可以读取到当前线程池的运行状态以及参数,如下图所示:

图24 线程池实时运行情况

动态化线程池基于这几个接口封装了运行时状态实时查看的功能,用户基于这个功能可以了解线程池的实时状态,比如当前有多少个工作线程,执行了多少个任务,队列中等待的任务数等等。效果如下图所示:

图25 线程池实时运行情况

3.4 实践总结

面对业务中使用线程池遇到的实际问题,我们曾回到支持并发性问题本身来思考有没有取代线程池的方案,也曾尝试着去追求线程池参数设置的合理性,但面对业界方案具体落地的复杂性、可维护性以及真实运行环境的不确定性,我们在前两个方向上可谓“举步维艰”。最终,我们回到线程池参数动态化方向上探索,得出一个且可以解决业务问题的方案,虽然本质上还是没有逃离使用线程池的范畴,但是在成本和收益之间,算是取得了一个很好的平衡。成本在于实现动态化以及监控成本不高,收益在于:在不颠覆原有线程池使用方式的基础之上,从降低线程池参数修改的成本以及多维度监控这两个方面降低了故障发生的概率。希望本文提供的动态化线程池思路能对大家有帮助。

四、参考资料

作者简介

  • 致远,2018年加入美团点评,美团到店综合研发中心后台开发工程师。
  • 陆晨,2015年加入美团点评,美团到店综合研发中心后台技术专家。

招聘信息

美团到店综合研发中心长期招聘前端、后端、数据仓库、机器学习/数据挖掘算法工程师,欢迎感兴趣的同学发送简历到:tech@meituan.com(邮件标题注明:美团到店综合研发中心-上海)

阅读更多技术文章,请扫码关注微信公众号-美团技术团队!

查看原文

赞 27 收藏 23 评论 0

morethink 赞了回答 · 2019-11-20

类似百度网盘:虚拟文件夹移动、复制如何实现

文件结构可理解成树结构。

一个表存储单个节点(文件或文件夹)信息,另一表存储节点们的关系。

移动某个文件/文件夹时,只需要改变它与上一级的关系,即只改变表2。
复制某个文件/文件夹时,只需要添加一个新的关系,即只往表2添加一条记录,同时增加该节点的引用次数。

当用户更改某个文件/文件夹时,若节点引用次数大于一,则克隆出一个新节点做相应修改,否则直接修改当前节点。

关注 2 回答 1

morethink 赞了回答 · 2019-09-13

解决请教一个后端/后台的设计思路.

建立任务池,多个任务节点监控任务池,一旦有任务进入任务池就自动领任务
定时任务创建程序定时将任务扔入任务池
用户注册成功之后加入定时任务里

任务创建-->任务池-->任务节点运行任务

关注 5 回答 4

morethink 赞了回答 · 2019-09-13

用stream还能怎么优化下面代码?

按照@_TNT_的写法应该是比较合适的,不过个人只是想提一点,用stream在处理集合的时候,如果你只是想着把以前的for循环改为stream里的forEach,那你还不如不用stream

你得从思想上该改变哈,一个是命令式编程,一个是声明式编程(可能更进一步说是函数式编程)

简单来说,你用声明式编程的语法却干着命令式编程的“勾当” (⊙ˍ⊙)

先在循环外面初始化一个List<Integer> goodsIdList,然后再在循环里找到需要的数据添加进去。。。这就是命令式编程石锤了,并且你使用stream看不到任何比较细力度的“函数”。。。全是一个大代码块。。就是两个大循环,你可以对比哈@_TNT_的写法,就看得出来区别了╮(╯▽╰)╭

关注 5 回答 4

morethink 赞了文章 · 2019-09-13

烧不死的草鸡就是凤凰

先简单介绍一下自己吧,渣本大学本科应届生,毕业后入职菊场,成为了一名菊场最初级的软件开发工程师(程序员)。

clipboard.png

今年7月份入职,到现在差不多两个月。刚入职培训完之后,我被分到了做java开发的部门,当时我满怀激动和期待地想要验证多年来留在心中的好奇。比如男多女少,秃头,鼓励师,加班等等。刚进入部门,我扫了一圈周围,三个好奇就得到了验证,全是男性,不过头发都还健在,就是基本都有白头发。如果在场的有鼓励师,我宁肯不要。难道开发程序,不秃头光白头?我不禁摸了一下自己的秀发,几年后我也会这样吗。。。

clipboard.png

刚入职,项目就已经进入TR4的阶段,8月初转测。你们以为只是要求而已吗?测试团队已经把转测日期印成物料啦。各种彩页,海报,在公司的讨论群里都在锣鼓喧天鞭炮齐鸣的喊,我们8月初要转测啦。但我刚入职,该吃瓜还是该吃瓜。如果不通过,结果会是怎样?老板暴怒?测试团队耻笑?技术部全体AAR?最坏的情况不会是要裁新员工吧。这可是最坏的,所有我也一定要为转测贡献一份力量,话说转测是什么?。

我导师表面微笑,刚回到工位,就各种吐槽,把测试团队总监骂的狗血喷头,但是发泄完后,还得面对现实啊!这已经是类似于商业合同的事了,完不成目标一定会受罚。

这些是背景,中间过程略去不谈,就说招人面试加班熬夜通宵什么的最终勉强搞定,主管对我们说,兄弟们,拼一把,技术部证明自己的实力,我好去跟老板申请资源,如果证明不了,再想让老板重建信任,很难了。前辈们确实很棒,最终我们还是在8月初转测了。

呸呸呸,哪有这么好的事!当天熬了一个通宵,因为在部署时冒出一个低概率的bug导致部署失败。全员都没回家,作为新员工我也和他们一起熬了通宵,虽然我不知道我干嘛了

第二天全部门带着黑眼圈,有的躺在折叠床上,有的就直接回去了。测试的结果出来了,功能没有问题,就是在高并发情况下的性能宁人堪忧,所有我入职后第一个月就直接上手性能问题。不是说好的增删改查吗?‍♀️

clipboard.png

之后测试那边仅有的一位性能测试人员一下成了技术部的香馍馍,电话一天到晚都没停过。实在抢不过别的同事,就只能去把他的测试工具和分析工具全部学过来,我到底是测试还是开发啊。具体过程就不能多讲了。很小一部分的性能优化达到公司要求的水平,我从熟悉代码,修改代码,不断分析,测试,再分析。整整一周时间。都说入职后会发胖,而我入职半月掉两斤,再来几个性能需求,我是不是可以改行去当减肥励志鼓励师。

墨菲定理,最担心发生的事总会发生。我一看下个月的需求,通篇都是达到,扛住,高并发。。程序员能搞定一切,只要你给我时间。就算你不给我时间,我也能搞定一切。

这就是我,flag立的飞起的程序员。不说了,请性能测试人员吃饭啦

查看原文

赞 5 收藏 0 评论 2

morethink 赞了文章 · 2019-08-26

8个不可不知的Mac OS X专用命令行工具

OS X的终端下通用很多Unix的工具和脚本。如果从Linux迁移到OS X会发现很多熟悉的命令和脚本工具,其实并没有任何区别。

但是OS X也提供了很多其他系统所没有的特别的命令行工具。我们推荐8个这类的工具,希望有助于提高在Mac的命令行环境下的效率。

1. open

open命令用于打开文件、目录或执行程序。就等同于在命令行模式下,重复图形界面“双击”的动作。例如这个命令与在Finder中双击Safari是一样的:

$ open /Applications/Safari.app/

如果open一个文件,则会使用关联的程序打开之。例如open screenshot.png会在Preview中查看图片。

可以使用-a选项要求自行选择打开的程序,或使用-e强制在TextEdit中编辑此文件。

open一个目录会在Finder窗口中打开此目录。一个很有用的技巧是open .打开当前目录。

Finder和终端的交互是双向的——把文件从Finder中拖入终端,就等同于把文件的完整路径粘贴到命令行中。

2. pbcopy 和 pbpaste

这两个工具可以打通命令行和剪贴板。当然用鼠标操作复制粘贴也可以——但这两个工具的真正威力,发挥在将其用作Unix工具的时候。意思就是说:可以将这两个工具用作管道、IO重定向以及和其他命令的整合。例如:

$ ls ~ | pbcopy

可以将主目录的文件列表复制到剪贴板。

也可以把任意文件的内容读入剪贴板:

$ pbcopy < blogpost.txt

做点更疯狂的尝试:获取最新Google纪念徽标(doodle)的URL并复制到剪贴板:

$ curl http://www.google.com/doodles#oodles/archive | grep -A5 'latest-doodle on' | grep 'img src' | sed s/.*'<img data-original="\/\/'/''/ | sed s/'" alt=".*'/''/ | pbcopy

使用管道语法配合pbcopy工具可以简单的抓取命令的输出,而不必向上滚动翻阅终端窗口。可以用于和他人分享命令行的标准和错误输出。pbcopypbpaste也可以用于自动化或加速执行一些事情。例如把一些邮件的主题存为任务列表,就可以先从Mail.app中复制主题,再运行:

$ pbpaste >> tasklist.txt

3. mdfind

许多Linux用户都发现Linux下查找文件的方法在OS X上不好用。当然经典的Unix find命令总是可以,但既然OS X有杀手级搜索工具Spotlight,为什么不在命令行上也使用一下呢?

这就是mdfind命令了。Spotlight能做的查找,mdfind也能做。包括搜索文件的内容和元数据(metadata)。

mdfind还提供更多的搜索选项。例如-onlyin选项可以约束搜索范围为一个目录:

$ mdfind -onlyin ~/Documents essay

mdfind的索引数据库在后台自动更新,不过你也可以使用mdutil工具诊断数据库的问题,诊断mdfind的问题也等同于诊断Spotlight。如果Spotlight的工作不正确,mdutil -E命令可以强制重建索引数据库。也可以用mdutil -i彻底关闭文件索引。

4. screencapture

screencapture命令可以截图。和Grab.appcmd + shift + 3cmd + shift + 4热键相似,但更加的灵活。

抓取包含鼠标光标的全屏幕,并以image.png插入到新邮件的附件中:

$ screencapture -C -M image.png 

用鼠标选择抓取窗口(及阴影)并复制到剪贴板:

$ screencapture -c -W

延时10秒后抓屏,并在Preview中打开之:

$ screencapture -T 10 -P image.png

用鼠标截取一个矩形区域,抓取后存为pdf文件:

$ screencapture -s -t pdf image.pdf

更多用法请参阅screencapture --help

5. launchctl

launchctl管理OS X的启动脚本,控制启动计算机时需要开启的服务。也可以设置定时执行特定任务的脚本,就像Linux cron一样。

例如,开机时自动启动Apache服务器:

$ sudo launchctl load -w /System/Library/LaunchDaemons/org.apache.httpd.plist

运行launchctl list显示当前的启动脚本。sudo launchctl unload [path/to/script]停止正在运行的启动脚本,再加上-w选项即可去除开机启动。用这个方法可以一次去除Adobe或Microsoft Office所附带的所有“自动更新”后台程序。

Launchd脚本存储在以下位置:

~/Library/LaunchAgents    
/Library/LaunchAgents          
/Library/LaunchDaemons
/System/Library/LaunchAgents
/System/Library/LaunchDaemons

启动脚本的格式可以参考这篇blog,或苹果开发者中心的文章。你也可以使用Lingon应用来完全取代命令行。

6. say

say是一个文本转语音(TTS)的有趣的工具,引擎和OS X使用的一样也是VoiceOver。如果不加其他选项,则会简单的语音朗读你给定的字符串:

$ say "Never trust a computer you can't lift."

-f选项朗读特定文本文件,-o选项将朗读结果存为音频文件而不是播放:

$ say -f mynovel.txt -o myaudiobook.aiff

say命令可以用于在脚本中播放警告或提示。例如你可以设置Automator或Hazel脚本处理文件,并在任务完成时用say命令语音提示。

最好玩(不过也负罪感十足)的用法是:通过SSH连接到朋友或同事的计算机,然后用say命令给他们一个大大大惊喜……

可以在系统设置(System Preferences)的字典和语音(Dictation & Speech)选项中调整系统的语音选项甚至是语音的语言。

7. diskutil

diskutil是OS X磁盘工具应用的命令行版。既可以完成图形界面应用的所有任务,也可以做一些全盘填0、全盘填随机数等额外的任务。先使用diskutil list查看所有磁盘的列表和所在路径,然后对特定的磁盘执行命令。

警告:不正确使用diskutil可能意外的破坏磁盘数据。请小心。

8. brew

Homebrew程序提供的brew,严格来讲不是一个OS X的原生命令,但任何一个OS X的专业用户都不会错过它。“OS X缺少的包管理器”这个评价是恰如其分的。如果你曾经在Linux上使用过apt-get(或其他包管理器——译者注),你就会发现Homebrew基本上是一样的。

使用brew可以简单的获取数千种开源工具和函数库。例如brew install imagemagick就可以安装ImageMagick(几乎可以处理任何图像问题,转换任何格式的图像工具),brew install node可以安装Node.js(当前大热的服务器端JavaScript编程工具)。

也可以通过Homebrew做有趣的事情:brew install archey会安装Archey(在启动命令行时显示苹果LOGO和计算机硬件参数的小工具)。

请输入图片描述

Homebrew能安装的工具数量庞大,并且一直保持更新。Homebrew最棒的一点是:所有的文件都被约束在/usr/local/一个位置之下。也就是说可以通过Homebrew安装新版软件的同时,保持系统内置的依赖库或其他软件不变。同时如果想彻底删除Homebrew,也变得非常简单。

(注:删除Homebrew最好还是不要直接删除/usr/local/。应当用这个卸载脚本。)

最后,这里还有一篇OS X所有命令的完整列表


8个不可不知的Mac OS X专用命令行工具
原文:Eight Terminal Utilities Every OS X Command Line User Should Know
编译:SegmentFault
责任:沙渺

查看原文

赞 56 收藏 147 评论 8

morethink 回答了问题 · 2019-08-26

解决springmvc的getrealpath如何取得当前项目的路径,而不是tomcat的

应该是这个原因 https://www.morethink.cn/how-...

关注 4 回答 3

morethink 赞了文章 · 2019-08-10

容器隔离性带来的问题--容器化Java应用比虚机启动速度慢

引发的问题

同等配置下,虚机中的java 服务的启动速度,要比容器快很多(将近两倍)

实测数据

在同是1c1g的虚机和容器中,虚机启动时间大概在1min20s,容器启动时间大概在2min40s。

排查思路

怀疑网络

最开始怀疑是网络问题,因为业务依赖外部数据库,在容器和虚机中ping、telnet外部数据库,能通而且延迟差不多。

咨询熟悉java的小伙伴,说 spingboot可能有潜在的外部网络请求延迟(如请求Spring官网等),请求可能多次失败超时,不影响服务启动,但会影响启动时间。通过在虚机和容器中抓包,抓到了一个外部域名,但是虚机容器中都可以正常联通。包括修改域名服务器,都没有效果

硬件差异

排查问题陷入僵局后,咨询小伙伴的建议,涛哥提出是不是因为硬件差异导致的?这是个新的思路,之前只关注了软件层面的。

google了下,确实有人遇到了因为cpu频率的差异,导致虚机和容器中业务性能的差异。查看了容器和虚机所在主机的cpu频率后,进一步证实了涛哥的猜想,cpu频率确实有将近一倍的差异。根据文章中提供的解决办法,通过修改cpu的工作模式,从
powersave到performance,来提高cpu的工作频率。命令如下:

# 查看cpu频率
# lscpu    
Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                48
On-line CPU(s) list:   0-47
Thread(s) per core:    2
Core(s) per socket:    12
Socket(s):             2
NUMA node(s):          2
Vendor ID:             GenuineIntel
CPU family:            6
Model:                 79
Model name:            Intel(R) Xeon(R) CPU E5-2650 v4 @ 2.20GHz
Stepping:              1
CPU MHz:               2494.133
CPU max MHz:           2900.0000
CPU min MHz:           1200.0000
BogoMIPS:              4389.67
Virtualization:        VT-x
L1d cache:             32K
L1i cache:             32K
L2 cache:              256K
···
# 查看cpu工作模式
# cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
powersave
powersave
...

# 修改cpu工作模式
# cpupower -c all frequency-set -g performance
# 查看每个cpu的频率
# grep -i mhz /proc/cpuinfo
cpu MHz        : 1870.495
cpu MHz        : 2348.156
cpu MHz        : 2160.900
cpu MHz        : 1918.896
··· 

在修改完cpu工作模式后,cpu MHz确实有很大的提高,但是实测容器中业务启动时间并没有预期的和虚机中的速度一样,只有一点优化。看来cpu MHz不是决定的影响因素。

后来详细查了一下,cpu MHz是个不断浮动的素质,cpu性能要看CPU max MHz和工作模式。两台宿主机的cpu型号是一致的,改动cpu工作模式影响有限

容器对java的隔离缺陷

在之前容器化java业务的时候就遇到了OOMKilled,以及Runtime.getRuntime().availableProcessors()获取的cpu核数问题。当时通过引入了lxcfs,以及替换jvm libnumcpus.so文件,通过环境变量注入cpu核数来解决这个问题。

在怀疑是隔离引起的问题后,对比了虚机和容器中java进程的线程数,发现确实有比较大的差异。命令如下:

# 虚机中
···
[root@data-message-b69c847c7-sjlrx /]# cat /proc/136/status |grep Threads
Threads:    42
[root@data-message-b69c847c7-sjlrx /]# cat /proc/136/status |grep Threads
Threads:    42
[root@data-message-b69c847c7-sjlrx /]# cat /proc/136/status |grep Threads
Threads:    42
[root@data-message-b69c847c7-sjlrx /]# cat /proc/136/status |grep Threads
Threads:    42
[root@data-message-b69c847c7-sjlrx /]# cat /proc/136/status |grep Threads
Threads:    42
[root@data-message-b69c847c7-sjlrx /]# cat /proc/136/status |grep Threads
Threads:    42
···


# 容器中
···
[root@data-message-79bb65797d-ffsfb /]# cat /proc/42/status |grep Threads
Threads:    74
[root@data-message-79bb65797d-ffsfb /]# cat /proc/42/status |grep Threads
Threads:    74
[root@data-message-79bb65797d-ffsfb /]# cat /proc/42/status |grep Threads
Threads:    76
[root@data-message-79bb65797d-ffsfb /]# cat /proc/42/status |grep Threads
Threads:    76
[root@data-message-79bb65797d-ffsfb /]# cat /proc/42/status |grep Threads
Threads:    76
···

解决办法

使用包含了cpu-online /sys/devices/system/cpu/online的lxcfs(我们之前引入的lxcfs还未支持cpu-online)

在引入新版lxcfs cpu-online后,线程数下降明显,启动速度有明显的改善,达到和虚机同等水平。

LXCFS 3.1.2 has been released

Virtualize /sys/devices/system/cpu/online

LXCFS now also partially virtualizes sysfs. The first file to virtualize is /sys/devices/system/cpu/online per container.

结论

容器java进程启动慢的最终原因,还是容器的隔离性不够,导致jvm启动过多的线程,线程频繁切换带来的性能下降。目前使用包含cpu-online的lxcfs能解决这个问题。

查看原文

赞 2 收藏 0 评论 1

morethink 关注了用户 · 2019-08-06

SegmentFault @segmentfault

SegmentFault 社区管理媛 - 思否小姐姐

纯粹的技术社区离不开所有开发者的支持和努力 biubiu

更多技术内容与动态欢迎关注 @SegmentFault 官方微博与微信公众号!

点击添加思否小姐姐个人微信号

关注 84103

morethink 赞了文章 · 2019-07-31

七道常见的Redis面试题分享

clipboard.png

绝大部分写业务的程序员,在实际开发中使用 Redis 的时候,只会 Set Value 和 Get Value 两个操作,对 Redis 整体缺乏一个认知。这里以面试题的形式对 Redis 常见问题做一个总结,解决大家的知识盲点。

1、为什么使用 Redis?

在项目中使用 Redis,主要考虑两个角度:性能和并发。如果只是为了分布式锁这些其他功能,还有其他中间件 Zookpeer 等代替,并非一定要使用 Redis。

性能:

如下图所示,我们在碰到需要执行耗时特别久,且结果不频繁变动的 SQL,就特别适合将运行结果放入缓存。这样,后面的请求就去缓存中读取,使得请求能够迅速响应。

特别是在秒杀系统,在同一时间,几乎所有人都在点,都在下单。。。执行的是同一操作———向数据库查数据。

clipboard.png

根据交互效果的不同,响应时间没有固定标准。在理想状态下,我们的页面跳转需要在瞬间解决,对于页内操作则需要在刹那间解决。

并发:

如下图所示,在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用 Redis 做一个缓冲操作,让请求先访问到 Redis,而不是直接访问数据库。

clipboard.png

使用 Redis 的常见问题

  • 缓存和数据库双写一致性问题
  • 缓存雪崩问题
  • 缓存击穿问题
  • 缓存的并发竞争问题

2、单线程的 Redis 为什么这么快

这个问题是对 Redis 内部机制的一个考察。很多人都不知道 Redis 是单线程工作模型。

原因主要是以下三点:

  • 纯内存操作
  • 单线程操作,避免了频繁的上下文切换
  • 采用了非阻塞 I/O 多路复用机制

仔细说一说 I/O 多路复用机制,打一个比方:小名在 A 城开了一家快餐店店,负责同城快餐服务。小明因为资金限制,雇佣了一批配送员,然后小曲发现资金不够了,只够买一辆车送快递。

经营方式一

客户每下一份订单,小明就让一个配送员盯着,然后让人开车去送。慢慢的小曲就发现了这种经营方式存在下述问题:

  • 时间都花在了抢车上了,大部分配送员都处在闲置状态,抢到车才能去送。
  • 随着下单的增多,配送员也越来越多,小明发现快递店里越来越挤,没办法雇佣新的配送员了。
  • 配送员之间的协调很花时间。

综合上述缺点,小明痛定思痛,提出了经营方式二。

经营方式二

小明只雇佣一个配送员。当客户下单,小明按送达地点标注好,依次放在一个地方。最后,让配送员依次开着车去送,送好了就回来拿下一个。上述两种经营方式对比,很明显第二种效率更高。

在上述比喻中:

  • 每个配送员→每个线程
  • 每个订单→每个 Socket(I/O 流)
  • 订单的送达地点→Socket 的不同状态
  • 客户送餐请求→来自客户端的请求
  • 明曲的经营方式→服务端运行的代码
  • 一辆车→CPU 的核数

于是有了如下结论:

  • 经营方式一就是传统的并发模型,每个 I/O 流(订单)都有一个新的线程(配送员)管理。
  • 经营方式二就是 I/O 多路复用。只有单个线程(一个配送员),通过跟踪每个 I/O 流的状态(每个配送员的送达地点),来管理多个 I/O 流。

下面类比到真实的 Redis 线程模型,如图所示:

-clipboard.png

Redis-client 在操作的时候,会产生具有不同事件类型的 Socket。在服务端,有一段 I/O 多路复用程序,将其置入队列之中。然后,文件事件分派器,依次去队列中取,转发到不同的事件处理器中。

3、Redis 的数据类型及使用场景

一个合格的程序员,这五种类型都会用到。

String

最常规的 set/get 操作,Value 可以是 String 也可以是数字。一般做一些复杂的计数功能的缓存。

Hash

这里 Value 存放的是结构化的对象,比较方便的就是操作其中的某个字段。我在做单点登录的时候,就是用这种数据结构存储用户信息,以 CookieId 作为 Key,设置 30 分钟为缓存过期时间,能很好的模拟出类似 Session 的效果。

List

使用 List 的数据结构,可以做简单的消息队列的功能。另外,可以利用 lrange 命令,做基于 Redis 的分页功能,性能极佳,用户体验好。

Set

因为 Set 堆放的是一堆不重复值的集合。所以可以做全局去重的功能。我们的系统一般都是集群部署,使用 JVM 自带的 Set 比较麻烦。另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。

Sorted Set

Sorted Set 多了一个权重参数 Score,集合中的元素能够按 Score 进行排列。可以做排行榜应用,取 TOP N 操作。Sorted Set 可以用来做延时任务。

4、Redis 的过期策略和内存淘汰机制

Redis 是否用到家,从这就能看出来。比如你 Redis 只能存 5G 数据,可是你写了 10G,那会删 5G 的数据。怎么删的,这个问题思考过么?

正解:Redis 采用的是定期删除+惰性删除策略。

为什么不用定时删除策略

定时删除,用一个定时器来负责监视 Key,过期则自动删除。虽然内存及时释放,但是十分消耗 CPU 资源。在大并发请求下,CPU 要将时间应用在处理请求,而不是删除 Key,因此没有采用这一策略。

定期删除+惰性删除如何工作

定期删除,Redis 默认每个 100ms 检查,有过期 Key 则删除。需要说明的是,Redis 不是每个 100ms 将所有的 Key 检查一次,而是随机抽取进行检查。如果只采用定期删除策略,会导致很多 Key 到时间没有删除。于是,惰性删除派上用场。

采用定期删除+惰性删除就没其他问题了么

不是的,如果定期删除没删除掉 Key。并且你也没及时去请求 Key,也就是说惰性删除也没生效。这样,Redis 的内存会越来越高。那么就应该采用内存淘汰机制。

在 redis.conf 中有一行配置:

# maxmemory-policy volatile-lru

该配置就是配内存淘汰策略的:

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。(推荐使用,目前项目在用这种)(最近最久使用算法)
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key。(应该也没人用吧,你不删最少使用 Key,去随机删)
  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。(不推荐)
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。(依然不推荐)
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。(不推荐)

5、Redis 和数据库双写一致性问题

一致性问题还可以再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题。前提是如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终一致性。

另外,我们所做的方案从根本上来说,只能降低不一致发生的概率。因此,有强一致性要求的数据,不能放缓存。首先,采取正确更新策略,先更新数据库,再删缓存。其次,因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。

6、如何应对缓存穿透和缓存雪崩问题

这两个问题,一般中小型传统软件企业很难碰到。如果有大并发的项目,流量有几百万左右,这两个问题一定要深刻考虑。缓存穿透,即黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而数据库连接异常。

缓存穿透解决方案:

  • 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试。
  • 采用异步更新策略,无论 Key 是否取到值,都直接返回。Value 值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。
  • 提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的 Key。迅速判断出,请求所携带的 Key 是否合法有效。如果不合法,则直接返回。

缓存雪崩,即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常。

缓存雪崩解决方案:

  • 给缓存的失效时间,加上一个随机值,避免集体失效。
  • 使用互斥锁,但是该方案吞吐量明显下降了。
  • 双缓存。我们有两个缓存,缓存 A 和缓存 B。缓存 A 的失效时间为 20 分钟,缓存 B 不设失效时间。自己做缓存预热操作。
  • 然后细分以下几个小点:从缓存 A 读数据库,有则直接返回;A 没有数据,直接从 B 读数据,直接返回,并且异步启动一个更新线程,更新线程同时更新缓存 A 和缓存 B。

7、如何解决 Redis 的并发竞争 Key 问题

这个问题大致就是,同时有多个子系统去 Set 一个 Key。这个时候要注意什么呢?大家基本都是推荐用 Redis 事务机制。

但是我并不推荐使用 Redis 的事务机制。因为我们的生产环境,基本都是 Redis 集群环境,做了数据分片操作。你一个事务中有涉及到多个 Key 操作的时候,这多个 Key 不一定都存储在同一个 redis-server 上。因此,Redis 的事务机制,十分鸡肋。

如果对这个 Key 操作,不要求顺序

这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做 set 操作即可,比较简单。

如果对这个 Key 操作,要求顺序

假设有一个 key1,系统 A 需要将 key1 设置为 valueA,系统 B 需要将 key1 设置为 valueB,系统 C 需要将 key1 设置为 valueC。

期望按照 key1 的 value 值按照 valueA > valueB > valueC 的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳。

假设时间戳如下:

系统 A key 1 {valueA 3:00}

系统 B key 1 {valueB 3:05}

系统 C key 1 {valueC 3:10}

那么,假设系统 B 先抢到锁,将 key1 设置为{valueB 3:05}。接下来系统 A 抢到锁,发现自己的 valueA 的时间戳早于缓存中的时间戳,那就不做 set 操作了,以此类推。其他方法,比如利用队列,将 set 方法变成串行访问也可以。

8、总结

Redis 在国内各大公司都能看到其身影,比如我们熟悉的新浪,阿里,腾讯,百度,美团,小米等。学习 Redis,这几方面尤其重要:Redis 客户端、Redis 高级功能、Redis 持久化和开发运维常用问题探讨、Redis 复制的原理和优化策略、Redis 分布式解决方案等。

如果你觉得文章对你有帮助的话,可以关注、点赞、收藏、转发走一波,谢谢!
查看原文

赞 51 收藏 32 评论 0

morethink 回答了问题 · 2019-07-29

解决Netty 舍弃 Http Response

public class HttpDiscardHandler extends ChannelInboundHandlerAdapter {
    
        private boolean ignore = false;
    
        private int maxContentLength = 9 * 1024;
    
        private String body = "返回体超过" + maxContentLength + "B,已省略";
    
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            System.out.println("success!~!~");
            if (msg instanceof HttpResponse) {
                HttpResponse response = (HttpResponse) msg;
                if (Integer.valueOf(response.headers().get("Content-Length")) > maxContentLength) {
                    msg = new DefaultFullHttpResponse(response.protocolVersion(), response.status(), Unpooled.copiedBuffer(body, CharsetUtil.UTF_8));
                    ignore = true;
                }   else {
                ignore = false;
            }
                ctx.fireChannelRead(msg);
            } else {
                if (ignore) {
                    ReferenceCountUtil.release(msg);
                } else {
                    ctx.fireChannelRead(msg);
                }
            }
        }
    }

关注 1 回答 1

morethink 提出了问题 · 2019-07-29

解决Netty 舍弃 Http Response

使用Netty作为Http Client 发送请求,添加如下Handler

pipeline.addLast(new HttpClientCodec());
pipeline.addLast(new HttpObjectAggregator(10 * 1024));

现在发现当返回体很大的时候,会触发 TooLongFrameException
请问如何在连接不断开的情况在HttpClientCodecHttpObjectAggregator中间加一个HttpDiscardHandler,发现Content-length大于10 * 1024时就舍弃得到的Http Response

关注 1 回答 1

morethink 回答了问题 · 2019-07-26

解决问一个关于前端跨域的问题

当GET请求携带Access-Token头部的时候,会触发CORS中的预检OPTIONS请求,加上你的CORS没有处理OPTIONS请求,因此造成再次跨域。

详情可以看看 SpringMVC解决跨域问题

关注 4 回答 4

morethink 回答了问题 · 2019-07-26

解决java后台,ajax提交到后台的数据,后台接受后,为什么在本地是正常的,放到服务器就是乱码?

CentOS(Linux)中解决MySQL中文乱码

在文件 /etc/my.cnf 中添加如下文本

[mysql] 
default-character-set=utf8 
[client] 
default-character-set=utf8 
[mysqld] 
default-character-set=utf8

关注 6 回答 5

morethink 关注了问题 · 2019-07-19

jQuery AJAX 传递base64 数据,Nginx 报403 错误

题目描述

jQuery 用Ajax 传递一个图片到后台,服务端用的Nginx 代理

  1. 不存在跨域问题
  2. 不走Nginx 直接访问tomcat 的话可以成功
  3. 其他Ajax 调用都能成功

题目来源及自己的思路

使用postman 试了半天之后发现结果出现在参数上面
参数格式为:data:image/png;base64, 开头,并且提交方式为form-data或者x-www-form-urlencoded的时候Nginx 就会抛出403 的错误
使用raw 的时候就没问题

错误信息

<html>
    <head>
        <title>403 Forbidden</title>
    </head>
    <body bgcolor="white">
        <center>
            <h1>403 Forbidden</h1>
        </center>
        <hr>
        <center>HillstoneNetworks</center>
    </body>
</html>
<!-- a padding to disable MSIE and Chrome friendly error page -->
<!-- a padding to disable MSIE and Chrome friendly error page -->
<!-- a padding to disable MSIE and Chrome friendly error page -->
<!-- a padding to disable MSIE and Chrome friendly error page -->
<!-- a padding to disable MSIE and Chrome friendly error page -->
<!-- a padding to disable MSIE and Chrome friendly error page -->

想知道Nginx 为什么会产生这个问题

关注 3 回答 0

morethink 赞了回答 · 2019-07-19

如何判断两个list相等

如果要判断两个数组完全相等(即List元素的顺序也一致),可以用
ListUtils.isEqualList(Collection<?> list1, Collection<?> list2)
import org.apache.commons.collections4.ListUtils;

如果按楼主的需求,我可能会这么写

        List<Integer> list1 =  Lists.newArrayList();
        list1.add(1);
        list1.add(2);
        list1.add(3);
        List<Integer> list2 =  Lists.newArrayList();
        list1.add(3);
        list1.add(2);
        list1.add(1);
        
        if(list1 == null && list2 == null){
            return true;
        }
        
        if(list1 != null && list1.containsAll(list2) && 
         list2 != null && list2.containsAll(list1)){
            return  true;
        }else {
            return false;
        }

关注 5 回答 4

morethink 回答了问题 · 2019-07-19

解决一个学习过程中的小疑惑。

关注 4 回答 2