
能力说明:
掌握封装、继承和多态设计Java类的方法,能够设计较复杂的Java类结构;能够使用泛型与集合的概念与方法,创建泛型类,使用ArrayList,TreeSet,TreeMap等对象掌握Java I/O原理从控制台读取和写入数据,能够使用BufferedReader,BufferedWriter文件创建输出、输入对象。
暂时未有相关云产品技术能力~
阿里云技能认证
详细说明RabbitMQ核心思想 MQ是干什么用的? 应用解耦、异步、流量削锋、数据分发、错峰流控、日志收集等等... 当前最主流的消息中间件。 高可用性,支持发送确认,投递确认等特性 高可用,支持镜像队列 支持插件 优点: 基于 Erlang, 支持高并发 支持多种平台,多种客户端,文档齐全 可靠性高 在互联网公司有较大规模等使用,社区活跃度高 1. AMQP协议介绍 Broker :接受和分发消息等的应用,RabbitMQ就是Message Virtual Host : 虚拟机Broker , 将多个单元隔离开 Connection : publisher / consumer 和 broker之间的TCP连接 Channel : connection内部建立的逻辑连接,通常没个线程创建单独的channel Rounting Key : 路由键,用来只是消息的路由转发,相当于快递的地址 Exchange : 交换机 ,相当于快递的分拨中心 Queue : 消息队列,消息最终被送到这里等待consumer 取走 Binding : exchange 和 queue之间的虚拟连接,用于message的分发依据 AMQP协议的核心概念-Exchange 在AMQP协议或者是RabbitMQ实现中,最核心的组件是Exchange Exchange 承担 RabbitMQ 的核心功能 --- 路由转发 Exchange 有多个种类,配置多变,需要详细讲解 RabbitMQ核心 -- Exchange解析 Exchange是 AMQP 协议和RabbitMQ的 核心组件 Exchange的功能是根据 绑定关系 和 路由键为消息提供路由,将消息转发至相应的队列 Exchange有4种类型 :Direct / Topic / Fabout /Headers Direct Exchange (直接路由) Message中的Routing Key 如果和 Binding Key 一致, Direct Exchange 则将 message 发到对应的 queue中 Fanout Exchange (广播路由) 每个发到 Fanout Exchange 的 message 都会分发到所有绑定到queue上去 Topit Exchange (话题路由) 根据 Routing Key 及通配规则,Topic Exchange 将消息分发目标 Queue中 全匹配 :与Direct 类似 Binding Key 中的 #:匹配任意个数的word 2. Docker 安装 RabbitMQ docker pull rabbitmq 这里是直接安装最新的 docker run -d --hostname my-rabbit --name rabbit -p 15672:15672 -p 5672:5672 rabbitmq 访问 : http://IP地址:15672 用户名和密码默认都是guest 3. RabbitMQ保证消息的可靠性 需要使用RabbitMQ发送端确认机制,确认消息成功发送到RabbitMQ并被处理 需要使用RabbitMQ消息返回机制,若没发现目标队列,中间件会通知发送方 需要使RabbitMQ消息端确认消息,确认消息没有发生异常 需要使用RabbitMQ消费端限流机制,限制消息推送速度 ,保障接受端服务稳定 大量到堆积消息会给RabbitMQ产生很大到压力,需要使用RabbitMQ消息过期时间,防止消息大量积压 过期后会直接丢弃, 不符合业务逻辑,需要使用RabbitMQ死信队列,收集过期消息,以供分析 4. 发送确认机制原理 消息真的发出去了吗? 消息发送后,发送端不知道RabbitMQ是否真的收到了消息,若RabbitMQ异常,消息丢失,业务异常,这个时候我们就需要使用RabbitMQ发送端确认机制,确认消息发送 三种确认机制 1. 单条同步确认 配置channel,开启确认模式:channel.confirmSelect() 每发送一条消息,调用channel.waitForConfirms()方法等待确认 //建立连接 ConnectionFactory connectionFactory = new ConnectionFactory(); connectionFactory.setHost("127.0.0.1"); Connection connection = connectionFactory.newConnection(); Channel channel = connection.createChannel(); String message = objectMapper.writeValueAsString(orderMessageDTO); channel.confirmSelect(); channel.basicPublish( "exhange.order.restaurant", "key.restaurant", null, message.getBytes()); if(channel.waitForConfirms()){ //表示发送确认处理逻辑 }else{ //发送失败 } 2. 多条同步确认 配置channel,开启确认模式:channel.confirmSelect() 发送多条消息后,调用channel.waitForConfirms()方法等待确认 //建立连接 ConnectionFactory connectionFactory = new ConnectionFactory(); connectionFactory.setHost("127.0.0.1"); Connection connection = connectionFactory.newConnection(); Channel channel = connection.createChannel(); String message = objectMapper.writeValueAsString(orderMessageDTO); channel.confirmSelect(); channel.basicPublish( "exhange.order.restaurant", "key.restaurant", null, message.getBytes()); if(channel.waitForConfirms()){ //表示发送确认处理逻辑 }else{ //发送失败 } 3. 异步确认 配置channel,开启确认模式:channel.confirmSelect() 在channel上添加监听: addConfirmListener , 发送消息后,会回调此方法,通知是否发送成功 异步确认有可能是单条,也有可能是多条,取决于MQ //建立连接 ConnectionFactory connectionFactory = new ConnectionFactory(); connectionFactory.setHost("127.0.0.1"); Connection connection = connectionFactory.newConnection(); Channel channel = connection.createChannel(); String message = objectMapper.writeValueAsString(orderMessageDTO); channel.confirmSelect(); channel.basicPublish( "exhange.order.restaurant", "key.restaurant", null, message.getBytes()); ConfirmListener confirmListener = new ConfirmListener(){ @Override public void handleAck(long deliveryTag, boolean multiple) throws IOException { System.out.println("Ack " + deliveryTag + multiple); } @Override public void handleNack(long deliveryTag, boolean multiple) throws IOException { System.out.println("Nack " + deliveryTag + multiple); } }; channel.addConfirmListener(confirmListener); 5. 消息返回机制 消息真被路由了吗? 消息发送后,发送端不知道消息是否被正确路由,若路由异常,消息会被丢弃,业务异常,需要使用RabbitMQ消息返回机制,确认消息被正确路由 消息的开启方法: 在RabbitMQ基础配置中又一个关键配置项:Mandatory Mandatory若为false,RabbitMQ将直接丢弃无法路由的消息 Mandatory若为true,RabbitMQ才会处理无法路由的消息 DeliverCallback deliverCallback = ((consumerTag, message) -> { //拿到消息 String messageBody = new String(message.getBody()); ConnectionFactory connectionFactory = new ConnectionFactory(); connectionFactory.setHost("127.0.0.1"); try { Connection connection = connectionFactory.newConnection(); Channel channel = connection.createChannel(); channel.addReturnListener(new ReturnListener() { @Override public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException { log.info("Message Return:"); //处理失败的业务逻辑 } }); channel.basicPublish( "exhange.order.restaurant", "key.restaurant", true, null, messageBody.getBytes()); }catch (Exception e){ log.error(e.getMessage()); } }); 6. 消费端确认机制 消费端处理异常怎么办? 默认情况下,消费端接收消息时,消息会被自动确认(ACK),发生异常时,发送端与消息中间件无法得知消息处理情况,需要使用RabbitMQ 消息端确认机制,确认消息被正确处理 消费端ACK类型 手动ACK:消费端收到消息后,不会自动签收消息,需要我们在业务代码中显式签收消息 - 单条手动ACK : multiple = false - 多条手动ACK : multiple = true - 推荐使用单条ACK channel.basicAck(message.getEnvelope().getDeliveryTag(),false); channel.basicNack(message.getEnvelope().getDeliveryTag(),false,false); 自动ACK:消费端收到消息后,会自动签收消息 7. 消费端限流机制 业务高峰期,可能出现发送端与接收端性能不一致,大量消息被同时推给接受端,造成接受端服务奔溃 在高并发端场景下,有个微服务奔溃了,本科期间队列挤压了大量消息,微服务上线后,收到大量并发消息。将同样多端消息推给能力不同端副本,会导致部分副本异常 针对以上问题,RabbitMQ 开发了 Qos (服务质量保证) 功能,Qos功能保证了在一定树木消息违背确认前,不消费新的消息 //这样RabbitMQ就会使得每个Consumer在同一个时间点最多处理一个Message。换句话说,在接收到该Consumer的ack前,他它不会将新的Message分发给它。 channel.basicQos(1); 8. RabbitMQ的过期时间(TTL) RabbitMQ的过期时间称为 TTL (time to live), 生存时间 RabbitMQ的过期时间分为消息TTL 和 队列 TTL 消息TTL设置了单条消息的过期时间 队列TTL设置了队列中所有消息的过期时间 AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder() .deliveryMode(2) //deliveryMode=1代表不持久化,deliveryMode=2代表持久化 .contentEncoding("UTF-8") // 编码方式 .expiration("10000") // 过期时间 .headers(headers) //自定义属性 .build(); String messageBody = "发送的消息" //发送通道 channel.basicPublish( "exhange.order.restaurant", "key.restaurant", true, properties, messageBody.getBytes()); 9. 死信队列 如何转移过期的消息? 消息被设置了过期时间,过期后会直接被丢弃,直接被丢弃的消息无法对系统运行异常发出警报,需要使用RabbitMQ死信队列,收集过期消息,以供分析 什么是死信队列 队列被配置了DLX属性 (Dead-Letter-Exchange) 当一个消息变成死信(dead message)后,能重新被发布到另一个 Exchange , 这个Exchange也是一个普通交换机,死信被死信交换机路由后,一般进入一个固定队列 怎么变成死信 消息被拒绝 (reject / nack) 并且 requeue = false 消息过期(TTL到期) 队列达到最大长度 个人博客地址:http://blog.yanxiaolong.cn/
数值计算:注意精度、舍入和溢出问题 在《Effective Java》这本书中也提到这个原则,float和double只能用来做科学计算或者是工程计算,在商业计算中我们要用java.math.BigDecimal 1. Double的坑 四则运算: public static void main(String[] args) throws Exception { System.out.println(0.1+0.2); System.out.println(1.0-0.8); System.out.println(4.015*100); System.out.println(123.3/100); double amount1 = 2.15; double amount2 = 1.10; if (amount1 - amount2 == 1.05) System.out.println("OK"); } 可以看到,输出结果和我们预期的很不一样。比如,0.1+0.2 输出的不是 0.3 而是0.30000000000000004;再比如,对 2.15-1.10 和 1.05 判等,结果判等不成立。 出现这种问题的主要原因是,计算机是以二进制存储数值的,浮点数也不例外。Java 采用了IEEE 754 标准实现浮点数的表达和运算,你可以通过这里查看数值转化为二进制的 结果。 比如,0.1 的二进制表示为 0.0 0011 0011 0011… (0011 无限循环),再转换为十进制就是 0.1000000000000000055511151231257827021181583404541015625。对于计算 机而言,0.1 无法精确表达,这是浮点数计算造成精度损失的根源。 在《Effective Java》这本书中也提到这个原则,float和double只能用来做科学计算或者是工程计算,在商业计算中我们要用java.math.BigDecimal #### BigDecimal 四则运算: public static void main(String[] args) throws Exception { System.out.println(new BigDecimal(0.1).add(new BigDecimal(0.2))); System.out.println(new BigDecimal(1.0).subtract(new BigDecimal(0.8))); System.out.println(new BigDecimal(4.015).multiply(new BigDecimal(100))); System.out.println(new BigDecimal(123.3).divide(new BigDecimal(100))); } 可以看到 ,运算还是不精确,只不过是精度高了,使用 BigDecimal 表示和计算浮点数,且务必使用字符串的构造方法来初始化 public static void main(String[] args) throws Exception { System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2"))); System.out.println(new BigDecimal("1.0").subtract(new BigDecimal("0.8"))); System.out.println(new BigDecimal("4.015").multiply(new BigDecimal("100"))); System.out.println(new BigDecimal("123.3").divide(new BigDecimal("100"))); } 2. 考虑浮点数舍入和格式化的方式 关于浮点类型的四舍五入 也是千奇百怪,让人莫不着头脑 public static void main(String[] args) throws Exception { double num1 = 3.35; float num2 = 3.35f; System.out.println(String.format("%.1f", num1));//四舍五入 System.out.println(String.format("%.1f", num2)); } 如果我们希望使用其他舍入方式来格式化字符串的话,可以设置 DecimalFormat public static void main(String[] args) throws Exception { double num1 = 3.35; float num2 = 3.35f; DecimalFormat format = new DecimalFormat("#.##"); format.setRoundingMode(RoundingMode.DOWN); System.out.println(format.format(num1)); format.setRoundingMode(RoundingMode.DOWN); System.out.println(format.format(num2)); } 因此,即使通过 DecimalFormat 来精确控制舍入方式,double 和 float 的问题也可能产生意想不到的结果,所以浮点数避坑第二原则:浮点数的字符串格式化也要通过BigDecimal 进行。 public static void main(String[] args) throws Exception { BigDecimal num1 = new BigDecimal("3.35"); BigDecimal num2 = num1.setScale(1, BigDecimal.ROUND_DOWN); System.out.println(num2); BigDecimal num3 = num1.setScale(1, BigDecimal.ROUND_HALF_UP); System.out.println(num3); } 3. BigDecimal不能使用equals 案例 : public static void main(String[] args) throws Exception { BigDecimal a = new BigDecimal(0.00); BigDecimal b = new BigDecimal(0); boolean result = a.equals(b); System.out.println("a equals b -->" + result); BigDecimal c = new BigDecimal("0.00"); BigDecimal d = new BigDecimal("0"); boolean result1 = c.equals(d); System.out.println("c equals d -->" + result1); } 结果: a equals b -->true c equals d -->false 我们来看下 BigDecimal 的 equals 方法 源码,equals 比较的是 BigDecimal 的 value 和 scale,1.0 的 scale 是 1,1 的scale 是 0,所以结果一定是 false: /** * Compares this {@code BigDecimal} with the specified * {@code Object} for equality. Unlike {@link * #compareTo(BigDecimal) compareTo}, this method considers two * {@code BigDecimal} objects equal only if they are equal in * value and scale (thus 2.0 is not equal to 2.00 when compared by * this method). * * @param x {@code Object} to which this {@code BigDecimal} is * to be compared. * @return {@code true} if and only if the specified {@code Object} is a * {@code BigDecimal} whose value and scale are equal to this * {@code BigDecimal}'s. * @see #compareTo(java.math.BigDecimal) * @see #hashCode */ @Override public boolean equals(Object x) { if (!(x instanceof BigDecimal)) return false; BigDecimal xDec = (BigDecimal) x; if (x == this) return true; if (scale != xDec.scale) return false; long s = this.intCompact; long xs = xDec.intCompact; if (s != INFLATED) { if (xs == INFLATED) xs = compactValFor(xDec.intVal); return xs == s; } else if (xs != INFLATED) return xs == compactValFor(this.intVal); return this.inflated().equals(xDec.inflated()); } 如果我们希望只比较 BigDecimal 的 value,可以使用 compareTo 方法 public static void main(String[] args) throws Exception { BigDecimal c = new BigDecimal("0.00"); BigDecimal d = new BigDecimal("0"); boolean result2 = c.compareTo(d) == 0; System.out.println("c compareTo d -->" + result2); } 结果: c compareTo d -->true 4. 数值溢出问题 数值计算还有一个要小心的点是溢出,不管是 int 还是 long,所有的基本数值类型都有超出表达范围的可能性。 对Long 的最大值 进行 + 1 public static void main(String[] args) throws Exception { long l = Long.MAX_VALUE; System.out.println(l + 1); System.out.println(l + 1 == Long.MIN_VALUE); } 结果: 输出结果是一个负数,因为 Long 的最大值 +1 变为了 Long 的最小值: -9223372036854775808 true 改进: public static void main(String[] args) throws Exception { BigInteger i = new BigInteger(String.valueOf(Long.MAX_VALUE)); System.out.println(i.add(BigInteger.ONE).toString()); } 结果 : 9223372036854775808 通过 BigInteger 对 Long 的最大值加 1 一点问题都没有 个人博客地址:http://blog.yanxiaolong.cn/
Java判等问题:细节决定成败 判等问题,在我们代码中就是一句话的事情,但是这一行代码如果处理不好,不仅会出现致命的bug,下面我们就以Java中 equals、compareTo 和 Java 的数值缓存、字符串驻留等问题展开讨论 1. 注意 equals 和 == 的区别 在业务代码中,我们通常使用 equals 或 == 进行判等操作。equals是方法而 ==是操作符: 1.对基本类型,比如 int 、long、进行判断,只能使用 == ,比较对是直接值,因为基本类型对值就是其数值: 2.对引用类型,比如Integer 、Long 和 String 进行判等,需要使用 equals 进行内容判等。因为引用类型,需要使用equals进行内容判等。因为饮用类型等直接值是指针,使用 == 的话,比较的是指针,也就是两个对象在内存中的地址,即比较他们是不是同一个对象,而不是比较对象内容 结论: 比较值的内容,除了基本类型只能使用 ==外,其他类型都需要使用 equals。 案例: public static void main(String[] args) throws Exception { Integer a = 127; Integer b = 127; System.out.println(" a == b " +(a == b)); Integer c = 128; Integer d = 128; System.out.println(" c == d " + (c == d)); Integer g = new Integer(127); Integer h = new Integer(127); System.out.println(" g == h " + (g == h)); Integer i = 128; int j = 128; System.out.println(" i == j " + (i == j)); } 结果 : a == b true c == d false g == h false i == j true 在 a == b 中,编译器会把 a = 127 转换为 Integer.valueOf(127),源码可以发现,这个转换是内部其实做了缓存,使得两个 Integer 指向同一个对象 所以返回true public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } 在 c == d 中使用128 返回false ,Integer 当不符合-128 127值范围时候。记住用的:new,开辟新的内存空间,不属于IntergerCache管理区 private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } 在g == h案例中,New 出来的 Integer 始终是不走缓存的新对象。比较两个新对象,或者比较一个新对象和一个来自缓存的对象,结果肯定不是相同的对象,因此返回 false。 2. equals 没有这么简单 如果看过 Object 类源码,你可能就知道,equals 的实现其实是比较对象引用: public boolean equals(Object obj) { return (this == obj); } 重点(注意点): 不重写equals方法与“ == ”一样,用于比较对象的引用是否相等。 之所以 Integer 或 String 能通过 equals 实现内容判等,是因为它们都重写了这个方法。 String 的 equals 的实现: /** * Compares this string to the specified object. The result is {@code * true} if and only if the argument is not {@code null} and is a {@code * String} object that represents the same sequence of characters as this * object. * * @param anObject * The object to compare this {@code String} against * * @return {@code true} if the given object represents a {@code String} * equivalent to this string, {@code false} otherwise * * @see #compareTo(String) * @see #equalsIgnoreCase(String) */ public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; } Integer.equals.() /** * Compares this object to the specified object. The result is * {@code true} if and only if the argument is not * {@code null} and is an {@code Integer} object that * contains the same {@code int} value as this object. * * @param obj the object to compare with. * @return {@code true} if the objects are the same; * {@code false} otherwise. */ public boolean equals(Object obj) { if (obj instanceof Integer) { return value == ((Integer)obj).intValue(); } return false; } 个人博客地址:http://blog.yanxiaolong.cn/
数据库 -- 索引并不是万能的 索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。如果想按特定职员的姓来查找他或她,则与在表中搜索所有的行相比,索引有助于更快地获取信息。但是索引也不是万能的 ,有时候发现我们 sql 中索引不生效的,我们深入理解下索引的原理,以及误区, InnoDB是如何存储数据的? MySQL把数据存储和查询操作抽象成了存储引擎,不同的存储引擎,对数据的存储和读取方式各不相同。MySQL支持多种存储引擎,并且可以以表为粒度设置存储引擎。因为支持事物,我们最常用的是InnoDB 虽然数据保存在磁盘中,但其处理是在内存进行的。为了减少磁盘随机读取次数,InnoDB 采用页而不是行但粒度来保存数据,即数据被分成若干页,以页为单位保存在磁盘中,InnoDB的页大小,一般是16kb。各页中又一个页目录,方便按照主键查询记录。 数据页结构: 页目录通过槽把记录分成不同的小组,没个小组有若干条记录。如图所示,记录中最前面的小方块的数字,代表的是当前分组的记录条数,最小和最大的槽指向 2个特殊的伪记录。有了槽之后,我们按照主键搜索页中记录时,就可以采用二分法快速搜索,无需从最小记录开始遍历整个页中记录链表。 举例:搜索主键(pk) = 15的记录 先二分得出槽中间位是(0+6)/2=3 , 看到其指向的记录是 12 < 15 , 所以需要从 #3 槽后继续搜索; 再使用二分搜索出 #3槽和 #6槽的中间位 (3+6)/2=4.5 取整4,#4槽对应的记录是 16 > 15,所以记录一定在#4槽中; 在从 #3 槽指向的12号记录开始向下搜索3次,定位到15号记录。 聚簇索引和非聚簇索引 InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,聚簇索引就是按照每张表的主键构造一颗B+树,同时叶子节点中存放的就是整张表的行记录数据,也将聚集索引的叶子节点称为数据页。这个特性决定了索引组织表中数据也是索引的一部分; 一般建表会用一个自增主键做聚簇索引,没有的话MySQL会默认创建,但是这个主键如果更改代价较高,故建表时要考虑自增ID不能频繁update这点。 我们日常工作中,根据实际情况自行添加的索引都是辅助索引,辅助索引就是一个为了需找主键索引的二级索引,现在找到主键索引再通过主键索引找数据; B+ 树的特点包括:: 最底层的节点叫作叶子节点,用来存放数据;: 其他上层节点叫作非叶子节点,仅用来存放目录项,作为索引;: 非叶子节点分为不同层次,通过分层来降低每一层的搜索量;: 所有节点按照索引键大小排序,构成一个双向链表,加速范围查找。 因此,InnoDB 使用 B+ 树,既可以保存实际数据,也可以加速数据搜索,这就是聚簇索引。如果把上图叶子节点下面方块中的省略号看作实际数据的话,那么它就是聚簇索引的示 意图。由于数据在物理上只会保存一份,所以包含实际数据的聚簇索引只能有一个。 InnoDB 会自动使用主键(唯一定义一条记录的单个或多个字段)作为聚簇索引的索引键(如果没有主键,就选择第一个不包含 NULL 值的唯一列)。上图方框中的数字代表了索 引键的值,对聚簇索引而言一般就是主键。 为了实现非主键字段的快速搜索,就引出了二级索引,也叫作非聚簇索引、辅助索引。二级索引,也是利用的 B + 数的数据结构 这次二级索引的叶子节点中保存的不是实际数据,而是主键,获得主键值后去聚簇索引中获得数据行。这个过程就叫作回表。 回表是什么意思?就是你执行一条sql语句,需要从两个b+索引中去取数据 表tbl有a,b,c三个字段,其中a是主键,b上建了索引,然后编写sql语句 SELECT * FROM tbl WHERE a=1 这样不会产生回表,因为所有的数据在a的索引树中均能找到 SELECT * FROM tbl WHERE b=1 这样就会产生回表,因为where条件是b字段,那么会去b的索引树里查找数据,但b的索引里面只有a,b两个字段的值,没有c,那么这个查询为了取到c字段,就要取出主键a的值,然后去a的索引树去找c字段的数据。查了两个索引树,这就叫回表。索引覆盖就是查这个索引能查到你所需要的所有数据,不需要去另外的数据结构去查。其实就是不用回表。 考虑额外创建二级索引的代价 创建二级索引的代价,主要表现在维护代价、空间代价和回表代价三个方面。 维护代价:创建 N 个二级索引,就需要再创建 N 棵 B+ 树,新增数据时不仅要修改聚簇索引,还需要修改这 N 个二级索引。 空间代价:虽然二级索引不保存原始数据,但要保存索引列的数据,所以会占用更多的空间 回表代码:二级索引不保存原始数据,通过索引找到主键后需要再查询聚簇索引,才能得到我们想要的数据 不是所有针对索引列的查询都能用上索引 1. 索引只能匹配列前缀 比如下面的 LIKE 语句,搜索 name 后缀为 name123 的用户无法走索引,执行计划的 type=ALL 代表了全表扫描: EXPLAIN SELECT * FROM person WHERE NAME LIKE '%name123' LIMIT 100 把百分号放到后面走前缀匹配,type=range 表示走索引扫描,key=name_score 看到实际走了索引 EXPLAIN SELECT * FROM person WHERE NAME LIKE 'name123%' LIMIT 100 2. 条件涉及函数操作无法走索引。 比如搜索条件用到了 LENGTH 函数,肯定无法走索引 EXPLAIN SELECT * FROM person WHERE LENGTH(NAME)=7 3.联合索引只能匹配左边的列 对 name 和 score 建了联合索引,但是仅按照 score 列搜索无法走索引 EXPLAIN SELECT * FROM person WHERE SCORE>45678 个人博客地址:http://blog.yanxiaolong.cn/
线程池:业务代码常见的问题 在程序中,我们会使用各种池优化缓存创建昂贵的对象,比如线程池、连接池、内存池。一般是预先创建一些对象放入池中,使用的时候直接取出使用,用完归还以便复用,还会通过一定策略调整池中缓存的对象数量,实现动态伸缩。 由于线程的创建比较昂贵,随意、没有控制地创建大量线程会造成性能问题,因此短平快的任务一般优先考虑使用线程池来处理,而不是直接创建线程 1. 线程池的声明需要手动进行 Java 中的 Executors 类定义了一些快捷的工具方法,来帮助我们快速创建线程池。《阿里 巴巴 Java开发手册》中提到,禁止使用这些方法来创建线程池,而应该手动 new ThreadPoolExecutor来创建线程池。这一条规则的背后,是大量血淋淋的生产事故,最典 型的就是 newFixedThreadPool 和 newCachedThreadPool,可能因为资源耗尽导致 OOM 问题。 阿里巴巴文档: 测试 OOM问题 : 来初始化一个单线程的 FixedThreadPool,循环 1 亿次向线程池提交任务,每个任务都会创建一个比较大的字符串然后休眠一小时 public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(100000); System.out.println("开始执行"); for (int i = 0; i < 100000000; i++) { executorService.execute(() -> { String payload = IntStream.rangeClosed(1, 1000000) .mapToObj(__ -> "a") .collect(Collectors.joining("")) + UUID.randomUUID().toString(); System.out.println("等待一小时开始"); try { TimeUnit.HOURS.sleep(1); }catch (Exception e){ log.info(payload); } }); } executorService.shutdown(); executorService.awaitTermination(1,TimeUnit.HOURS); } 结果:java.lang.OutOfMemoryError 错误 首先我们看下 newFixedThreadPool 方法的源码,发现,线程池的工作队列直接 new 了一个 LinkedBlockingQueue, /** * Creates a thread pool that reuses a fixed number of threads * operating off a shared unbounded queue. At any point, at most * {@code nThreads} threads will be active processing tasks. * If additional tasks are submitted when all threads are active, * they will wait in the queue until a thread is available. * If any thread terminates due to a failure during execution * prior to shutdown, a new one will take its place if needed to * execute subsequent tasks. The threads in the pool will exist * until it is explicitly {@link ExecutorService#shutdown shutdown}. * * @param nThreads the number of threads in the pool * @return the newly created thread pool * @throws IllegalArgumentException if {@code nThreads <= 0} */ public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } 点进去查看 LinkedBlockingQueue构造方法 是一个 Integer.MAX_VALUE长度的队列,可以认为是无界的 /** * Creates a {@code LinkedBlockingQueue} with a capacity of * {@link Integer#MAX_VALUE}. */ public LinkedBlockingQueue() { this(Integer.MAX_VALUE); } 虽然 newFixedThreadPool 可以把工作线程控制在固定的数量上,但任务队列是无界但。如果任务较多并且执行较慢但话,队列可能会快速积压,撑爆内存导致OOM 测试newCachedThreadPool 如果我们把 newFixedThreadPool 改成 newCachedThreadPool方法来获取线程池。程序运行不久后,同样会看到 OOM 异常 java.lang.OutOfMemoryError: unable to create new native thread 源码: /** * Creates a thread pool that creates new threads as needed, but * will reuse previously constructed threads when they are * available, and uses the provided * ThreadFactory to create new threads when needed. * @param threadFactory the factory to use when creating new threads * @return the newly created thread pool * @throws NullPointerException if threadFactory is null */ public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), threadFactory); } 这种线程池的最大线程数是 Integer.MAX_VALUE ,认为是没有上限的,可以认为是没有上限的,而其工作队列 SynchronousQueue 是一个没有存储空间的阻塞队列。这意味着,只要有请求到来,就必须找到一条工作线程来处理,如果当前没有空闲的线程就再创建一条新的。由于我们的任务需要一小时才能完成,大量的任务进来后会创建大量的线程,我们知道线程是分配一定的内存空间做为线程栈,比如 1MB,因此无限创建线程必然会导致OOM 我们不建议使用 Executors 提供的两种快捷的线程池,原因如下: 我们需要根据自己的场景、并发情况来评估线程池的几个核心参数,包括核心线程数、最大线程数、线程回收策略、工作队列的类型,以及拒绝策略,确保线程池的工作行为符合要求,一般都需要设置有界的工作队列和 可控的线程数。 任何时候,都于根伟自定义线程池指定有意思的名称,以方便排查问题。当出现线程数量暴增、线程死锁、线程占用大量CPU 、线程执行出现异常等问题时,我们往往会抓取线程栈,此时,有意义的线程名称,就可以方便我们定位问题。 总结 线程池工作行为: 如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务; 如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务; 如果队列已经满了,则在总线程数不大于maximumPoolSize的前提下,则创建新的线程 如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理; 如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。 2.确认线程池是否在复用 在生产环境中,监控一直报警当前使用线程数太多,一会又将下来,但是当前用户访问量也不是很大 通过代码排查 发现项目中使用了 Executors.newCachedThreadPool(); 创建线程池使用,我们知道newCachedThreadPool 会在需要时创建必要多的线程,业务代码的一次业务操作会向线程池提交多个慢任务,这样执行一次业务操作就会开启多个线程,如果业务操作量较大,的确有可能一下子开启几千个线程 源码发现 /** * Creates a thread pool that creates new threads as needed, but * will reuse previously constructed threads when they are * available. These pools will typically improve the performance * of programs that execute many short-lived asynchronous tasks. * Calls to {@code execute} will reuse previously constructed * threads if available. If no existing thread is available, a new * thread will be created and added to the pool. Threads that have * not been used for sixty seconds are terminated and removed from * the cache. Thus, a pool that remains idle for long enough will * not consume any resources. Note that pools with similar * properties but different details (for example, timeout parameters) * may be created using {@link ThreadPoolExecutor} constructors. * * @return the newly created thread pool */ public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); } 它的核心线程数是0,而最大线程数 Integer的最大值,一般来说机器都没那么大内存给它不断使用,而 keepAliveTime 是60秒,也就是在 60秒之后所有的线程都是可以回收的,采用SynchronousQueue装等待的任务,这个阻塞队列没有存储空间,这意味着只要有请求到来,就必须要找到一条工作线程处理他,如果当前没有空闲的线程,那么就会再创建一条新的线程。 所以我们在使用线程池要根据任务的“轻重缓急”来指定线程池的核心参数,包括线程数、回收策略和任务队列:: 1. 对于执行比较慢、数量不大的 IO 任务,或许要考虑更多的线程数,而不需要太大的队列。: 2. 而对于吞吐量较大的计算型任务,线程数量不宜过多,可以是 CPU 核数或核数 *2(理由是,线程一定调度到某个 CPU 进行执行,如果任务本身是 CPU 绑定的任务,那么过多的线程只会增加线程切换的开销,并不能提升吞吐量),但可能需要较长的队列来做缓冲。 个人博客地址:http://blog.yanxiaolong.cn/
2020年12月
2020年11月
2020年10月