延时消息是项目中经常用到的一种解决方案, 本篇文章我们就来尝试探探它到底是如何实现的?以及有哪些方案。
为了更能直观的感受,我们还是通过案例来进行表述。
万年不变老案例:下单5分钟后,支付超时取消订单。
public void order(){ // 假设这里已经下单并得到了订单id String orderId = UUID.randomUUID().toString(); new Thread(() -> { try { // 延时5分钟 TimeUnit.MINUTES.sleep(5L); // 查询订单是否支付,未支付则取消 boolean isPay = checkOrderPayState(orderId); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); }
咱先不论回不回家等通知,你就说能不能用?
诶,它还真能用,但不多。
主要有两个问题:
1、性能很差,一个订单就开个线程等5分钟,好家伙,多来点订单直接内存溢出了。
老板:业务上不去原来就是你小子啊
2、服务停机,5分钟内的订单取消逻辑全部消失了。
用户:咦,咋昨天的订单还能支付勒?
我们先想想第一个问题咋解决。
既然问题主要在于一个订单就会开一个线程,那我能不能把线程省着点用?
省线程?线程复用?这不直接触发了关键字:线程池。
试试?
private static final ExecutorService executor = new ThreadPoolExecutor(5, 20, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(60)); @Test public void orderPro() throws IOException { // 假设这里已经下单并得到了订单id String orderId = UUID.randomUUID().toString(); executor.execute(() -> { try { // 延时5分钟 TimeUnit.MINUTES.sleep(5L); // 查询订单是否支付,未支付则取消 boolean isPay = checkOrderPayState(orderId); log.info("订单支付状态:{}", isPay); } catch (InterruptedException e) { e.printStackTrace(); } }); }
这里阿紫先补充一下线程池的执行机制,以防有小伙伴吃懵逼果。
以上代码的线程池参数分别为:核心线程数5,最大线程数20,线程空闲超时时间1分钟,任务队列容量60.
执行机制是这样的:
1、线程池收到任务,先判断核心线程数是否已满(达到5)
2、未满则创建线程执行任务
3、已满则将任务放入任务队列
4、如果任务队列也满了,任务放不进,则继续创建线程(非核心线程)
5、如果非核心线程也满了(达到20),则拒绝该任务
线程空闲超时时间作用:如果非核心线程在空闲超时时间(一分钟)内没收到任务,则回收该线程
核心线程和非核心线程只是线程池的一个概念,用来区分哪些线程可以回收,实际上没有区别。
叠个甲:核心线程实际也可以被回收,给个允许核心线程超时的参数就行。
好,现在就来看看这个改造可不可行?
假设现有6个任务
订单创建时间 | 期望检查时间 | |
---|---|---|
订单1 | 10:00:00 | 10:05:00 |
订单2 | 10:00:30 | 10:05:30 |
订单3 | 10:01:00 | 10:06:00 |
订单4 | 10:01:30 | 10:06:30 |
订单5 | 10:02:00 | 10:07:00 |
订单6 | 10:02:30 | 10:07:30 |
由于线程池有5个核心线程,所以前5个订单都正常交给了线程执行。
到第6个订单时,由于核心线程数已满,所以第6个订单放入队列。直到第一个订单的线程执行完毕,也就是在10:05:00
. 这时候线程开始执行第6个订单的检查任务,检查时间为10:05:00
+ 5分钟 = 10:10:00
程序出错,问题原因在于我们期望创建订单后马上执行任务,但是由于线程数不足,任务在队列中等待了一段时间后才执行。
程序的逻辑应该从等待5分钟
改为等待至订单支付超时的时间点
public void orderPlus() throws IOException { // 假设这里已经下单并得到了订单id String orderId = UUID.randomUUID().toString(); // 计算订单检查是否支付超时时间点 long checkTime = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5); executor.execute(() -> { // 延时等待至订单超时时间 Thread.sleep(checkTime - System.currentTimeMillis()); // 查询订单是否支付,未支付则取消 boolean isPay = checkOrderPayState(orderId); log.info("订单支付状态:{}", isPay); }); }
到这里好像问题已经解决了,程序的线程数可控,程序执行也没有问题。
但如果细想一下,我们就会发现:先创建的订单总是先执行任务,这好像是句废话,因为订单的延时时间是固定的5分钟,所以任务天然是按照订单创建顺序排好队等待执行的。
但这句废话会让我们得出一个结论:一般情况下,有且只有一个线程会执行任务。我们不妨把线程池的线程数改为1进行验证一下,就会发现确实如此。
不知道为什么,说起废话总会让我想起鸽巢原理
既然一个线程就能解决的事情,那我们就尝试回归一下,用最开始的单线程试试。
// 定义阻塞队列 private static final LinkedBlockingQueue<Order> checkQueue = new LinkedBlockingQueue<>(100); @Test public void orderProMax() { // 假设这里已经下单并得到了订单id String orderId = UUID.randomUUID().toString(); // 计算订单检查是否支付超时时间点 long checkTime = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5); // 将任务放入队列 checkQueue.offer(new Order(orderId, checkTime)); } static { // 开启线程从队列中获取任务 new Thread(() -> { try { while (true) { Order order = checkQueue.take(); // 延时等待至订单超时时间 Thread.sleep(order.checkTime - System.currentTimeMillis()); // 查询订单是否支付,未支付则取消 boolean isPay = checkOrderPayState(order.orderId); log.info("订单支付状态:{}", isPay); } } catch (InterruptedException e) { e.printStackTrace(); } }).start(); }
可能有小伙伴好奇为什么线程里面
Order order = checkQueue.take();
不会出现order为null的情况因为
Order order = checkQueue.take();
take方法原理是当队列有元素时取出,无元素时阻塞等待。
现在的方案是不是又更优雅了一些?只用了一个线程,程序执行正常。
案例中的检查时间固定是5分钟后,这时候业务发生变更,有一类特殊的订单,检查时间是2分钟后,程序还能hold住吗?
假设:
订单1创建时间为10:00:00
,5分钟后10:05:00
检查.
订单2创建时间为10:01:00
,2分钟后10:03:00
检查.
这时候在10:04:00
时,订单1还在等待至超时时间,但是订单2已经超时了。
GG!
分析一下,一般情况下,有且只有一个线程会执行任务
这个结论在此案例中仍然是起效的。
问题在于任务的执行顺序出现了异常。我们应该让订单2的任务排在订单1前面?
所以这个队列应该是可以排序的,并且排序方式是按照订单检查时间顺序从早到晚。
又更进一步分析,由于程序每次只取第一个任务执行,所以我们只需要保证队列中第一个任务是最先执行的就可以了。
恰好就有个这样的数据结构:堆!
我们待会再来论证为什么用堆而不是纯粹的排序列表
定义:
完全二叉树: 除去最后一层外,其余层为满二叉树状态,并且最后一层的叶子结点都往左排列
满二叉树: 除叶子结点外,其他的结点都有两个子结点
拿上图的大顶堆举例,要注意的是:堆的结构是每个结点比它的子结点大,而不是每一层比下一层大。上图第二层的17
就比18
要小,完全可能出现最后一层的结点比根的另一分叉都要大的情况。
那堆这个数据结构是怎么插入新结点呢?
1、首先将新结点放到末尾
2、将结点与父结点相互比较,如果比父节点大,则进行交换,否则插入结束
25比此时的父结点30要小,所以插入结束
如何取值?取值也可以认为是删除根结点
1、与末尾结点交换
2、从子结点中选出较大的子结点,进行比较,若比子节点小,则进行交换,否则堆化完毕
3、删除结点
分析以上过程,不难看出,堆的新增和删除的时间复杂度都是O(logn)
, 而排序列表插入时间复杂度是O(n)
,删除头结点是O(1)
数据结构选定了,还有个问题没解决。
正在执行中的线程如何得知有了一个更早执行的任务进入了队列呢?这时候就要唤醒线程,让线程重新获取最近的任务。
案例代码中我们使用的是sleep
方式让线程休眠,可以采用interrupt
线程中断的方式将线程唤醒。
但java中还有更为优雅的方式,那就是park
机制。
public void testPark() throws InterruptedException { Thread thread = new Thread(() -> { log.info("线程开始休眠"); LockSupport.park(this); log.info("线程已被唤醒"); }); thread.start(); // 模拟2秒后有新的任务 Thread.sleep(2000); log.info("放入新任务"); // 唤醒线程 LockSupport.unpark(thread); }
LockSupport.park(this);
会将当前线程阻塞
LockSupport.unpark(thread);
会唤醒指定的线程
打印日志如下
10:44:53.493 [Thread-1] INFO com.xxx - 线程开始休眠10:44:55.497 [main] INFO com.xxx - 放入新任务10:44:55.500 [Thread-1] INFO com.xxx - 线程已被唤醒
1、首先,队列我们改为java中的PriorityBlockingQueue
优先级队列,此队列的内部结构即为堆数据结构
// 定义阻塞队列 private static final BlockingQueue<Order> checkQueue = new PriorityBlockingQueue<>(100);
2、定义订单类,由于使用优先级队列,需要实现Comparable
接口用于排序
class Order implements Comparable<Order> { String orderId; long checkTime; Order(String orderId, long checkTime) { this.orderId = orderId; this.checkTime = checkTime; } // 为方便使用封装一个获取延时时间方法 public long getDelay() { return checkTime - System.currentTimeMillis(); } @Override public int compareTo(Order o) { return (int) (this.getDelay() - o.getDelay()); } }
3、编写在线程中获取订单方法
// 定义静态变量用于唤醒线程时使用 private static Thread thread = null; private void start() { // 开启线程从队列中获取任务 thread = new Thread(() -> { while (true) { try{ Order order = take(); // 查询订单是否支付,未支付则取消 boolean isPay = checkOrderPayState(order.orderId); log.info("执行完毕,订单id:{}, 检查时间:{}", order.orderId, df.format(new Date(order.checkTime))); }catch (Exception e){ log.info(e.getMessage(), e); } } }); thread.start(); } private Order take() { while (true) { Order order = checkQueue.peek(); // 无订单则直接阻塞等待 if (order == null) { LockSupport.park(this); } else { if (order.getDelay() <= 0) { return checkQueue.poll(); } // 延时等待至订单超时时间 LockSupport.parkNanos(this, TimeUnit.MILLISECONDS.toNanos(order.getDelay())); } } }
4、编写将订单放入队列方法
public void orderPlusMax() { // 假设这里已经下单并得到了订单id String orderId = String.valueOf(orderIdGenerator.incrementAndGet()); // 随机一个20秒内的检查时间 long checkTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(new Random().nextInt(20)); // 将任务放入队列 if (checkQueue.offer(new Order(orderId, checkTime))) { log.info("订单id:{}, 检查时间:{}", orderId, df.format(new Date(checkTime))); // 判断新放入的订单是否是第一个,是则说明新的订单是最早执行的 if (orderId.equals(checkQueue.peek().orderId)) { // 唤醒线程 LockSupport.unpark(thread); } } }
5、测试
public void test() throws IOException, InterruptedException { start(); for (int i = 0; i < 6; i++) { orderPlusMax(); } }
测试效果
11:37:27.751 [main] INFO com.xx - 订单id:1, 检查时间:11:37:3211:37:27.752 [main] INFO com.xx - 订单id:2, 检查时间:11:37:4211:37:27.752 [main] INFO com.xx - 订单id:3, 检查时间:11:37:3111:37:27.752 [main] INFO com.xx - 订单id:4, 检查时间:11:37:3911:37:27.752 [main] INFO com.xx - 订单id:5, 检查时间:11:37:3311:37:27.752 [main] INFO com.xx - 订单id:6, 检查时间:11:37:4111:37:31.753 [Thread-1] INFO com.xx - 执行完毕,订单id:3, 检查时间:11:37:3111:37:32.756 [Thread-1] INFO com.xx - 执行完毕,订单id:1, 检查时间:11:37:3211:37:33.758 [Thread-1] INFO com.xx - 执行完毕,订单id:5, 检查时间:11:37:3311:37:39.754 [Thread-1] INFO com.xx - 执行完毕,订单id:4, 检查时间:11:37:3911:37:41.757 [Thread-1] INFO com.xx - 执行完毕,订单id:6, 检查时间:11:37:4111:37:42.757 [Thread-1] INFO com.xx - 执行完毕,订单id:2, 检查时间:11:37:42
以上
优先级队列+LockSupport
其实就是Java中DelayQueue
的原理什么?为什么我不直接扒开
DelayQueue
的源码给你看,哦,它的源码太简单了,我看你看着会想睡觉。
在一步一步的分析优化下,性能问题我们解决了,订单不同的延时时间问题我们也解决了。
但是还是不能上生产,因为服务停机导致取消逻辑消失的问题还没解决。
细想一下,这里的根本原因是因为队列是在内存里,服务停机导致内存数据清空。
如果我们把这个队列进行了持久化,是不是就可行了?
第一种方式,我们可以借用数据库,每次将任务放入队列的同时,往MySQL中插入一条任务数据,执行完任务更新任务状态,程序重启时查询未执行的任务放入队列
1、定义任务类
public class Task { private Order order; // 执行状态:1、未执行 2、执行完成 private Integer state;}
2、增加插入逻辑
public void orderPlusMax() { // ...... Order order = new Order(orderId, checkTime); if (checkQueue.offer(order)) { log.info("订单id:{}, 检查时间:{}", orderId, df.format(new Date(checkTime))); // 插入数据到MySQL saveTask(order); // ...... } }
3、订单检查后将任务状态修改为执行中
private void start() { // 开启线程从队列中获取任务 thread = new Thread(() -> { while (true) { // ...... log.info("执行完毕,订单id:{}, 检查时间:{}", order.orderId, df.format(new Date(order.checkTime))); // 修改任务状态 updateTaskState(order.orderId); // ...... } }); }
4、程序重启时查询未执行的任务放入队列
private void rePushTask(){ // 查询未执行的任务列表 List<Task> taskList = queryTask(); for (Task task : taskList) { // 将任务放入队列 checkQueue.offer(task.getOrder()); } }
当然,还有种更为轻量的方法就是利用JVM的程序停止的回调函数, 在程序停止时将队列中还未执行的任务进行持久化。
Runtime.getRuntime().addShutdownHook(new Thread(() -> { // 读取队列中的所有任务 for (Order order : checkQueue) { // 持久化保存 saveTask(order); } }));
但是这种方法只能在程序正常停止的情况下使用,非正常停止(kill)则失效,故不推荐。
MySQL有一个问题就是不太好做分布式,而Redis的原子性(单线程)特性则很容易做到。
我们当然也可以直接像用MySQL一样直接把Redis当数据库用。用Redis的List数据类型就可以了。
但如果你熟悉Redis数据类型,再类比一下以上案例,会不会想到:
案例中的优先级队列
是有序
的数据结构,Redis也有一个zset
的有序集合
,我们能不能直接把内存中的优先级队列
直接换成zset
?
在这之前,我们先看看Redis的zset
数据类型用法
public void testZSet(){ String key = "task"; // 往集合中添加一个元素 参数分别为:集合key, 元素值, 分数 redisTemplate.opsForZSet().add(key, "2", 2); // 从集合中按从小到大的方式取出元素,参数分别为:集合key, 分数最小值, 分数最大值 // 最小值和最大值是边界,-1, 6表示取出集合中分数为-1到6(闭区间)的元素 Set<String> set = redisTemplate.opsForZSet().rangeByScore(key, -1, 6); // 取出元素并带分数 TypedTuple类型有两个变量:value, score Set<TypedTuple<String>> set2 = redisTemplate.opsForZSet().rangeByScoreWithScores(key, -1, 6); // 取出元素,最小值-1,最大值6,从offset 0开始,总数取1个 Set<String> set3 = redisTemplate.opsForZSet().rangeByScore(key, -1, 6, 0, 1); redisTemplate.delete(key); }
改造流程图
更改代码
1、注入Redis
@Resource private RedisTemplate<String, String> redisTemplate;
2、修改创建订单代码
public void orderPlusMax() { // 假设这里已经下单并得到了订单id String orderId = String.valueOf(orderIdGenerator.incrementAndGet()); // 随机一个20秒内的检查时间 long checkTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(new Random().nextInt(20)); // 将任务放入队列 if (Boolean.TRUE.equals(zSetOperations.add(key, orderId, checkTime))) { log.info("订单id:{}, 检查时间:{}", orderId, df.format(new Date(checkTime))); // 获取第一个订单 Set<String> orderSet = zSetOperations.rangeByScore(key, 0, Long.MAX_VALUE, 0, 1); Optional<String> first = orderSet.stream().findFirst(); String redisOrderId = first.get(); // 判断新放入的订单是否是第一个,是则说明新的订单是最早执行的 if (orderId.equals(redisOrderId)) { // 唤醒线程 LockSupport.unpark(thread); } } }
3、修改获取订单代码
private Order take() { while (true) { // 获取第一个订单 Set<TypedTuple<String>> typedTuples = zSetOperations.rangeByScoreWithScores(key, 0, Long.MAX_VALUE, 0, 1); Optional<TypedTuple<String>> first = typedTuples.stream().findFirst(); // 无订单则直接阻塞等待 if (!first.isPresent()) { LockSupport.park(this); } else { String orderId = first.get().getValue(); Double checkTime = first.get().getScore(); Order order = new Order(orderId, checkTime.longValue()); if (order.getDelay() <= 0) { // 删除zset中的订单 zSetOperations.remove(key, orderId); return order; } // 延时等待至订单超时时间 LockSupport.parkNanos(this, TimeUnit.MILLISECONDS.toNanos(order.getDelay())); } } }
对比来看,Redis的方式是否比MySQL更为简洁呢?当然,Redis一定要开启持久化配置,否则redis挂了同样也会数据丢失。
虽然最后进一步了优化,解决了持久化问题,但是该方案仍然只局限于单机环境,问题的根本原因在于LockSupport的机制是线程内通信,而不是进程间的通信。这个问题又该如何解决?
虽然还有那么多虽然,但是已经写了那么多,容我下篇继续可好?
本篇文章如之前所言,延时消息确实涉及太多的知识点,大家可以自己算一下~
最后,如果可以的话,还请点赞转发关注,阿紫非常感谢大家的捧场!
]]>This is my little brother, George.这是我的弟弟乔治
弟弟 little brother/younger brother哥哥 big brother/older brother妹妹 little sister/younger sister姐姐 big sister/older sister
little brother/big brother/little sister/big sister 这种说法感觉要更亲昵一些.
elder brother /elder sister 这种说法比较老旧,用的少,类似兄长/家姐
This is Mummy Pig.这是我的妈妈。And this is Daddy Pig.这是我的爸爸。
猪妈妈是 mommy pig 而不是 pig mommy,英文比较喜欢先说重点,猪妈妈的重点是妈妈,而不是介绍猪,猪爸爸也是一样,是 Daddy Pig 而不是 Pig daddy
Muddy Puddles.泥坑
泥泞的是 muddy,是个形容词,puddle 是水坑的意思,muddy puddle 就是泥坑,但是这里说的不是一个泥坑而是指所有的泥坑,所以要加s
It is raining today.今天是雨天。
形容天气的句子一般都用 it 做主语来代表天气,这句话描绘的场景就是正在下雨。那就用现在进行时,It is raining. 同理如果是正在下雪就是 It is snowing. 还可以在最后加上时间,today,英语里时间地点这种词都喜欢放在最后,所以就是 It is raining today.
So, Peppa and George cannot play outside.所以佩奇和乔治不能在外边玩。
玩是 play,外面是 outside所以佩奇和乔治不能玩 就是So, Peppa and George can’t play. 那么所以佩奇和乔治不能在外边玩。在最后再加个 outside 就行了,So, Peppa and George can’t play outside. 注意这里没有介词,不要说 Peppa and George can’t play at outside.因为外面它并不是一个具体地点,如果说,所以佩奇和乔治不能在学校玩。那可以说So, Peppa and George can’t play at school. 而 outside 可以把它理解为一个大方向,用来补充说明一下 play 的方向,所以直接说 play outside 就可以了。
Daddy, it's stopped raining.爸爸,现在雨停了。
当形容一个刚才一直在发生的动作现在结束了,或者完成了,有了个结果的时候就可以用现在完成时have/has+动词的过去分词.雨停了,形容天气用 it 开头代表天气,所以是 it has stopped,但光说 it has stopped不知道是啥停了,所以在最后再加上一个 raining 来代表下雨这件事。如果是雪停了,就说 it has stopped snowing. 口语里 it 和 has 经常会缩略成 it's,it‘s stopped raining,it‘s stopped snowing
Can we go out to play?我们能出去玩吗?
出去 go out 出去玩就是 go out to play我能干嘛干嘛吗?Can I..... 我们能干嘛干嘛吗?Can we..... 所以整句话就是Can we go out to play?
Alright, run along you two.好的,你们两个去玩吧。
小朋友想去玩,征求你的同意的时候,你应该怎么说呢,就是 alright,run along,alright 表示好的好吧,你说 ok 也可以,不重要。run along 就是去吧,去玩吧的意思。通常都是对小朋友说的,对成年人这么说就感觉很没有礼貌,就像你用中文跟成年人说,去吧,去玩吧就也挺冒犯的。除非你们的关系就是那种类似大人跟孩子的关系。一个小朋友要去玩,你说 run along,两个小朋友要去玩,可以说run along you two,三个小朋友可以说 run along you three
Peppa loves jumping in muddy puddles.佩奇喜欢在泥坑里玩。
这句话如果是佩奇喜欢泥坑那就可以说 Peppa loves muddy puddles. 那么在泥坑里玩这里指的情景其实是在泥坑里跳啊跳,jump 是跳的意思,在泥坑里玩就可以说 jump in muddy puddles但是连起来Peppa loves jump in muddy puddles.就不对了love 是动词,正常就应该是 I love 啥,啥这个位置应该是个名词块,muddypuddles 就是个名词块,泥坑的意思,而 jump in muddy puddles,在泥坑里跳是个动作,它一个动作放在名词块的位置,怎么行呢,除非,它把自己伪装成名词块,也就是在 jump 后面加个 ing,把动词 jump 变成动名词 jumping 就可以了。也可以在后面加个to,可以这么理解,I love 啥/ I love to 干啥。所以在 to 后面就是动词位置,就可以用原形,而啥是个名词块的位置,你得把动作加个 ing 伪装成名词块。所以这句话也可以说:Peppa loves to jump in muddy puddles
Peppa. lf you jump in muddy puddles, you must wear your boots.佩奇,如果你要在泥坑里跳,你必须要穿上靴子才行。
如果你要。。。。。你必须要。。。。。。If you.....you must......穿上靴子,穿是 wear,靴子是 boot,但是一般都不会穿一只靴子的,两只脚都要穿,所以是两只靴子,加个 s,wear your boots.
George, let's find some more puddles.乔治,我们再去找几个泥坑跳吧。
提出我们一起去干嘛干嘛的建议,那就用 Let’s 开头,Let‘s 干嘛呢,再找几个泥坑,不用去管这个跳吧这两个字,不要逐字翻译,要想的是现在这个场景,怎么表达清楚意思就行了,找泥坑当然是去跳的,不然还去喝水吗,所以就表达清楚“我们再去找几个泥坑吧”就行了。这句话如果只是说,我们去找几个泥坑,我们去找些泥坑,就可以说Let's find some muddy puddles. 那“我们再去找几个泥坑吧”多了个再,就在 some 后面加个 more 就可以了,就像你说:要喝点茶吗?Would you like some tea?那如果现在人家已经喝了一些了,你问要再喝点茶吗?就是Would you like some more tea?
Peppa and George are having a lot of fun.佩奇和乔治玩得很开心。
have fun 是一个惯用词组,字面意思是拥有乐趣,拥有快乐,实际上是指玩得开心。have a lot of fun 也是一个惯用词组,意思是玩得很开心,a lot of 是很多的意思。一般形容别人玩得很开心就用 have a lot of fun 个惯用词组,如果别人要去哪里玩你祝别人玩得开心,就用 have fun 这个词组,这就跟中文一样的,人家跟你说我要去三亚玩啦,你说,哇,玩得开心 have fun. 你不会说玩得很开心 have a lot of fun这里的场景是佩奇和乔治正玩着呢,玩得很开心,所以用现在进行时。Peppa and George are having a lot of fun.
Peppa has found a little puddle.佩奇找到了一个小泥坑。George has found a big puddle.乔治找到了一个大泥坑。
这里是找到了,用现在完成时:have/has + 动词的过去分词找到是 find,过去分词 found,佩奇是第三人称单数,has found
Look, George. There's a really big puddle. 你看,乔治。那里有一个很大的泥坑。
如果这句话是,看!我有个大泥坑。就是,look ,I have a big puddle. 但现在要说的是,那里有个大泥坑,是某个位置有某个东西,不是某个人有某个东西,某个人有某个东西可以用 have,某个地方有某个东西要用 there be 结构。比如,你跟朋友逛商场呢,看到了一家海底捞,你说:那里有家海底捞 There is a HaiDiLao. 桌子上有两本书 There are two books on the table. 所以那里有个大泥坑就可以说 There is a big puddle. there 和 is 可以缩略成 There’s如果要强调这个泥坑很大,可以在 big 前面加个 really 来加强一下语气和程度,big 是大,really big 就是很大,所以这句话是Look, George. There's a really big puddle.
George wants to jump into the big puddle first.乔治想第一个跳到泥坑里去玩。
先说乔治想跳到泥坑里去玩。这里的动作是乔治想跳到这个大泥坑里去,这个大泥坑就是他们刚才找到的那个大泥坑,所以用 the big puddle 来形容这个泥坑,那跳到这个泥坑里说 jump in the big puddle合适吗?不合适,因为 jump in 表达的感觉就是在这个大泥坑里跳啊跳,就像刚才那些句子都是指的乔治和佩奇喜欢在泥坑里跳啊跳,所以都用的 jump in,而这里的动作是从旁边跳进来,要描述跳进来这个动作,用 jump into 比较合适,into 就有个方向感。那乔治想要干嘛干嘛就是, George wants to .... George wants to jump ino the big puddle. 他想第一个跳进去,在句子最后加个 first 就可以了,整句话就是George wants to jump into the big puddle first.
Stop, George.l must check if it's safe for you.等一下,乔治。我得检查一下这里安不安全。
等一下,乔治,这里就是让乔治停下,直接说 stop 就行了,因为乔治正要跳嘛,佩奇阻止他,佩奇说,我得检查一下这里安不安全,我得检查一下我得确认一下....是不是.....I must check if........ 比如,出门的时候你可以说:I must check if I have my phone. 我得检查一下我带没带手机。吃瓜的时候你可以说:I must check if it's the real thing. 我得确认一下是不是真事。那佩奇说 我得检查一下这里是不是安全,如果是安全的,就是说对乔治是安全的,她这里是要帮乔治检查,所以可以说 it's safe for you. 那我得检查一下这里是不是安全.就可以说I must check if it's safe for you.
Sorry, George. It's only mud.对不起,乔治。只是些泥而已。
muddy 是泥泞的的意思,是个形容词,而 mud 是名词,泥的意思,是泥就可以说 It’s mud. 只是泥就在泥前面加个 only 就可以了, It's only mud.
Come on, George. Let's go and show Daddy.走,乔治。我们去给爸爸看看。
这里是佩奇想叫上乔治跟她一起去给爸爸展示身上的泥,给别人展示什么东西的时候可以用 show 这个词,比如说,你在网上看到一个图特别搞笑,要给朋友看,就可以说,I’ll show you a picture.我给你看个图。那我们给爸爸看,这里又是佩奇提出我们一起去干嘛干嘛的建议,那就用 Let’s 开头,我们给爸爸看看,就可以说Let’s show Daddy. 现在爸爸不在眼前,得先去爸爸那边,我们去给爸爸看看,就可以说Let’s go and show Daddy.
Daddy. Daddy.Guess what we've been doing.爸爸,爸爸,你猜猜我们刚才干了些什么。
这里是让爸爸猜刚才这一段时间里他们在做的事情,这种描述刚才的一段时间里一直在做的事的句子可以用现在完成进行时。就是 have/has+been 加动词的 ing 形式比如:你放学回家之后就一直在写作业,朋友给你打了好多电话你都没接,后来你看她一直打,你就接了,你一接她就问你怎么都不接电话,你就可以说I have been doing my homework. 这句话就表示的是,你刚一直在写作业,如果你说I‘m doing my homework. 所以猜猜我们刚才干什么了,先说猜 guess,然后 what,guess what we have beendoing.
Have you been watching television?你们刚才看电视了?
描述刚一段时间在做的事,那就用现在完成进行时,但这是个问句,现在完成进行时的问句用 have 开头Have you been..... Has he been...... Has she been.............. 这里就是 have you been 后面在加一个 watching television ,整句话就是Have you been watching television?
Have you just had a bath?你们刚才洗澡了?
洗澡可以说 have a bath/take a bath , 那么这句话就跟刚才一样在 Have you been 后面加个 having a bath?Have you been having a bath?你们刚在洗澡?这么说是可以的,但是这句话也可以用现在完成时Have you had a bath?这种说法的重点就在结果了,一个是洗澡的过程,一个是洗完了澡的结果,这里佩奇他们明显很脏,爸爸说这句话就是逗一下他们,把重点放在洗澡的结果,所以用现在完成时,就更合适一些,还可以在 had a bath 前面加个 just ,代表刚刚,你们这么脏一定是刚洗完澡吧,这种感觉。所以整句话就是Have you just had a bath?
I know. You've been jumping in muddy puddles.我知道了。你们刚才在泥坑里跳来跳去。
我知道了虽然也是一个结果,猜着猜着,哎知道了,但是它不是动作,它就是你的思想,跟写作业找东西那些都不一样,所以直接说 I know。不需要用现在完成时。
Ho. Ho. And look at the mess you're in.呵呵,看看你们弄得多脏呀。
看看多脏可以说 look at the mess,mess 是个名词,意思是脏,不整洁,比如你进你弟弟的房间,看到房间超级脏乱,你就可以跟他说,Look at the mess 你看看多脏。如果是你弟弟出去玩,把自己弄的特别脏,你就可以说:Look at the mess you are in. 字面意思就是你在这个脏里,那就是你脏嘛。
Let's clean up quickly before Mummy sees the mess.快清理干净,别让妈妈看到那么脏。(在妈妈看到前弄干净)
清理干净可以说 clean up,这里是提议我们一起来清理干净,我们一起来做这件事,那就是 Let’s clean up, 再加个快,quickly,Let's clean up quickly. 别让妈妈看到那么脏,就是在妈妈看到前弄干净,before mummy sees the mess. 整句话就是Let's clean up quickly before Mummy sees the mess.
Daddy, when we've cleaned up, will you and Mummy come and play, too?爸爸,我们清理干净以后,你和妈妈也会一起来玩吗?
我们清理干净之后又是做完了一个什么事情,就是爸爸在这里擦擦擦擦擦,哎,擦好了,所以也用现在完成时。我们清理干净之后,当我们清理干净了的那个时候,就可以用when,When we have cleaned up,可以缩略成 When we’ve cleaned up,这种 when加上一个现在完成时的句子在生活中很常见的。等我写完作业了,我可以看电视吗?When I‘ve finished my homework,can I watch TV?等我吃完早饭,我能跟朋友出去吗?When I‘ve finished my breakfast,can I go out with my friends?会干嘛干嘛吗?用 will 开头:你和妈妈会来玩吗?Will you and Mommy come and play?“也”会来玩吗?最后加个 too 就可以了。整句话就是Daddy, when we've cleaned up, will you and Mummy come and play, too?
Yes, we can all play in the garden.是的,我们都可以在花园玩。
我们可以在花园玩,We can play in the garden. 我们都可以在花园玩就可以说,Wecan all play in the garden.或者 we all can play in the garden.差别不大,都行。
Peppa and George are wearing their boots.佩奇和乔治穿着他们的靴子。
一般形容穿着什么东西都是用现在进行时,因为这东西就一直在你身上嘛,所以这句话就是Peppa and George are wearing their boots.
Everyone loves jumping up and down in muddy puddles.每个人都喜欢在泥坑里跳来跳去。
每个人是 everyone 它也是第三人称单数,因为它指的是一个一个的人,所以这里的 love后面也要加 s,就是Everyone loves jumping up and down in muddy puddles.
概述:表示正在进行的动作或存在的状态
构成:be(am/is /are)+现在分词
1、肯定式:be(am/is/are)+动词-ing
It is raining / It is snowing.
2、否定式:be+not+动词-ing形式
I am not watching TV.
3、一般疑问句:be 动词提前
肯定回答:Yes,主语+be
否定回答:No,主语+be not
Are you watching TV now?
Yes, I am. \ No, I am not.
动词-ing形式的构成规则
情况 | 规则 | 例词 |
---|---|---|
一般情况下 | 直接在动词末尾加-ing | go-going, talk-talking |
以不发音的字母e结尾的动词 | 去e加-ing | come-coming, make-making |
以单个福音字母结尾的重读闭音节动词 | 双写末尾的辅音字母后加-ing | sit-sitting, get-getting |
以ie结尾的动词 | 变ie为y后加-ing | die-dying, tie-tying |
have/has+been 加动词的 ing 形式
have/has + 动词的过去分词
等我写完作业了,我可以看电视吗?
When I‘ve finished my homework,can I watch TV?
]]>路由基础知识
Fundamentals: 基础知识
术语
First, you will see these terms being used throughout the documentation. Here’s a quick reference:
首先,你会在整个文档中看到这些术语。下面是一个快速参考:
throughout: 从头到尾
Tree: A convention for visualizing a hierarchical structure. For example, a component tree with parent and children components, a folder structure, etc.
树:可视化分层结构的公约。例如包含父组件和子组件的组件树、文件夹结构等。
Subtree: Part of a tree, starting at a new root (first) and ending at the leaves (last).
Root: The first node in a tree or subtree, such as a root layout.
Leaf: Nodes in a subtree that have no children, such as the last segment in a URL path.
convention: 公约
visualizing: 可视化的
hierarchical:分层的
composed of: 由…组成
app
RouterIn version 13, Next.js introduced a new App Router built on React Server Components, which supports shared layouts, nested routing, loading states, error handling, and more.
在第 13 版中,Next.js 引入了基于 React Server Components 的全新 App Router,它支持共享布局、嵌套路由、加载状态、错误处理等功能。
The App Router works in a new directory named app
. The app
directory works alongside the pages
directory to allow for incremental adoption.
App 路由器在一个名为 app 的新目录中工作。app 目录与 pages 目录一起工作,以支持渐进性采用。
alongside: 与此同时
This allows you to opt some routes of your application into the new behavior while keeping other routes in the pages
directory for previous behavior. If your application uses the pages
directory, please also see the Pages Router documentation.
这允许您将应用程序的某些路由选择性地采用新行为,同时保留 pages 目录中的其他路由以维持以前的行为。如果您的应用程序使用 pages 目录,请同时查看页面路由器文档。
Good to know: The App Router takes priority over the Pages Router. Routes across directories should not resolve to the same URL path and will cause a build-time error to prevent a conflict.
需要注意的是:App 路由器优先于页面路由器。跨目录的路由不应该解析到相同的 URL 路径,否则会在构建时出现错误以防止冲突。
By default, components inside app
are React Server Components. This is a performance optimization and allows you to easily adopt them, and you can also use Client Components.
默认情况下,app
内的组件是 React Server Components。这是一种性能优化,使您能够轻松采用它们,并且你也可以使用 Client Components。
adopt: 采用
Recommendation: Check out the Server page if you’re new to Server Components.
**建议:**如果您对服务器组件不熟悉,可以查看 Server 页面。
Next.js uses a file-system based router where:
page.js
file. See Defining Routes.Next.js 使用基于文件系统的路由器,其中:
Each folder in a route represents a route segment. Each route segment is mapped to a corresponding segment in a URL path.
路由中的每个文件夹代表一个路由段。每个路由段都映射到 URL 路径中的相应段。
represents:代表
corresponding: 相应的
To create a nested route, you can nest folders inside each other. For example, you can add a new /dashboard/settings
route by nesting two new folders in the app
directory.
要创建嵌套路由,你可以让文件夹互相嵌套,例如:你可以通过在app
目录下嵌套两个文件夹添加一个新的 /dashboard/settings
路由
The /dashboard/settings
route is composed of three segments:
/
(Root segment)dashboard
(Segment)settings
(Leaf segment)该 /dashboard/settings
路由由三个段组成:。。。
文件公约
Next.js provides a set of special files to create UI with specific behavior in nested routes:
Next.js 提供了一组特殊文件,用于在嵌套路由中创建具有特定行为的用户界面:
layout | Shared UI for a segment and its children |
page | Unique UI of a route and make routes publicly accessible |
loading | Loading UI for a segment and its children |
not-found | Not found UI for a segment and its children |
error | Error UI for a segment and its children |
global-error | Global Error UI |
route | Server-side API endpoint |
template | Specialized re-rendered Layout UI |
default | Fallback UI for Parallel Routes |
Good to know:
.js
,.jsx
, or.tsx
file extensions can be used for special files.
组件层次结构
The React components defined in special files of a route segment are rendered in a specific hierarchy:
在路由段的特殊文件中定义的 React 组件会以特定的层次结构呈现:
layout.js
template.js
error.js
(React error boundary)loading.js
(React suspense boundary)not-found.js
(React error boundary)page.js
or nested layout.js
In a nested route, the components of a segment will be nested inside the components of its parent segment.
在嵌套路由中,段的组件将嵌套在父段的组件内。
文件存放
In addition to special files, you have the option to colocate your own files (e.g. components, styles, tests, etc) inside folders in the app
directory.
除特殊文件外,您还可以选择将自己的文件(如组件、样式、测试等)放在app
目录下的文件夹中。
This is because while folders define routes, only the contents returned by page.js
or route.js
are publicly addressable.
这是因为虽然文件夹定义了路由,但只有 page.js 或 route.js 返回的内容才是可公开寻址的。
Learn more about Project Organization and Colocation.
高级路由模式
The App Router also provides a set of conventions to help you implement more advanced routing patterns. These include:
应用程序路由器还提供了一系列约定,帮助您实现更高级的路由模式。这些约定包括:
Parallel Routes: Allow you to simultaneously show two or more pages in the same view that can be navigated independently. You can use them for split views that have their own sub-navigation. E.g. Dashboards.
并行路由:允许您在同一视图中同时显示两个或多个可独立导航的页面。您可以将其用于有自己子导航的分割视图。例如仪表盘。
Intercepting Routes: Allow you to intercept a route and show it in the context of another route. You can use these when keeping the context for the current page is important. E.g. Seeing all tasks while editing one task or expanding a photo in a feed.
拦截路由:允许您截取一条路由,并在另一条路由的上下文中显示它。当保持当前页面的上下文非常重要时,就可以使用这些功能。例如,在编辑一项任务时查看所有任务,或在 feed 中展开一张照片。
simultaneously: 同时
independently: 独立的
These patterns allow you to build richer and more complex UIs, democratizing features that were historically complex for small teams and individual developers to implement.
通过这些模式,您可以构建更丰富、更复杂的用户界面,并将以往由小型团队和个人开发人员实施的复杂功能平民化。
]]>richer:更丰富
democratizing: 民主化
historically: 历史上
Next.js provides the building blocks to create flexible, full-stack web applications. The guides in Building Your Application explain how to use these features and how to customize your application’s behavior.
Next.js提供了构建灵活、全栈Web应用程序的基础模块。在“构建应用程序”部分的指南中,解释了如何使用这些功能以及如何自定义应用程序的行为。
flexible:灵活的
The sections and pages are organized sequentially, from basic to advanced, so you can follow them step-by-step when building your Next.js application. However, you can read them in any order or skip to the pages that apply to your use case.
这些章节和页面按照从基础到高级的组织顺序,因此您可以在构建Next.js应用程序时,按照步骤阅读它们。但是,您也可以以任何顺序阅读它们,或跳到适用于您使用情况的页面。
organized:有组织的
sequentially:依次地
apply to your use case:适用于您的用例
If you’re new to Next.js, we recommend starting with the Routing, Rendering, Data Fetching and Styling sections, as they introduce the fundamental Next.js and web concepts to help you get started. Then, you can dive deeper into the other sections such as Optimizing and Configuring. Finally, once you’re ready, checkout the Deploying and Upgrading sections.
如果你是Next.js的新手,我们建议从Routing、Rendering、Data Fetching和Styling部分开始学习,因为它们介绍了基本的Next.js和Web概念帮助你入门。然后,你可以深入研究其他部分,比如Optimizing和Configuring。最后,当你准备好时,请查看Deploying和Upgrading部分。
]]>fundamental:基本的
dive:潜水
Next.js项目结构
This page provides an overview of the file and folder structure of a Next.js project. It covers top-level files and folders, configuration files, and routing conventions within the app
and pages
directories.
本页面概述了Next.js项目的文件和文件夹结构,它涵盖了顶层文件和文件夹、配置文件以及在app
和pages
目录中的路由规则。
conventions: 公约
顶层文件夹
名称 | 描述 |
---|---|
app | App Router |
pages | Pages Router |
public | Static assets to be served(待服务的静态资产) |
src | Optional application source folder(可选的应用程序源文件夹) |
顶层文件
Next.js | |
---|---|
next.config.js | Configuration file for Next.js |
package.json | Project dependencies and scripts |
instrumentation.ts | OpenTelemetry and Instrumentation file(OpenTelemetry和仪表文件,todo不知道啥意思) |
middleware.ts | Next.js request middleware(Next.js请求中间件) |
.env | Environment variables |
.env.local | Local environment variables |
.env.production | Production environment variables |
.env.development | Development environment variables |
.eslintrc.json | Configuration file for ESLint |
.gitignore | Git files and folders to ignore |
next-env.d.ts | TypeScript declaration file for Next.js(Next.js的TypeScript声明文件) |
tsconfig.json | Configuration file for TypeScript |
jsconfig.json | Configuration file for JavaScript |
middleware: 中间件
declaration:声明
app
Routing Conventionsapp路由规则
路由文件
layout | .js .jsx .tsx | Layout |
page | .js .jsx .tsx | Page |
loading | .js .jsx .tsx | Loading UI |
not-found | .js .jsx .tsx | Not found UI |
error | .js .jsx .tsx | Error UI |
global-error | .js .jsx .tsx | Global error UI |
route | .js .ts | API endpoint |
template | .js .jsx .tsx | Re-rendered layout(重新渲染的布局) |
default | .js .jsx .tsx | Parallel route fallback page |
嵌套路由
folder | Route segment(路由段) |
folder/folder | Nested route segment(嵌套路由段) |
动态路由
[folder] | Dynamic route segment |
[...folder] | Catch-all route segment |
[[…folder]] | Optional catch-all route segment |
动态路由即是匹配路径作为参数,[folder]匹配单个路径:
Route | Example URL | params |
---|---|---|
app/blog/[slug]/page.js | /blog/a | { slug: 'a' } |
app/blog/[slug]/page.js | /blog/b | { slug: 'b' } |
app/blog/[slug]/page.js | /blog/c | { slug: 'c' } |
[…folder]匹配多个路径:
Route | Example URL | params |
---|---|---|
app/shop/[...slug]/page.js | /shop/a | { slug: ['a'] } |
app/shop/[...slug]/page.js | /shop/a/b | { slug: ['a', 'b'] } |
app/shop/[...slug]/page.js | /shop/a/b/c | { slug: ['a', 'b', 'c'] } |
[[…folder]]匹配零或多个路径:
Route | Example URL | params |
---|---|---|
app/shop/[[...slug]]/page.js | /shop | {} |
app/shop/[[...slug]]/page.js | /shop/a | { slug: ['a'] } |
app/shop/[[...slug]]/page.js | /shop/a/b | { slug: ['a', 'b'] } |
app/shop/[[...slug]]/page.js | /shop/a/b/c | { slug: ['a', 'b', 'c'] } |
路由组和私有文件夹
(folder) | Group routes without affecting routing(在不影响路由选择的情况下分组路由) |
_folder | Opt folder and all child segments out of routing(选择将文件夹和所有子文件排除在路由选择之外) |
路由组主要解决多个路由的不同布局问题。app路由只有个根布局,默认路由的布局都是使用它。这会带来一个问题:多个路由的布局不同时如何解决?路由组可以让不同组的布局分开,A组使用A组的布局,B组使用B组的布局。
私有文件主要解决让一些app目录下的文件不被路由,由于app路由存在文件夹就是路由路径的方式,反过来就是说所有文件夹都是路由路径,如果需要指定某些文件夹不加入路由,就可以在文件夹前加入_
前缀,表示这个文件夹是私有的,不参与路由。
并行路由和拦截路由
@folder | Named slot |
(.)folder | Intercept same level |
(..)folder | Intercept one level above |
(..)(..)folder | Intercept two levels above |
(...)folder | Intercept from root |
并行路由可以在同一个页面下展示多种布局,比如做聚合页时,如何系统本身就有多个页面,那么只要使用并行路由将它们发一起即可。
拦截路由可以使一个页面在另一个页面上层展示,而不必跳转。比如在图片列表打开图片详情,图片详情直接悬浮在图片列表上。当然,图片详情页面也可以通过路由的方式和原来一样在新的页面打开。相当于一个页面的不同用法。
元数据文件规则
App图标
favicon | .ico | Favicon file |
icon | .ico .jpg .jpeg .png .svg | App Icon file |
icon | .js .ts .tsx | Generated App Icon |
apple-icon | .jpg .jpeg , .png | Apple App Icon file |
apple-icon | .js .ts .tsx | Generated Apple App Icon |
Open Graph and Twitter 图片,用于社交媒体使用,分享网站时可以带上此类图片。
opengraph-image | .jpg .jpeg .png .gif | Open Graph image file |
opengraph-image | .js .ts .tsx | Generated Open Graph image |
twitter-image | .jpg .jpeg .png .gif | Twitter image file |
twitter-image | .js .ts .tsx | Generated Twitter image |
sitemap | .xml | Sitemap file |
sitemap | .js .ts | Generated Sitemap |
robots | .txt | Robots file |
robots | .js .ts | Generated Robots file |
pages
Routing ConventionsPages路由规则
_app | .js .jsx .tsx | Custom App |
_document | .js .jsx .tsx | Custom Document |
_error | .js .jsx .tsx | Custom Error Page |
404 | .js .jsx .tsx | 404 Error Page |
500 | .js .jsx .tsx | 500 Error Page |
Folder convention | ||
index | .js .jsx .tsx | Home page |
folder/index | .js .jsx .tsx | Nested page |
File convention | ||
index | .js .jsx .tsx | Home page |
file | .js .jsx .tsx | Nested page |
Folder convention | ||
[folder]/index | .js .jsx .tsx | Dynamic route segment |
[...folder]/index | .js .jsx .tsx | Catch-all route segment |
[[…folder]]/index | .js .jsx .tsx | Optional catch-all route segment |
File convention | ||
[file] | .js .jsx .tsx | Dynamic route segment |
[...file] | .js .jsx .tsx | Catch-all route segment |
[[…file]] | .js .jsx .tsx | Optional catch-all route segment |
安装
System Requirements:
系统要求:
自动安装
We recommend starting a new Next.js app using create-next-app
, which sets up everything automatically for you. To create a project, run:
我们建议使用create-next-app
启动一个新的 Next.js 应用程序,它会自动为你设置好一切。要创建项目,请运行:
npx create-next-app@latest
On installation, you’ll see the following prompts:
在安装中,你会看到以下提示:
What is your project named? my-appWould you like to use TypeScript? No / YesWould you like to use ESLint? No / YesWould you like to use Tailwind CSS? No / YesWould you like to use `src/` directory? No / YesWould you like to use App Router? (recommended) No / YesWould you like to customize the default import alias (@/*)? No / YesWhat import alias would you like configured? @/*
After the prompts, create-next-app
will create a folder with your project name and install the required dependencies.
在提示之后, create-next-app
会创建一个以你项目名称命名的文件夹并且安装需要的依赖。
Good to know:
TypeScript
, ESLint
, and Tailwind CSS
configuration by default.src
directory in the root of your project to separate your application’s code from configuration files.值得一提的是:
ships: 船只,默认携带或默认集成, 类比到产品交付的过程,表示这些功能在 Next.js 中已经预先包含,用户可以直接使用。
optionally: 可选择
separate: 分离
手动安装
To manually create a new Next.js app, install the required packages:
要手动创建一个新的Next.js应用程序,请安装所需的软件包:
npm install next@latest react@latest react-dom@latest
Open your package.json
file and add the following scripts:
打开你的package.json
文件并添加以下脚本:
{ "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" }}
These scripts refer to the different stages of developing an application:
这些脚本涉及开发应用程序的不同阶段:
创建目录
Next.js uses file-system routing, which means the routes in your application are determined by how you structure your files.
Next.js使用文件系统路由,这意味着应用程序中的路由是由您如何组织文件来确定的。
For new applications, we recommend using the App Router. This router allows you to use React’s latest features and is an evolution of the Pages Router based on community feedback.
对于新应用程序,我们建议使用App Router。这个路由器允许您使用React的最新功能,是基于社区反馈演变而来的Pages Router的升级版。
evolution: 进化, 演变
Create an app/ folder, then add a layout.tsx and page.tsx file. These will be rendered when the user visits the root of your application (/).
创建一个app/文件夹,然后添加layout.tsx
和page.tsx
文件。当用户访问你的应用程序的根目录(/)时,这些文件将被呈现出来。
Create a root layout inside app/layout.tsx
with the required <html>
and <body>
tags:
在app/layout.tsx
中创建一个根布局,其中包含所需的<html>
和<body>
标签:
export default function RootLayout({ children,}: { children: React.ReactNode}) { return ( <html lang="en"> <body>{children}</body> </html> )}
Finally, create a home page app/page.tsx
with some initial content:
最后,创建一个带有一些初始内容的主页app/page.tsx
:
export default function Page() { return <h1>Hello, Next.js!</h1>}
Good to know: If you forget to create layout.tsx, Next.js will automatically create this file when running the development server with next dev.
值得一提的是:如果你忘记了创建layout.tsx
文件,Next.js会在使用next dev
运行开发服务时自动的创建它。
Learn more about using the App Router.
了解有关使用应用程序路由器的更多信息。
If you prefer to use the Pages Router instead of the App Router, you can create a pages/
directory at the root of your project.
如果你喜欢使用 Pages Router 而非 App Router,你可以在项目的根目录下创建一个 pages/ 目录
Then, add an index.tsx
file inside your pages
folder. This will be your home page (/
):
然后,在你的 pages 文件夹中添加一个 index.tsx 文件。这将成为你的主页 (/)
pages/index.tsx
export default function Page() { return <h1>Hello, Next.js!</h1>}
Next, add an _app.tsx
file inside pages/
to define the global layout. Learn more about the custom App file.
接下来,接下来,在 pages/
目录下添加一个 _app.tsx
文件来定义全局布局。了解更多关于custom App file.
pages/_app.tsx
import type { AppProps } from 'next/app' export default function App({ Component, pageProps }: AppProps) { return <Component {...pageProps} />}
Finally, add a _document.tsx
file inside pages/
to control the initial response from the server. Learn more about the custom Document file.
最后,在 pages/
目录下添加 _document.tsx
文件,以控制服务器的初始响应。了解更多有关自定义文档文件的信息。
pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document' export default function Document() { return ( <Html> <Head /> <body> <Main /> <NextScript /> </body> </Html> )}
Learn more about using the Pages Router.
Good to know: Although you can use both routers in the same project, routes in app will be prioritized over pages. We recommend using only one router in your new project to avoid confusion.
需要注意:虽然你能在同一个项目中使用两种路由器,但是app路由器优先级会高于page路由器,我们建议在你的新项目中仅使用一个路由器,以避免混淆。
confusion:混乱
Create a public folder to store static assets such as images, fonts, etc. Files inside public directory can then be referenced by your code starting from the base URL (/).
创建一个公共文件夹来存储静态资源,如图片、字体等。在公共文件夹中的文件可以在代码中通过从基本URL (/) 开始进行引用。
assets: 资产
运行开发服务
Run npm run dev
to start the development server.
Visit http://localhost:3000 to view your application.
Edit app/layout.tsx
(or pages/index.tsx
) file and save it to see the updated result in your browser.
运行 npm run dev
来启动开发服务器。
访问 http://localhost:3000 来查看你的应用程序。
编辑 app/layout.tsx
(或 pages/index.tsx
)文件并保存, 在浏览器中查看更新的结果
Welcome to the Next.js documentation!
欢迎访问 Next.js 文档!
什么是Next.js
Next.js is a React framework for building full-stack web applications. You use React Components to build user interfaces, and Next.js for additional features and optimizations.
Next.js 是一个构建全栈的web应用框架. 你可以使用 React 组件来构建用户界面,并使用 Next.js 来提供额外的功能和优化。
interfaces: 界面
optimizations: 优化
Under the hood, Next.js / also / abstracts and automatically configures / tooling needed for React, like bundling, compiling, and more. This allows you to focus on building your application / instead of / spending time with configuration.
在底层,Next.js还抽象并自动配置了React所需的工具,如捆绑、编译等。这使您可以专注于构建应用程序,而不是花时间进行配置。
Under the hood: 在底层
Whether you’re an individual developer or part of a larger team, Next.js can help you build interactive, dynamic, and fast React applications.
无论你是个体开发者还是大团队中的一员, Next.js都能帮助你构建交互式、动态和快速的React应用.
interactive: 交互的
主要特性
Some of the main Next.js features include:
Next.js的一些主要特性包括:
Feature | Description |
---|---|
Routing | A file-system based router / built on top of Server Components / that supports layouts, nested routing, loading states, error handling, and more |
路由 | 一个基于服务器组件的文件系统路由器,支持布局、嵌套路由、加载状态、错误处理等功能。 |
Rendering | Client-side and Server-side Rendering with Client and Server Components. Further optimized with Static and Dynamic Rendering / on the server with Next.js. Streaming on Edge and Node.js runtimes. |
渲染 | 使用客户端和服务器组件进行客户端和服务器端渲染。利用 Next.js 进一步优化服务器上的静态和动态渲染。在 Edge 和 Node.js 运行时进行流式处理。 |
Data Fetching | Simplified data fetching with async/await in Server Components, and an extended fetch API for request memoization, data caching and revalidation. |
数据拉取 | 通过在Server Components中使用async/await简化了数据获取,并提供了一个扩展的fetch API,用于请求的记忆化、数据缓存和重新验证。 |
Styling | Support for your preferred styling methods, including CSS Modules, Tailwind CSS, and CSS-in-JS |
样式 | 支持您偏爱的样式方法,包括CSS模块,Tailwind CSS和CSS-in-JS。 |
Optimizations | Image, Fonts, and Script Optimizations to improve your application’s Core Web Vitals and User Experience. |
优化 | 图像、字体和脚本优化,以改善应用程序的核心网络性能指标和用户体验。 |
TypeScript | Improved support for TypeScript, with better type checking and more efficient compilation, as well as custom TypeScript Plugin and type checker. |
TypeScript | 改进了对 TypeScript 的支持,提供更好的类型检查和更高效的编译,同时还提供了自定义TypeScript插件和类型检查器。 |
A file-system based router: 一个基于文件系统的路由器, file-system based为形容词,基于文件系统的
request memoization:请求记忆化
revalidation: 重新验证
request memoization, data caching,为什么这里要用request memoization,而不是request caching,有什么区别吗?
"request memoization"和"request caching"之间有一些区别:
Request Caching(请求缓存)是指在发送请求后,将请求和响应的结果缓存起来,以便在后续的请求中直接使用缓存的结果,而无需再次发送请求到服务器。这样可以减少对服务器的请求,提高性能和效率。
Request Memoization(请求记忆化)是一种更加智能的缓存方式,它不仅仅缓存了请求的结果,还会缓存请求时的参数和上下文信息。当相同的请求再次被发送时,会首先检查缓存中是否存在相同参数的请求,并且仍处于有效期内。如果存在缓存,就直接返回缓存的结果,而无需再次发送请求。与简单的请求缓存相比,请求记忆化更加精细化,可以根据请求的参数进行缓存的判断。
因此,在这里使用"request memoization"表示使用了更加智能和精细化的缓存方式,不仅缓存了请求的结果,还会根据请求参数进行缓存的判断,提高了数据获取的效率和性能。
preferred:更喜欢的,优先的
efficient:高效的
as well as: 不仅…而且… He can play the guitar as well as the piano. (他既会弹吉他也会弹钢琴。)
Next.js has two different routers: the App Router and the Pages Router. The App Router is a newer router that allows you to use React’s latest features, such as Server Components and Streaming. The Pages Router is the original Next.js router, which allowed you to build server-rendered React applications and continues to be supported for older Next.js applications.
Next.js有两种不同的路由器:app路由器和pages路由器。app路由器是新的路由器,这允许你使用React的最新特性,比如Server Components和Streaming。
Pages路由器是原始的Next.js路由器,他允许你构建服务端渲染的React应用并且继续支持较旧的Next.js应用。
original: 最初的
必备知识
Although our docs are designed to be beginner-friendly, we need to establish a baseline so that the docs can stay focused on Next.js functionality. We’ll make sure to provide links to relevant documentation whenever we introduce a new concept.
虽然我们的文档旨在使初学者易于理解,但我们需要建立一个基准,以便文档可以专注于Next.js的功能。每当我们介绍新概念时,我们将确保提供相关文档的链接。
To get the most out of our docs, it’s recommended that you have a basic understanding of HTML, CSS, and React. If you need to brush up on your React skills, check out our Next.js Foundations Course, which will introduce you to the fundamentals.
为了充分利用我们的文档,建议您具备基本的 HTML、CSS 和 React 知识。如果您需要复习 React 技能,请查看我们的 Next.js Foundations Course,该课程将为您介绍基本原理。
]]>Although: 虽然、尽管
establish:建立
functionality:功能
relevant:有关
whenever:每当
concept:概念
get the most out of:充分利用
recommended:推荐
brush up:复习
fundamentals:基本原理
项目里存在一个这样的系统,它的主要功能类似于适配器,将一个系统的异构数据进行转化,处理成标准的数据流,交给另一个平台系统。
当然,也可以反过来理解,有一个平台级系统,需要从多种数据源(系统)中采集数据,每种数据源的数据结构都不相同,需要有个中间人进行转化。这个系统就承担了这样的角色。
这样的架构虽然降低了平台系统的复杂度,使每个适配器只专注于某一个数据源的对接。但由于平台系统从数据源获取数据是通过HTTP请求的方式完成的,一般来说可能涉及十几到几十个接口的对接。所以适配器的内部转化逻辑的代码编写也存在较大的工作量。
而每个适配器的转化逻辑大致是相同的,主要有几个方面:
当然还有很多等等等等,不再列举
为了解决以上的问题,我的想法是借鉴类Excel的方式,由于平台的数据结构是确定的,那么我只要编写一定的函数,将数据源的数据结构配置起来,系统通过解析配置的方式进行数据转化,比如
name=#usernamesex=if(eq(#gender,'男'),'M', 'F')user.username=#user.base.username
#代表取值 .表示层级 如{ user: { name: “张三”}}写作user.name
if(true of false, 真时的返回值,假时的返回值)
eq(value1, value2), 判断value1和value2是否相等,返回boolean值
if(eq(#sex,‘男’),‘M’, ‘F’): 当#gender为男时返回M,否则返回F
于是我遇到了我面临的第一个问题,也是本篇文章的主要内容:应该如何解析这串表达式if(eq(#gender,'男'),'M', 'F')
if(eq(#gender,'男'),'M', 'F')
这样的表达式有很多框架都能解析,如Spel、JEP。
我虽然不知道他们怎么实现的,但我也有自己的想法。
让我们逐一来分析这个表达式的特性
1、表达式由多个函数嵌套
组成
2、函数的格式为函数名(参数,参数,…)
3、不同的函数参数个数不同
4、不同的函数逻辑不同
得出:
1、多个函数&&逻辑不同:使用策略模式,每个函数一个类,使用策略的方式调度每一个函数
2、嵌套:凡嵌套,必递归
3、函数格式固定:通过模板方法统一解析
4、参数个数不同:使用集合保存所有参数
134点还比较简单,递归一般是难点,这里单独讲一下:
递归递归,有递有归,何时递何时归?
拿这个表达式分析if(eq(#gender,'男'),'M', 'F')
:
1、凡是遇到了函数(if, eq),就需要递
2、其他的如#
|'男'
就归
整个表达式的执行流程为:
1、先遇到if
,取出里面的3个参数
2、发现第一个参数是个函数eq
, 执行eq
逻辑
3、eq
中无嵌套函数,返回执行结果
4、继续判断if
的参数,第二三参数非函数,执行if
逻辑
整体UML图:
FuncHandler: 函数处理器,抽象类,公共的handler方法,实现解析参数,递归过程等通用流程;抽象的doHandler方法,交由子类实现函数的特定逻辑;funcName方法,获取函数名称,如
if
IfFuncHandler | EqFuncHandler: 子类,实现特定的函数逻辑
FuncEngine: 函数引擎,调度各类函数
这里要先解决个小问题,如何得到函数中的所有参数?
假设函数是eq(#gender, '男')
, 那只要
String func = "eq(#gender,'男')";// 得到#gender,'男'func = func.subString("eq".length() + 1, func.lastIndexOf(')'));// 分割String[] params = func.split(',');
但如果是if(eq(#gender,'男'),'M', 'F')
,就会发现最后分割出来的参数列表是错误的。
这里的关键点在于不能分割嵌套函数的逗号
,也就是eq(#gender, '男')
中的逗号
一道简单的算法题:如何判断字符串中的括号是否成对,比如((()))
和(()())
是成对的,(()))
不是成对的
String str = "((()))";int count = 0;for(char c, str.toCharArray()){ if(c == '('){ count ++; }else { count -- }}// count等于0即成对
所以我们判断函数中的某个逗号是否能分割,关键在于判断该逗号是否在嵌套函数内,也就是括号是否成对(count==0
), 如果count!=0
,说明此括号在函数内,不能分割。代码如下
protected List<String> extractParameters(String func, String funcName) { // 去除两边的括号,得到函数内的字符串 String content = func.substring(funcName.length() + 1, func.lastIndexOf(')')); // 保存参数的集合 List<String> parameters = new ArrayList<>(); // 参数起始下标,括号计数 int parameterStartIndex = 0, parenthesisCount = 0; for (int i = 0; i < content.length(); i++) { char ch = content.charAt(i); if (ch == '(') { parenthesisCount++; } else if (ch == ')') { parenthesisCount--; } else if (ch == ',' && parenthesisCount == 0) { // 当遇到逗号,且括号成对,说明逗号不在嵌套函数内,记录该参数 parameters.add(content.substring(parameterStartIndex, i)); // 修改起始值 parameterStartIndex = i + 1; } } // 保存最后一个的参数 parameters.add(content.substring(parameterStartIndex)); return parameters;}
这个小问题解决后,我们看一下FuncHandler
的整体代码
FuncHandler
代码实现如下:
public abstract class FuncHandler { @Autowired protected FuncEngine funcEngine; public Object handle(String func, JSONObject data) { // 获取函数中的参数 final List<String> parameters = extractParameters(getFuncLen(), func); List<Object> evaluateResult = new ArrayList<>(parameters.size()); for (String parameter : parameters) { // 继续递归执行每个参数(参数可能是个函数) 重要!! Object val = funcEngine.evaluateFunc(parameter, data); // 保存结果 evaluateResult.add(val); } // 调用子类函数 return doHandle(evaluateResult); } /** * 提取参数集合 * * @param openParenthesisIndex 左括号下标,函数名的长度就是左括号的下标位置 * @param func 当前函数 * @return 参数集合 */ protected List<String> extractParameters(int openParenthesisIndex, String func) { int closingParenthesisIndex = findClosingParenthesis(func, openParenthesisIndex); // 去除两边的括号,得到函数内的字符串 String content = func.substring(openParenthesisIndex + 1, closingParenthesisIndex); // 保存参数的集合 List<String> parameters = new ArrayList<>(); // 参数起始下标, 括号计数 int parameterStartIndex = 0, parenthesisCount = 0; for (int i = 0; i < content.length(); i++) { char ch = content.charAt(i); if (ch == '(') { parenthesisCount++; } else if (ch == ')') { parenthesisCount--; } else if (ch == ',' && parenthesisCount == 0) { // 当遇到逗号,切括号成对,说明逗号不在嵌套函数内,记录该参数 parameters.add(content.substring(parameterStartIndex, i)); // 修改起始值 parameterStartIndex = i + 1; } } // 保存最后一个的参数 parameters.add(content.substring(parameterStartIndex)); return parameters; } /** * 每一个函数处理结果方式不同交由子类实现 * * @param params 表达式解析结果集合 * @return 处理结果 */ protected abstract Object doHandle(List<Object> params); /** * 获取函数名称长度 * * @return 名称长度 */ protected int getFuncLen() { return funcName().length(); } /** * 获取函数名称 * @return 函数名称 */ public abstract String funcName();}
子类实现就比较简单了,只要实现doHandler
方法,专注于自己的逻辑即可
IfFuncHandler:
@Componentpublic class IfFuncHandler extends ObjectFuncHandler { @Override protected Object doHandle(List<Object> params) { Boolean booleanValue = (Boolean) params.get(0); if (Boolean.TRUE.equals(booleanValue)) { return params.get(1); } return params.get(2); } /** * IF(boolean,five,other) */ @Override public String funcName() { return "IF"; }}
EqFuncHandler
@Componentpublic class EqFuncHandler extends ObjectFuncHandler { @Override protected Object doHandle(List<Object> params) { return ObjectUtil.equal(params.get(0).toString(), params.get(1).toString()); } /** * EQ(#age,10) */ @Override public String funcName() { return "EQ"; }}
接下来是FuncEngine
的实现。
想要通过FuncEngine
调度所有函数,首先我们需要先将每个函数进行注册,这个可以通过Spring的依赖注入功能实现
@Resourceprivate List<FuncHandler> handlers;
像这样写,Spring即会将FuncHandler的所有子类注入到List中
其次我们还要注册一份函数表,用于通过函数名找到对应的函数bean
private static final Map<String, FuncHandler> HANDLER_MAP = new HashMap<>();
这个可以利用Spring的初始化功能,实现InitializingBean
接口或者使用@PostConstruct
注解
@Overridepublic void afterPropertiesSet() { for (FuncHandler handler : handlers) { HANDLER_MAP.put(handler.funcName().toLowerCase(Locale.ROOT), handler); }}
最后就是调度
public Object evaluateFunc(String func, JSONObject data) { final FuncHandler funcHandler = HANDLER_MAP.get(findMethod(func)); if (funcHandler != null) { return funcHandler.handle(func, data); } if(func.startsWith("#")){ // 截取掉# return data.get(func.substring(1)); } if(func.startsWith("'") && func.endsWith("'")){ return func.substring(1, func.length()-1); } return func;}public String findMethod(String func) { final int i = func.indexOf('('); if (i > 0) { return func.substring(0, i).toLowerCase(Locale.ROOT); } return null;}
完整代码
@Componentpublic class FuncEngine implements InitializingBean { @Resourceprivate List<FuncHandler> handlers; private static final Map<String, FuncHandler> HANDLER_MAP = new HashMap<>(); public Object evaluateFunc(String func, JSONObject data) { // 去除两边的空格 func = func.trim(); // 获取funcHandler final FuncHandler funcHandler = HANDLER_MAP.get(findMethod(func)); if (funcHandler != null) { return funcHandler.handle(func, data); } // 是否从json取值 if(func.startsWith("#")){ // 截取掉# return data.get(func.substring(1)); } // 去除单引号 if(func.startsWith("'") && func.endsWith("'")){ return func.substring(1, func.length()-1); } return func; } public String findMethod(String func) { final int i = func.indexOf('('); if (i > 0) { return func.substring(0, i).toLowerCase(Locale.ROOT); } return null; } @Override public void afterPropertiesSet() { for (FuncHandler handler : handlers) { HANDLER_MAP.put(handler.funcName().toLowerCase(Locale.ROOT), handler); } }}
@SpringBootTestpublic class SimpleTest { @Resource private FuncEngine funcEngine; @Test public void testExpress(){ String expression = "IF(GTE(#age,18), '成人', IF(LT(#age,12), '儿童','青少年'))"; { String jsonData = "{ \"age\": 9, \"name\": \"zhangsan\" }"; JSONObject data = JSON.parseObject(jsonData); Assertions.assertEquals("儿童", funcEngine.evaluateFunc(expression, data)); } { String jsonData = "{ \"age\": 18, \"name\": \"zhangsan\" }"; JSONObject data = JSON.parseObject(jsonData); Assertions.assertEquals("成人", funcEngine.evaluateFunc(expression, data)); } }}
GTE和LT两个函数是我新加的,加个函数老简单了。
GTE
@Override protected Object doHandle(List<Object> analyticRes) { return NumberUtil.isGreaterOrEqual(new BigDecimal(analyticRes.get(0).toString()), new BigDecimal(analyticRes.get(1).toString())); }
LT
@Override protected Object doHandle(List<Object> analyticRes) { return NumberUtil.isLess(new BigDecimal(analyticRes.get(0).toString()), new BigDecimal(analyticRes.get(1).toString())); }
通篇下来,会发现函数引擎代码量其实不大,主要考验程序设计能力,总体来说,我对这份代码还算满意,灵活性和扩展性还是足够大的。
由于整体被Spring接管,理论上可以通过它做任何事情,不管是执行sql,或者发http请求,都是可以的。使用者只需继承funcHandler
实现doHandler
方法。
其实我写这份代码从设计到完成,只花了大概1个小时的时间,但写文章却花了我两天。一个是我确实太久没写文章了,另外更重要的确实是文章难写啊。我一眼就看出来这个实现要用递归+策略+模板方法
, 但要讲清楚我这一眼
, 着实费劲。
代码已发布到凯桥组件库:https://github.com/lzj960515/kq-universal/tree/main/kq-universal-func-starter,欢迎阅读体验
点个赞再走呀!
]]>大家好,我是阿紫,最近做了个不输入任何关键词就出妹子的网站
如果大家不想听我下面的废话现在就可以去试试了,或者试玩后回来吐槽~
网址地址:https://airandomimage.art | https://airandomimage.top
1、学习AI绘画有很大一部分的精力就是学习如何写关键词,这让我很苦恼,因为我很多时候都不知道画什么,我唯一知道的就是我想要一直好看的妹子图片
2、玩抽卡游戏都知道, 比如原神,抽到一张五星卡的感觉真是太爽了!
于是我诞生了一个想法,为什么不搞一个抽卡程序呢,整理一大堆的关键词库,随机选取发给画图程序就好了呀
第一次做一个程序,想法不是很成熟,做了个把个月,磕磕绊绊的,大家可以试玩一下,欢迎大家在评论区留下自己的想法。轻喷,我会改的。
]]>前置条件:已安装elasticserch、kibana、fleet、docker
没有请查看:基于ElasticStack的监控告警统一解决方案 并安装8.9.2以上版本
docker: docker、compose最新版安装
在Fleet页面新增代理策略,复制token
使用docker安装
version: "3"services: elastic-agent: image: docker.elastic.co/beats/elastic-agent-complete:8.9.2 container_name: elastic-agent restart: always user: elastic-agent environment: - FLEET_ENROLLMENT_TOKEN=xxx - FLEET_ENROLL=1 - FLEET_URL=https://192.168.0.29:8220 - FLEET_INSECURE=true
Es升级是真快啊,几天一个版本更新,越来越好用了,8.9.2之后增加了Synthetics功能,可以直接在页面上建监测了,真香,升级起
本文只支持8.x升到8.x, 如果你是7.x,去看文档:Upgrade Elasticsearch
步骤:先升级node节点,再升级master节点
由于在关闭数据节点时,分配进程会等待一分钟时间,然后开始将该节点上的分片复制到群集中的其他节点,这可能会涉及大量 I/O。
但是我们的节点很快就会重新启动(升级很快),因此这种 I/O 是不必要的。因此可以在关闭数据节点前禁用复制的分配,从而避免I/O:
PUT _cluster/settings{ "persistent": { "cluster.routing.allocation.enable": "primaries" }}
POST /_flush
使用设置升级模式API暂时停止与你的机器学习作业和数据传输相关的任务,并防止打开新作业:
POST _ml/set_upgrade_mode?enabled=true
如果没开就不用设置
如果你是用systemd命令维护es就用这个指令,如果不是,请去看官方文档
sudo systemctl stop elasticsearch.service
用deb包或者rpm包装的可以直接安装
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.9.2-amd64.debwget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.9.2-amd64.deb.sha512shasum -a 512 -c elasticsearch-8.9.2-amd64.deb.sha512 sudo dpkg -i elasticsearch-8.9.2-amd64.deb
目前我安装的最新是8.9.2
重载一下配置文件
systemctl daemon-reload
systemctl start elasticsearch.service
查看节点情况
GET _cat/nodes
PUT _cluster/settings{ "persistent": { "cluster.routing.allocation.enable": null }}
GET _cat/health?v=true
等待状态列切换为绿色。
节点变为绿色后,所有主碎片和副本碎片都已分配。如果实在等不到就算了,我升级的时候等了半天,还是yellow,当然如果是red肯定不行
到这里node节点已经升级好了,重复以上步骤继续升级其他node,最后升级master即可
注意:如果你通过一些非正常手段开了白金版,升级后会告诉你许可证错误,参考一分钟开启elastic白金版再来一遍就好了
kibana升级比较简单,停止并重新安装即可,如果有多台kibana,要全部停掉
参考文档:Upgrade Kibana
systemctl stop kibanawget https://artifacts.elastic.co/downloads/kibana/kibana-8.9.2-amd64.debshasum -a 512 kibana-8.9.2-amd64.deb sudo dpkg -i kibana-8.9.2-amd64.debsystemctl start kibana
]]>
前提:假设你已经通过deb的方式部署了elasticsearch
curl -o LicenseVerifier.java -s https://raw.githubusercontent.com/elastic/elasticsearch/8.3/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseVerifier.javacurl -o XPackBuild.java -s https://raw.githubusercontent.com/elastic/elasticsearch/8.3/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackBuild.java
根据自己的版本进行修改,如 8.3 -> 8.4, 忽略小版本
修改LicenseVerifier.java
public static boolean verifyLicense(final License license, PublicKey publicKey) { return true;}
修改XPackBuild.java
Path path = getElasticsearchCodebase();shortHash = "Unknown";date = "Unknown";CURRENT = new XPackBuild(shortHash, date);
编译:
/usr/share/elasticsearch/jdk/bin/javac -cp "/usr/share/elasticsearch/lib/*:/usr/share/elasticsearch/modules/x-pack-core/*" LicenseVerifier.java/usr/share/elasticsearch/jdk/bin/javac -cp "/usr/share/elasticsearch/lib/*:/usr/share/elasticsearch/modules/x-pack-core/*" XPackBuild.java
替换:
cp /usr/share/elasticsearch/modules/x-pack-core/x-pack-core-8.3.3.jar x-pack-core-8.3.3.jarunzip x-pack-core-8.3.3.jar -d ./x-pack-core-8.3.3cp LicenseVerifier.class ./x-pack-core-8.3.3/org/elasticsearch/license/cp XPackBuild.class ./x-pack-core-8.3.3/org/elasticsearch/xpack/core//usr/share/elasticsearch/jdk/bin/jar -cvf x-pack-core-8.3.3.crack.jar -C x-pack-core-8.3.3 .
cp x-pack-core-8.3.3.crack.jar /usr/share/elasticsearch/modules/x-pack-core/x-pack-core-8.3.3.jar
申请证书: https://register.elastic.co/marvel_register
{"license": {"uid": "cd5c2258-7422-4f9a-a7c9-cb8d29a25361","type": "platinum","issue_date_in_millis": 1678665600000,"expiry_date_in_millis": 3207746200000,"max_nodes": 10000,"issued_to": "azi","issuer": "Web Form","signature": "AAAAAwAAAA1a8PJsIPdFZHe4WLkDAAABmC9ZN0hjZDBGYnVyRXpCOW5Bb3FjZDAxOWpSbTVoMVZwUzRxVk1PSmkxaktJRVl5MUYvUWh3bHZVUTllbXNPbzBUemtnbWpBbmlWRmRZb25KNFlBR2x0TXc2K2p1Y1VtMG1UQU9TRGZVSGRwaEJGUjE3bXd3LzRqZ05iLzRteWFNekdxRGpIYlFwYkJiNUs0U1hTVlJKNVlXekMrSlVUdFIvV0FNeWdOYnlESDc3MWhlY3hSQmdKSjJ2ZTcvYlBFOHhPQlV3ZHdDQ0tHcG5uOElCaDJ4K1hob29xSG85N0kvTWV3THhlQk9NL01VMFRjNDZpZEVXeUtUMXIyMlIveFpJUkk2WUdveEZaME9XWitGUi9WNTZVQW1FMG1DenhZU0ZmeXlZakVEMjZFT2NvOWxpZGlqVmlHNC8rWVVUYzMwRGVySHpIdURzKzFiRDl4TmM1TUp2VTBOUlJZUlAyV0ZVL2kvVk10L0NsbXNFYVZwT3NSU082dFNNa2prQ0ZsclZ4NTltbU1CVE5lR09Bck93V2J1Y3c9PQAAAQA+fZ30LicFouBamUw0wXUkbOsUP8p1bevJ+JsC4hWsed4ouqqJFipCa0bJJFISWzssU8BpxWQcnNE4WSSbZlSNuxzo2kGUuyE4wWyJyI7kfVpg8dm8POG0ugsIFLfgQISaFxI0MukpmGVyaukQONC9nqKSGgQ7xX2mOrnEC1tRwvBuiJT4aGulM2yMNxOB49DufwfR6w6KVZtpbbC/9BQtRVLl5Vyy/2I8F/il9q+U2J9EdGS4Gt6bW8N2GvZK4rqaPVSTxyh7YNar4IzErpfea9nYkdcgCJ9yOcZw4dCcwaTC90RTYqDIyIQ5h7ET+1Gpr9NemrrbYqfxUR2oIEmX","start_date_in_millis": 1678665600000}}
修改type为platinum, 修改expiry_date_in_millis过期时间
上传许可证
]]>wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo gpg --dearmor -o /usr/share/keyrings/elasticsearch-keyring.gpgsudo apt-get install apt-transport-httpsecho "deb [signed-by=/usr/share/keyrings/elasticsearch-keyring.gpg] https://artifacts.elastic.co/packages/8.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-8.x.list# 安装最新版本sudo apt-get update && sudo apt-get install elasticsearch# 安装指定版本wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.3.3-amd64.debwget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.3.3-amd64.deb.sha512shasum -a 512 -c elasticsearch-8.3.3-amd64.deb.sha512 sudo dpkg -i elasticsearch-8.3.3-amd64.deb
配置系统信息
#打开系统配置文件vim /etc/sysctl.conf#增加配置vm.max_map_count=262144#保存:wq#执行命令sysctl -w vm.max_map_count=262144vim /etc/security/limits.conf#增加以下配置,注意*号要留着* soft nofile 65536* hard nofile 131072* soft nproc 4096* hard nproc 4096* hard memlock unlimited* soft memlock unlimitedvim /etc/systemd/system.conf#修改以下配置DefaultLimitNOFILE=65536DefaultLimitNPROC=32000DefaultLimitMEMLOCK=infinity#关闭交换空间swapoff -a
证书位置:/etc/elasticsearch/certs/http_ca.crt
修改数据存储地址
mkdir -p /data/elasticsearch/datamkdir -p /data/elasticsearch/logschmod -R 777 /data/elasticsearch/data /data/elasticsearch/logs
修改配置
vim /etc/elasticsearch/elasticsearch.ymlcluster.name: es-clusternode.name: es01path.data: /data/elasticsearch/datapath.logs: /data/elasticsearch/logsnetwork.host: 0.0.0.0http.port: 9200discovery.seed_hosts: ["192.168.0.29", "192.168.0.30"]cluster.initial_master_nodes: ["es01", "es02"]bootstrap.system_call_filter: falsebootstrap.memory_lock: true
配置systemd
sudo systemctl daemon-reloadsudo systemctl enable elasticsearch.service
启动主节点
sudo systemctl start elasticsearch.service
换证书
将从节点的证书和key删除,将主节点的证书和key传给从节点scp -rp certs root@192.168.0.30:/etc/elasticsearch/scp elasticsearch.keystore root@192.168.0.30:/etc/elasticsearch/
启动从节点
sudo systemctl start elasticsearch.service
测试
curl --cacert /etc/elasticsearch/certs/http_ca.crt -u elastic 'https://localhost:9200/_cat/nodes?v=true&pretty'
Authentication and authorization are enabled.TLS for the transport and HTTP layers is enabled and configured.The generated password for the elastic built-in superuser is : K7-8f0CjVuYuPfJzffVqIf this node should join an existing cluster, you can reconfigure this with'/usr/share/elasticsearch/bin/elasticsearch-reconfigure-node --enrollment-token <token-here>'after creating an enrollment token on your existing cluster.You can complete the following actions at any time:Reset the password of the elastic built-in superuser with '/usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic'.Generate an enrollment token for Kibana instances with '/usr/share/elasticsearch/bin/elasticsearch-create-enrollment-token -s kibana'.Generate an enrollment token for Elasticsearch nodes with '/usr/share/elasticsearch/bin/elasticsearch-create-enrollment-token -s node'.
eyJ2ZXIiOiI4LjYuMiIsImFkciI6WyIxOTIuMTY4LjAuMjk6OTIwMCJdLCJmZ3IiOiJmYzQxYzI5ZjU1NmJiMDZmYWVjODMzMWM0ZjU1ZGY4M2Y2YzA3MTdmYjcyYjk4NDk3ZDU0N2UxZWVjOWM1ZWVlIiwia2V5IjoiNGZGTW9ZWUJwcjhMdzFKY1k1aVo6bkhwa0hxZzdROW1ibmtCM3dMMlVvdyJ9
wget https://artifacts.elastic.co/downloads/kibana/kibana-8.3.3-amd64.debshasum -a 512 kibana-8.3.3-amd64.deb sudo dpkg -i kibana-8.3.3-amd64.deb
生成token
/usr/share/elasticsearch/bin/elasticsearch-create-enrollment-token -s kibana
配置远程访问
server.host: "192.168.0.29"
启动
sudo /bin/systemctl daemon-reloadsudo /bin/systemctl enable kibana.servicesudo systemctl start kibana.service
打开页面,输入token,如:
eyJ2ZXIiOiI4LjMuMyIsImFkciI6WyIxOTIuMTY4LjAuMjk6OTIwMCJdLCJmZ3IiOiI0NmJlYjlkNDIzODZmNWY1NWU4NTY3NTZjMjk1ZDJkN2NkMzEwNzFlZGMwOTM5YWI3NmQ4M2U2MTJiNDBmOWUxIiwia2V5Ijoid05Cd3BvWUJqaHNTbnNsOHJtX0I6LXY5cjJKVG5TRU84SzRuamY5cnRSUSJ9
获取code
/usr/share/kibana/bin/kibana-verification-code
修改elastic密码
继续修改配置,增加elastic节点和中文、加密密钥
生成加密密钥
/usr/share/kibana/bin/kibana-encryption-keys generate
elasticsearch.hosts: ['https://192.168.0.29:9200','https://192.168.0.30:9200']i18n.locale: "zh-CN"xpack.encryptedSavedObjects.encryptionKey: 'fhjskloppd678ehkdfdlliverpoolfcr'xpack.actions.preconfiguredAlertHistoryEsIndex: true
重启
systemctl restart kibana.service
直接创建fleet服务器即可
复制elastic中的命令,在最后加上--insecure
curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent-8.3.3-linux-x86_64.tar.gztar xzvf elastic-agent-8.3.3-linux-x86_64.tar.gzcd elastic-agent-8.3.3-linux-x86_64sudo ./elastic-agent install --url=https://192.168.0.29:8220 --enrollment-token=xxxx --insecure
运行结束后可在界面上查看:
根据命令安装即可:
api_key可在Stack Management -> api_key中生成,指纹可在Fleet -> 设置 -> elasticsearh中查看
配置站点监测:
进入对应目录
cd /etc/heartbeat/monitors.d
cpoy一份配置文件,如
cp sample.http.yml youfile.yml
编辑配置文件
- type: http # 协议 id: your-server # 监控id name: 测试监控 # 监控名称 enabled: true schedule: '@every 5s' # 每5s发一次请求 hosts: ["https://baidu.com"] #监控地址,可以写多个 ipv4: true ipv6: true mode: any #timeout: 16s #超时时间,默认16s #username: '' #password: '' supported_protocols: ["TLSv1.0", "TLSv1.1", "TLSv1.2"] method: "GET" # 检查响应 check.response: #状态码,写200表示200为正常 status: 200 #json: #- description: Explanation of what the check does # condition: # equals: # myField: expectedValue tags: ["my-server","dev"] #tag
如果你还想检查响应结果,可以使用json配置
配置好后,可在监测中查看
使用告警索引模板
在kibana配置文件中添加:
xpack.actions.preconfiguredAlertHistoryEsIndex: true
重启kibana
此时添加告警规则时会出现一个es提供的索引连接器
在开发工具中添加别名
PUT kibana-alert-history-alert-000001{ "aliases": { "kibana-alert-history-alert": { "is_write_index": true } }}
在生命周期管理中添加该索引
当触发告警后将会往索引中写入数据。
如果监听索引的增量更新可以使用logstash, 见下文:logstash监听告警索引并推送至企业微信
免费版只支持日志和索引两种连接器,如何开启白金版学习版,请关注公众号:程序员阿紫,回复:Elastic白金版
给自己添加的监测开启告警
开启后编辑告警
如果弹窗消失了,也可以在这里找到他
编辑告警规则
在筛选中删除原来的条件,建议使用标签的方式进行筛选
状态检查部分可以自行调整
告警的内容可以随意发挥,这里是我用的
{ "ruleName":"{{rule.name}}","resource":"{{context.monitorUrl}}","reason":"{{context.reason}}","viewInAppUrl":"{{context.viewInAppUrl}}","state":"{{alert.actionGroupName}}","tags":"{{rule.tags}}", "phone":["你的手机号"]}!
加好之后,如果是同一个标签的服务,那就不用再加了。
在库存中添加磁盘告警规则
可添加筛选,以下规则表示该规则只适配chis开头的hostname
添加操作
选择磁盘告警
这里同样给出一份我用的文档
{"ruleName": "{{rule.name}}","resource": "{{alert.id}}","reason": "{{context.reason}}","viewInAppUrl": "{{context.viewInAppUrl}}","state": "{{alert.actionGroupName}}","tags": "{{rule.tags}}","phone": ["你的手机号"]}
如果你用的免费版,那么只能使用索引+logstash+自己写一个服务的方式实现告警
安装
wget https://artifacts.elastic.co/downloads/logstash/logstash-8.3.3-amd64.debsudo dpkg -i logstash-8.3.3-amd64.deb
编辑配置
cd /etc/logstash/conf.d/vim es-java.conf
input { elasticsearch { hosts => ["https://192.168.0.3:9200"] # Elasticsearch 服务器地址和端口 index => "kibana-alert-history-default" # 要监听的索引名称 query => '{"query":{"range":{"@timestamp":{"gte":"now-1m","lte":"now/m"}}}}' #查询前一分钟的增量数据 schedule => "* * * * *" #每分钟查询一次 scroll => "5m" api_key => "your key" ssl => true ca_trusted_fingerprint => "your ca" }}output { http { url => "http://192.168.0.13:8080/monitor/alert" # 替换为实际的 Webhooks URL http_method => "post" # 发送 POST 请求 format => "json" # 指定请求格式为 JSON message => '{"message": "%{message}", "timestamp": "%{[@timestamp]}" }' # 自定义请求消息体 headers => { "Content-Type" => "application/json" } # 指定请求头的 Content-Type }}
启动
systemctl start logstash
在集成中根据指引安装即可
接入APM第一点是查看该能力是否支持你的应用,比如你的应用的java版本是否为7以上,用的什么web框架?什么数据库?
详细请看官网:支持技术列表
注:该文档只介绍spring boot应用,其他语言可以查看官网文档
引入依赖主要用于收集日志,应用中的日志是非结构化的,该依赖使得应用日志能够结构化,方便filebeat收集
<dependency> <groupId>co.elastic.logging</groupId> <artifactId>logback-ecs-encoder</artifactId> <version>1.5.0</version></dependency>
在应用resource目录下编辑logback-spring.xml
文件
在原来的logback-spring.xml文件中增加以下配置
<include resource="co/elastic/logging/logback/boot/ecs-file-appender.xml" />
<springProfile name="online"> <root level="info"> <appender-ref ref="file"/> <appender-ref ref="ECS_JSON_FILE"/> </root></springProfile>
添加一个
<appender-ref ref="ECS_JSON_FILE"/>
如果你原来定义了FILE_LOG_PATTERN
,在里面增加%X
, 表示追踪id占位符
参考配置:
<?xml version="1.0" encoding="UTF-8"?><configuration scan="true" scanPeriod="60 seconds" debug="false"> <include resource="org/springframework/boot/logging/logback/defaults.xml"/> <springProperty name="applicationName" scope="context" source="spring.application.name" /> <property name="LOG_FILE" value="logs/${applicationName}/log.out"/> <include resource="co/elastic/logging/logback/boot/ecs-file-appender.xml" /> <!-- 日志格式 --> <property name="CONSOLE_LOG_PATTERN" value="%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%c){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/> <property name="FILE_LOG_PATTERN" value="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${applicationName} %X ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %c : %.-1024m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/> <!--输出到控制台--> <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${CONSOLE_LOG_PATTERN}</pattern> </encoder> </appender> <!--输出到文件--> <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_FILE}</file> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz</fileNamePattern> <!-- 日志保留天数 --> <maxHistory>7</maxHistory> <!-- 每个日志文件的最大值 --> <maxFileSize>10MB</maxFileSize> </rollingPolicy> <encoder> <pattern>${FILE_LOG_PATTERN}</pattern> </encoder> </appender> <!-- (多环境配置日志级别)根据不同的环境设置不同的日志输出级别 --> <springProfile name="local"> <root level="info"> <appender-ref ref="console"/> </root> </springProfile> <springProfile name="test"> <root level="info"> <appender-ref ref="console"/> </root> </springProfile> <springProfile name="dev"> <root level="info"> <appender-ref ref="file"/> <appender-ref ref="ECS_JSON_FILE"/> </root> </springProfile> <springProfile name="staging"> <root level="info"> <appender-ref ref="file"/> <appender-ref ref="ECS_JSON_FILE"/> </root> </springProfile> <springProfile name="online"> <root level="info"> <appender-ref ref="file"/> <appender-ref ref="ECS_JSON_FILE"/> </root> </springProfile></configuration>
以上
local、staging、online
为应用环境名,根据实际情况更改即可
mkdir -p /data/apm/agent && cd /data/apm/agentwget https://search.maven.org/remotecontent?filepath=co/elastic/apm/elastic-apm-agent/1.36.0/elastic-apm-agent-1.36.0.jar -O elastic-apm-agent.jar
增加jvm参数
-javaagent:/path/to/elastic-apm-agent-1.36.0.jar -Delastic.apm.service_name=user-server -Delastic.apm.server_urls=http://12:8200 -Delastic.apm.secret_token=xxx -Delastic.apm.environment=production -Delastic.apm.application_packages=com.my.user
/path/to/elastic-apm-agent-1.36.0.jar:你的agent路径
elastic.apm.server_urls:apm服务地址
elastic.apm.service_name:你的服务名称
elastic.apm.environment:你的环境
elastic.apm.application_packages:你的包路径
elastic.apm.secret_token: 密钥
修改Dockerfile文件, 增加JAVA_AGENT参数
案例:
FROM openjdk:8-jdk-oracleRUN mkdir /appENV SERVER_PORT=1113 \ JAVA_AGENT=-javaagent:/app/agent/elastic-apm-agent.jarCOPY target/youapp.jar /app/app.jarjava -Djava.security.egd=file:/dev/./urandom ${JAVA_AGENT} ${JVM_XMS} ${JVM_XMX} ${JVM_XMN} ${JVM_OPTS} ${JVM_GC} -jar /app/app.jar
修改docker-compose文件,增加apm参数
案例:
version: '3.5'services: user-server: restart: always image: user-server container_name: user-server environment: ELASTIC_APM_SERVICE_NAME: user-server ELASTIC_APM_APPLICATION_PACKAGES: com.my.user ELASTIC_APM_SERVER_URL: http://127.0.0.1:8200 ELASTIC_APM_SECRET_TOKEN: xxx ELASTIC_APM_ENVIRONMENT: production ports: - 8080:8080 volumes: - /var/log/server:/logs - /data/apm/agent:/app/agent networks: - commonnetworks: common: external: true
增加参数:ELASTIC_APM_SERVICE_NAME、ELASTIC_APM_APPLICATION_PACKAGES、ELASTIC_APM_SERVER_URL、ELASTIC_APM_SECRET_TOKEN、ELASTIC_APM_ENVIRONMENT
进入数据盘
cd /data
下载安装
curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-8.3.3-linux-x86_64.tar.gztar xzvf filebeat-8.3.3-linux-x86_64.tar.gzmv filebeat-8.3.3-linux-x86_64 filebeatcd filebeat
编辑配置文件
mv filebeat.yml filebeat.yml.bakvim filebeat.yml
filebeat.inputs:- type: filestream id: beat-log enabled: true # 你的日志文件 paths: - /var/log/server/**/*.json parsers: - ndjson: overwrite_keys: true add_error_key: true expand_keys: truefilebeat.config.modules: path: ${path.config}/modules.d/*.yml reload.enabled: falsesetup.template.settings: index.number_of_shards: 1setup.kibana: host: "http://127.0.0.1:5601"output.elasticsearch: hosts: ["127.0.0.1:9200"] protocol: "https" api_key: "your api key" ssl: enabled: true ca_trusted_fingerprint: "your ca" processors: - add_host_metadata: when.not.contains.tags: forwarded - add_cloud_metadata: ~ - add_docker_metadata: ~ - add_kubernetes_metadata: ~
初始化,要等几分钟
./filebeat setup -e
启动
sudo chown root filebeat.ymlnohup sudo ./filebeat -e &
sudo apt-get remove docker docker-engine docker.io containerd runcsudo apt-get updatesudo apt-get install -y \ ca-certificates \ curl \ gnupg \ lsb-releasesudo mkdir -m 0755 -p /etc/apt/keyringscurl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpgecho \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/nullsudo apt-get updatesudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -ysudo mkdir -p /etc/dockersudo tee /etc/docker/daemon.json <<-'EOF'{ "data-root": "/data/docker", "registry-mirrors": [ "https://3laho3y3.mirror.aliyuncs.com", "https://hub-mirror.c.163.com", "http://f1361db2.m.daocloud.io", "http://hub-mirror.c.163.com" ], "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "1" }}EOF#重启sudo systemctl daemon-reloadsystemctl restart docker#测试docker run hello-world
]]>
1、什么是Bean的生命周期?
2、Bean的生命周期是怎样的?
我们知道,在Java中,万物皆对象,这些对象有生命周期:实例化 -> gc回收
而Bean同样也是Java中的对象,只是在这同时,Spring又赋予了它更多的意义。
于是乎,我们将Bean从在Spring中创建开始,到Bean被销毁结束,这一过程称之为Bean的生命周期
那到底Bean在Spring中的创建过程是怎样的呢?
在Spring中,Bean的创建过程看起来复杂,但实际上逻辑分明。
如果我们将所有扩展性流程抛开,你会发现只剩下两个流程:对象的实例化和属性填充
我们在《从依赖倒置原则看Spring》文中手写的Spring,也只是完成了这两个流程,这足以说明只需要这两个流程就能完成一个简单的Spring框架,那其他的流程又是什么呢?他们又有什么作用?
那么我们现在就基于这两个核心流程出发,尝试完善整个Spring的Bean生命周期。
开始时,我们只有两个流程:对象的实例化和属性填充
我们知道,对象的实例化就是在Java里使用类构造器进行创建对象。而一个类中可能有很多的构造器,那么我们怎么才能知道使用哪个构造器进行实例化对象呢?
所以说,在实例化之前,还得先做一件事情:确定候选的构造器,也称之为构造器推断
功能描述:找寻beanClass中所有符合候选条件的构造器。
负责角色:AutowiredAnnotationBeanPostProcessor
候选条件:构造器上添加了@Autowired注解
推断流程:
1、获取beanClass中的所有构造器进行遍历,判断构造器上是否标识@Autowired注解,是则将构造器添加到候选构造器集合中
2、并进一步判断Autowired注解中required属性是否为true(默认为true),是则表示该beanClass已存在指定实例化的构造器,不可再有其他加了@Autowired注解的构造器,如果有则抛出异常。
3、如果Autowired注解中required属性为false,则可继续添加其他@Autowired(required=false)标识的构造器
4、如果候选构造器集合不为空(有Autowired标识的构造器),并且beanClass中还有个空构造器,那么同样将空构造器也加入候选构造器集合中。
5、如果候选构造器集合为空,但是beanClass中只有一个构造器且该构造器有参,那么将该构造器加入候选构造器集合中。
流程图:
当构造器遍历完毕之后,还有些许逻辑
以上判断条件很多,但始终是围绕这一个逻辑:这个beanClass中有没有被Autowired
标识的构造器,有的话required是true还是false,如果是true, 那其他的构造器都不要了。如果是false,那想加多少个构造器就加多少个。
咦,那要是没有Autowired
标识的构造器呢?
框架嘛,都是要兜底的,这里就是看beanClass中是不是只有一个构造器且是有参的。
那我要是只有个无参的构造器呢?
那确实就是没有候选的构造器了,但是Spring最后又兜底了一次,在没有候选构造器时默认使用无参构造器
那我要是有很多个构造器呢?
Spring表示那我也不知道用哪个呀,同样进入兜底策略:使用无参构造器(没有将抛出异常)
那么这就是构造器推断流程了,我们将它加入到流程图中
在得到候选的构造器之后,就可以对对象进行实例化了,那么实例化的过程是怎样的呢?
功能描述:根据候选构造器集合中的构造器优先级对beanClass进行实例化。
负责角色:ConstructorResolver
对象实例化的过程主要有两个方面需要关注:
1、构造器的优先级是怎样的?
2、如果有多个构造器,但是有部分构造器的需要的bean并不存在于Spring容器中会发生什么?也就是出现了异常怎么处理?
在Java中,多个构造器称之为构造器重载,重载的方式有两种:参数的数量不同,参数的类型不同。
在Spring中,优先级则是由构造器的修饰符(public or private)和参数的数量决定。
规则如下:
1、public修饰的构造器 > private修饰的构造器
2、修饰符相同的情况下参数数量更多的优先
这段流程很简单,代码只有两行:
// 如果一个是public,一个不是,那么public优先int result = Boolean.compare(Modifier.isPublic(e2.getModifiers()), Modifier.isPublic(e1.getModifiers()));// 都是public,参数多的优先return result != 0 ? result : Integer.compare(e2.getParameterCount(), e1.getParameterCount());
文中描述的规则是public > private, 只是为了更好的理解,实际上比较的是public和非public
当一个beanClass中出现多个构造器,但是有部分构造器的需要的bean并不存在于Spring容器中,此时会发生什么呢?
比如以下案例中,InstanceA具有三个构造方法,其中InstanceB并未注入到Spring中
@Componentpublic class InstanceA {@Autowired(required = false)public InstanceA(InstanceB instanceB){System.out.println("instance B ...");}@Autowired(required = false)public InstanceA(InstanceC instanceC){System.out.println("instance C ...");}@Autowired(required = false)public InstanceA(InstanceB instanceB, InstanceC instanceC, InstanceD InstanceD){System.out.println("instance B C D...");}}
那么启动时是报错呢?还是选择只有InstanceC的构造器进行实例化?
运行结果会告诉你:Spring最终使用了只有InstanceC的构造器
这一部分的具体过程如下:
1、将根据优先级规则排序好的构造器进行遍历
2、逐个进行尝试查找构造器中的需要的bean是否都在Spring容器中,如果成功找到将该构造器标记为有效构造器,并立即退出遍历
3、否则记录异常继续尝试使用下一个构造器
4、当所有构造器都遍历完毕仍未找到有效的构造器,抛出记录的异常
5、使用有效构造器进行实例化
到这里,beanClass实例化了一个bean,接下来需要做的便是对bean进行赋值,但我们知道,Spring中可以进行赋值的对象不仅有通过@Autowired
标识的属性,还可以是@Value
,@Resource
,@Inject
等等。
为此,Spring为了达到可扩展性,将获取被注解标识的属性的过程与实际赋值的过程进行了分离。
该过程在Spring中被称为处理beanDefinition
功能描述:处理BeanDefintion的元数据信息
负责角色:
1、AutowiredAnnotationBeanPostProcessor: 处理@Autowird
,@Value
,@Inject
注解
2、CommonAnnotationBeanPostProcessor:处理@PostConstruct
,@PreDestroy
,@Resource
注解
这两个后置处理器的处理过程十分类似, 我们以AutowiredAnnotationBeanPostProcessor
为例:
1、遍历beanClass中的所有Field
、Method
(java中统称为Member
)
2、判断Member
是否标识@Autowird
,@Value
,@Inject
注解
3、是则将该Member
保存,封装到一个叫做InjectionMetadata
的类中
4、判断Member
是否已经被解析过,比如一个Member
同时标识了@Autowired
和@Resource
注解,那么这个Member
就会被这两个后置处理器都处理一遍,就会造成重复保存
5、如果没被解析过就将该Member
放置到已检查的元素集合中,用于后续填充属性时从这里直接拿到所有要注入的Member
其中,AutowiredAnnotationBeanPostProcessor
和InjectionMetadata
的结构如下
同样,我们将这一部分流程也加入到流程图中
现在,beanClass中的可注入属性都找出来了,接下来就真的要进行属性填充了
功能:对bean中需要自动装配的属性进行填充
角色:
1、AutowiredAnnotationBeanPostProcessor
2、CommonAnnotationBeanPostProcessor
在上一个流程中,我们已经找到了所有需要自动装配的Member
,所以这一部流程就显得非常简单了
我们同样以AutowiredAnnotationBeanPostProcessor
为例
1、使用beanName为key,从缓存中取出InjectionMetadata
2、遍历InjectionMetadata
中的checkedElements
集合
3、取出Element
中的Member
,根据Member
的类型在Spring中获取Bean
4、使用反射将获取到的Bean设值到属性中
在Spring中,Bean填充属性之后还可以做一些初始化的逻辑,比如Spring的线程池ThreadPoolTaskExecutor
在填充属性之后的创建线程池逻辑,RedisTemplate
的设置默认值。
Spring的初始化逻辑共分为4个部分:
1、invokeAwareMethods:调用实现特定Aware
接口的方法
2、applyBeanPostProcessorsBeforeInitialization:初始化前的处理
3、invokeInitMethods:调用初始化方法
4、applyBeanPostProcessorsAfterInitialization:初始化后的处理
这块逻辑非常简单,我直接把源码粘出来给大家看看就明白了
private void invokeAwareMethods(String beanName, Object bean) {if (bean instanceof Aware) {if (bean instanceof BeanNameAware) {((BeanNameAware) bean).setBeanName(beanName);}if (bean instanceof BeanClassLoaderAware) {ClassLoader bcl = getBeanClassLoader();if (bcl != null) {((BeanClassLoaderAware) bean).setBeanClassLoader(bcl);}}if (bean instanceof BeanFactoryAware) {((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this);}}}
功能:调用初始化方法前的一些操作
角色:
1、InitDestroyAnnotationBeanPostProcessor:处理@PostContrust注解
2、ApplicationContextAwareProcessor:处理一系列Aware接口的回调方法
这一步骤的功能没有太大的关联性,完全按照使用者自己的意愿决定想在初始化方法前做些什么,我们一个一个来过
这里的逻辑与属性填充过程非常相似,属性填充过程是取出自动装配
相关的InjectionMetadata
进行处理,而这一步则是取@PostContrust
相关的Metadata
进行处理,这个Metadata
同样也是在处理BeanDefinition过程解析缓存的
1、取出处理BeanDefinition过程解析的LifecycleMetadata
2、遍历LifecycleMetadata
中的checkedInitMethods
集合
3、使用反射进行调用
这一步与invokeAwareMethods大同小异,只不过是其他的一些Aware接口,同样直接粘上代码
private void invokeAwareInterfaces(Object bean) {if (bean instanceof EnvironmentAware) {((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment());}if (bean instanceof EmbeddedValueResolverAware) {((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver(this.embeddedValueResolver);}if (bean instanceof ResourceLoaderAware) {((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext);}if (bean instanceof ApplicationEventPublisherAware) {((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext);}if (bean instanceof MessageSourceAware) {((MessageSourceAware) bean).setMessageSource(this.applicationContext);}if (bean instanceof ApplicationContextAware) {((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);}}
在Spring中的初始化方法有两种
1、实现InitializingBean
接口的afterPropertiesSet
方法
2、@Bean
注解中的initMethod
属性
调用顺序是先调用afterPropertiesSet
再initMethod
1、判断Bean是否实现InitializingBean
接口
2、是则将Bean强转成InitializingBean
,调用afterPropertiesSet
方法
3、判断BeanDefinition中是否有initMethod
4、是则找到对应的initMethod
,通过反射进行调用
在Spring的内置的后置处理器中,该步骤只有ApplicationListenerDetector
有相应处理逻辑:将实现了ApplicationListener接口的bean添加到事件监听器列表中
如果使用了Aop相关功能,则会使用到
AbstractAutoProxyCreator
,进行创建代理对象。
ApplicationListenerDetector
的流程如下
1、判断Bean是否是个ApplicationListener
2、是则将bean存放到applicationContext
的监听器列表中
到这里,Bean的生命周期主要部分已经介绍完了,我们将流程图补充一下
同样还有其他的一些逻辑
该过程处于Bean生命周期的最开始部分。
功能:由后置处理器返回Bean,达到中止创建Bean的效果
角色:无,Spring的内置后置处理器中,无实现。
Bean的生命周期十分复杂,Spring允许你直接拦截,即在创建Bean之前由自定义的后置处理器直接返回一个Bean给Spring,那么Spring就会使用你给的Bean,不会再走Bean生命周期流程。
案例演示:
@Componentpublic class Car {@Autowiredprivate Person person;public void checkPerson(){if(person == null){System.out.println("person is null");}}}
由于在Person
属性上加了@Autowired
,所以正常来说person必然不能为空,因为这是必须要注入的。
现在我们自定义一个BeanPostProcessor进行拦截
@Componentpublic class InterruptBeanPostProcessor implements InstantiationAwareBeanPostProcessor {@Overridepublic Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {if("car".equals(beanName)){try {return beanClass.newInstance();} catch (InstantiationException | IllegalAccessException e) {e.printStackTrace();}}return null;}}
测试结果如下
该步骤跟随在Spring实例化bean之后,将bean进行缓存,其目的是为了解决循环依赖问题。
该过程暂时按下不表,单独提出放于循环依赖章节。
与中止创建Bean逻辑相同,Spring同样也允许你在属性填充前进行拦截。在Spring的内置处理器中同样无该实现。
实现手段为实现InstantiationAwareBeanPostProcessor
接口,在postProcessAfterInstantiation
方法中返回false
@Componentpublic class InterruptBeanPostProcessor implements InstantiationAwareBeanPostProcessor {@Overridepublic boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {if(beanName.equals("car")){return false;}return true;}}
Spring中不仅有@PostContrust
、afterProperties
、initMethod
这些bean创建时的初始化方法,同样也有bean销毁时的@PreDestory
、destroy
,destroyMethod
。
所以在Bean的生命周期最后一步,Spring会将具备这些销毁方法的Bean注册到销毁集合中,用于系统关闭时进行回调。
比如线程池的关闭,连接池的关闭,注册中心的取消注册,都是通过它来实现的。
最后,附上一张Bean生命周期的完整流程图
]]>那我们就通过这个哲学问题谈一谈:一个对象在JVM中经历了什么?
我:对象从哪里来?
同事甲:呃,国家发的?
同事乙:充话费送的?
咳咳,我说的是JVM的对象
对于我们程序员来说,没有对象?不存在的!我直接创建一个!
所以这个问题很简单:对象都是创建出来的。
但是这个问题也很难:对象是怎么创建出来的?
这就像咱都知道咱都是咱妈生的,但咱是怎么……?
那我们就先来探讨一下咱是怎么……? 对象是怎么创建出来的?
想要创建对象,首先得找到它的类元信息,所以创建对象的第一步,就是类加载检查。
虚拟机遇到一条加载类指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
有没有一种字都认识,连在一起就不晓得啥意思的感觉?
没关系,我会出手
常见的加载类指令有:
加载类指令的参数就是指new User()
的User
简单来说,符号引用就是字面量,比如User
就是一个符号引用。
详细来说,符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
现在再来看类加载的解释,就是当虚拟机遇到new User()
时,首先会检查能否在常量池中定位到User
, 并且检查User
这个类是否被类加载过。
如果没有,那必须先执行相应的类加载过程,那么类加载过程是怎么样的?
我们先假设类已经加载过了
类加载检查通过之后,下一步就得给对象买房分配内存了。
如果没有通过自然会发生
ClassNotFound
异常
分配内存第一个问题:我怎么知道对象要的房子内存有多大?三室一厅?
要回答这个问题,就必须先要知道对象的内存结构是怎样的?
对象的内存结构由三部分组成
对象头分为两部分:
Mark Word标记字段:标记对象的hashcode,分代年龄等,见下图。
该部分在32位机器上占4字节,64位占8字节
Klass Pointer类型指针:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
该部分开启指针压缩占4字节,不开启占8字节
实例数据就是对象中的一些变量,基础类型该多大就多大,引用类型占4个字节(关闭指针压缩占8字节)
如int类型占4个字节,String, User,数组等就是引用类型。
当一个对象的对象头+实例数据所占的内存非8字节的倍数时,就会使用对齐填充的方式补上一些字节,让该对象所需内存达到8字节的倍数。
如该对象:
public static class B { //8B mark word 64位机器战8字节 //4B Klass Pointer 如果关闭压缩-XX:-UseCompressedClassPointers或-XX:-UseCompressedOops,则占用8B int id; //4B String name; //4B 如果关闭压缩-XX:-UseCompressedOops,则占用8B // 8+4+4+4=20, 所以还需对齐填充4字节。}
综上,其实对象所需的内存在类加载之后就已经确定了。
既然已经知道对象所需的内存了,那么又要怎么给对象划分内存呢?
划分内存有两种方式:指针碰撞和空闲列表
如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
由对象结构可知,对象的大小都是8字节的倍数,假设JVM的内存都是一格一格的,每格8字节,现在要为一个16字节的对象分配内存,则指针碰撞的方式可以用下图表示
如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
仍然是分配一个16字节的对象,用下图表示
划分内存的方式有了,但是还有一个问题,业务系统都是多线程运行的,也就是对象内存的分配存在并发问题。
用指针碰撞的方式举例,A对象和B对象同时需要分配内存,很可能指针在分配A对象内存后已经到了新的位置,但是分配B对象时的指针还在原来的位置上,此时再移动指针,就出现了并发问题。
可以类比成多线程执行count++
,count
会被重复累加的问题。
解决方式有两种:
CAS(compare and swap):虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。
即:分配对象的内存时,先进行比较指针是否发生变化,未发生变化则进行更改指针的位置,比较和更改这两步操作是原子性的。如果发生变化,则使用新的指针位置,并重试该步骤。
TLAB(thread local allocation buffer):这是一种非常值得学习的思想,他的思想是:既然不同的线程分配对象内存是存在冲突,那我是否可以在创建线程时就事先划分好一大块区域,每个线程分配对象内存时只在自己区域里做操作,这样就避免了并发问题。
这同样可以借鉴到业务开发中,在Java中,很多Util不是线程安全的, 比如SimpleDateFormat
,一个笨方法是每次使用时都新new
一个,如果借鉴了这个思想,那我们可以做一个Map
出来,key为线程id,value为SimpleDateFormat
对象实例,这样每个线程都有自己专属的SimpleDateFormat
对象,就避免了并发问题。
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。
这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
比如你只写了int a
但没赋值,JVM就给他赋个0,当然,你赋值了JVM也会先给他赋个0, 赋你写的值是在后面执行<init>方法。
关于对象头的信息在对象的结构部分已经详细说明
该步骤就是给对象头设置一些必要的信息:对象的哈希码、对象的GC分代年龄等,还有类型指针,这样在使用时才能知道这个对象是哪个类的实例。
给对象的属性赋值,即程序员赋的值。
以及执行构造方法。
到此,一个对象就算是创建出来了。
对象从哪里来我们是知道了,那么对象到哪里去呢?
变为一抔黄土?
没错,对象最终也会嗝屁,对象是怎么嗝屁的?
关于对象是怎么嗝屁的,那先得知道对象在哪里?
没错,通过对象创建的过程我们知道了对象需要分配在内存里。
那到底是分配到内存哪里呢?
JVM的组成主要有三部分:类加载子系统、字节码执行引擎、运行时数据区。
对象就在运行时数据区中。
而运行时数据区又分为五个部分,堆、方法区、虚拟机栈、本地方法栈、程序计数器
存放了几乎所有的对象实例。
存储每个类的结构,例如运行时常量池,字段和方法数据,以及方法和构造函数的代码,包括用于类和实例初始化和接口初始化的特殊方法。
随着线程创建而同步创建的一块内存区域,在每个方法被执行时,都会创建一个栈帧。
栈帧包含:
每一个方法调用完毕,就对应着一个栈帧在虚拟机栈从入栈到出栈的过程
存储当前正在执行的字节码指令。
首先,讨论这个问题前,我们必须有一个共识:对象和对象是不一样的。
嗯,我说的是Java里的对象。
有一些对象存活时间长,甚至是应用运行了多长时间,它就存活了多长时间。比如类Class对象,Spring的Bean对象。
有一些对象存活时间短,刚创建就被销毁,比如业务对象。
针对不同的对象,当然要有不同的分配方式。
是的,对象除了会在堆里,同样也会栈上分配。
先看以下代码:
private void alloc() { User user = new User(); user.name = "a"; user.age = 10;}class User{ String name; int age;}
请问,user
对象什么时候会变成垃圾对象?
显然, 当alloc
方法结束后,user
对象就已经变成垃圾对象了。
所以user
随着alloc
方法的退出就已经可以被销毁了,没有必要等到gc。
另外,我们也知道,栈帧内的局部变量会随着栈帧的关闭而销毁。
所以,能不能把上面的代码改成这样:
private void alloc() { String name = "a"; int age = 10;}
这样不就可以让name
和age
随着栈帧关闭而销毁了。
没错,以上过程就是对象在栈上分配,该方式依赖于两个方面:
逃逸分析:分析一个对象的作用域,是否不被外部方法所引用,只在本方法中使用。
就是上面讨论的user
是否可以随着alloc
方法退出而销毁。
标量替换: 通过逃逸分析确定一个对象不会被外部访问,JVM就不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替。
就是上面将代码改造的过程。
标量:不可被进一步分解的量, 如基础数据类型
聚合量:可以被进一步分解的量, 如对象
大多数情况下,对象都是在年轻代中Eden区分配。
Eden区内存不够时,则触发YoungGc,将存活的对象放入Survivor区。
大对象就是需要大量连续内存空间的对象,关于如何定义大对象可以通过参数-XX:PretenureSizeThreshold=1m
指定(这里设置的是1m),当对象的大小超过1m后,会直接进入老年代。
因为在业务中,通常大对象都是长期存活的对象,如大数组,静态变量。对于长期存活的对象,如果还是像普通对象一样在Eden区和Survivor区反复gc后才进入老年代,这是没有意义且耗费资源的事情,所以应当让这样的对象尽早进入老年代,提升gc效率。
注意,业务对象千万不要做成了大对象,因为它会直接进入老年代,导致频繁full gc
如果对象每并经过一次Young GC后仍然能够存活,则对象年龄+1,当年龄达到15(默认值)后就会进入老年代。
对于这一点,我们可以设置合理的阈值。
比如我明确知道业务中对象绝对不会超过3次gc就会被回收,那么反过来说,超过3次gc还没有被回收的对象,就是些可以长期存活的对象。
长期存活的对象应该让它尽早进入老年代,所以我保险一点,可以设置阈值为5。
流程图
随着程序的运行,当一些对象变成了垃圾对象,就会随着gc被回收内存。
那JVM要如何判断哪些对象是垃圾对象呢?
引用计数法是一种非常简单的实现:给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1。
任何时候计数器为0的对象就是不可能再被使用的。
当它有一个致命的问题:循环引用。
如下代码:
public class ReferenceCountingGc {Object instance = nulL; public static void main(String[〕 args) { ReferenceCountingGc objA = new ReferenceCountingGc(); ReferenceCountingGc obiB = new ReferenceCountingGc(); obiA.instance = objB; obiB.instance = objA; objA=null; objB=null; }}
除了对象objA
和objB
相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为0,于是引用计数算 法无法通知GC回收器回收他们。
将GC Roots
对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象
GC Roots:线程栈的本地变量、静态变量、本地方法栈的变量等等
当触发gc后,便会有垃圾收集器对这些可以回收的对象进行回收,对象也就被销毁了。
本文从一个对象在JVM的经历出发,串讲了JVM中的一些知识点,如对象的结构是怎样的?JVM是如何给对象分配内存的,分配的方式又有哪些?
其中有部分内容由于篇幅所限,阿紫会另开章节单独介绍,如类加载机制是怎样的?垃圾收集器又有哪些?
本文到此结束,希望大家有所收获。
]]>索引是MySQL的数据结构,关系着MySQL如何存储数据,查询数据;而如何操作数据,解决多线程时操作数据带来的问题,则需要通过事务来完成。
InnoDB引擎支持事务,MyISAM引擎不支持事务
事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为事务的ACID属性
用大白话说:
原子性:事务里的所有操作,要么是commit全部提交成功,要么是rollback全部回滚。
一致性:个人认为更多在于业务操作,如A用户向B用户转账100,必须是A-100, B+100,不能出现A转账成功,B未收到情况。
隔离性:A事务在操作数据时,不受B事务影响。这点会在本文详细说明。
持久性:对数据的所有成功操作,都会落到磁盘上。
InnoDB中,一共有四种隔离级别:读未提交、读已提交、可重复读、可串行化。默认为可重复读。
它们分别会对应一些并发问题,如表格所示:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 有可能 | 有可能 | 有可能 |
读已提交 | 不可能 | 有可能 | 有可能 |
可重复度 | 不可能 | 不可能 | 有可能 |
可串行化 | 不可能 | 不可能 | 不可能 |
下面将对这些问题做详细解释
在该隔离级别下,事务A可以读到事务B尚未提交的数据。
设置方式:
set tx_isolation='read-uncommitted';
如以下事务A先进行查询用户数据, 此时jack
的余额为10
事务B修改jack
的余额为20
begin;update account set balance = balance + 10 where id = 1;
注意:此时事务B并未提交
事务A再次查询,发现jack
的余额已变为为20
若此时事务A用该数据进行业务处理,比如购买商品,完成之后,事务B发生回滚。那么就相当于事务A用了错误的数据进行了业务。
在该隔离级别下,事务A可以读到事务B已经提交的数据。
设置方式:
set tx_isolation='read-committed';
如以下事务A先进行查询用户数据, 此时jack
的余额为10
事务B修改jack
的余额为20
begin;update account set balance = balance + 10 where id = 1;
注意:此时事务B并未提交
事务A再次查询,jack
的余额仍然为10
此时事务B提交数据,事务A再次查询,发现jack
的余额变为了20
此时就会带来一个新的问题:在事务A中,明明没有对该条数据做任何修改,但多次查询发现数据一直变化,就会给人带来疑惑:我到底应该用哪个数据完成业务呢?
在该隔离级别下,事务A每次查询的数据都和第一次查询的数据相同。
设置方式:
set tx_isolation='repeatable-read';
如以下事务A先进行查询用户数据, 此时jack
的余额为10
事务B修改jack
的余额为20, 并且提交数据
begin;update account set balance = balance + 10 where id = 1;commit;
注意:此时事务B已经提交了
事务A再次查询,jack
的余额仍然为10
在其他事务中查询,可以发现其实jack
的余额已经是20了
现在,尝试在事务A中查询id<5
的数据,此时只查出两条数据
在其他事务中插入一条id=4
的记录并提交
INSERT INTO `account` (`id`, `name`, `balance`)VALUES(4, 'zhangsan', 30);
在事务A中更新id=4
的数据,注意,更新的是id=4
的数据
然后再次尝试查询id<5
的数据,此时发现多出了一条id=4
的数据
在同一个事务里,重复查询同一条数据,数据不会发生改变,这是可重复读。
但是存在可以更新一条“不存在”的数据,然后把它查出来,这是幻读。
对于该事务来说不存在
在该隔离级别下,执行任何sql都是串行的(加锁)。
设置方式:
set tx_isolation='serializable';
如以下事务A先进行查询用户数据, 此时jack
的余额为10
在事务B中尝试修改该条数据,你会发现,锁住了
在该隔离级别,执行任何sql,包括查询sql,MySQL都会给你加上一把锁,让所有的操作都成线性的,这便是可串行化。
该隔离级别性能极低,不建议使用。
在本章节中,简单介绍了MySQL的四种隔离级别和他们所带来的问题。
最后再说一点关于读已提交
和可重复读
的想法:
在读已提交的隔离级别下,虽然说在同一事务中,存在数据发生变化的情况,但实际在开发时,很少会重复查询同一条数据,所以问题其实不大,并且读已提交的性能要比可重复读要好一些,如果想要提升性能,业务又不存在或者不在意极端的情况,可以考虑使用读已提交的隔离级别。
]]>在平常,我们写的分页查询sql一般是这样
explain select * from employees order by name limit 10000,10;
这样的sql你会发现越翻到后面查询会越慢,这是因为这里看似是从表中查询10条记录,实际上是在表中查询了10010条记录,然后将10000条记录丢弃所得到的结果。
优化sql如下:
explain select * from employees t1 join (select id from employees order by `name` limit 10000, 10) t2 on t1.id = t2.id;
执行计划:
优化思路:先使用覆盖索引方式查出10条数据,再使用这10条数据连接查询。
覆盖索引:查询的字段被索引完全覆盖,比如id在联合索引中
原理:结合MySQL数据结构, 主键索引(innodb引擎)会存储完整的记录,而二级索引只存储主键。MySQL一个结点默认为16KB。
故:二级索引一个叶子结点能够存放的记录会多的多,扫描二级索引比扫描主键索引的IO次数会少很多。
图示:
优化前sql查询时间
set global query_cache_size=0;
set global query_cache_type=0;
优化后:
jion查询分为内连接,左连接,右连接;
关联时又有两种情况:使用索引字段关联,不使用索引字段关联。
我以案例举例说明,如以下两张表t1,t2, a字段有索引,b字段无索引
CREATE TABLE `t1` ( `id` int(11) NOT NULL AUTO_INCREMENT, `a` int(11) DEFAULT NULL, `b` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_a` (`a`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;
t2表结构与t1完全相同
其中t1表具有1w条数据,t2表具有100条数据。
explain select * from t1 inner join t2 on t1.a = t2.a;
执行计划:
分析执行计划:
1、先全表扫描t2表(100条数据)
2、使用t2表的a字段关联查询t1表,使用索引idx_a
3、取出t1表中满足条件的行,和t2表的数据合并,返回结果给客户端
成本计算:
1、扫描t2表:100次
2、扫描t1表:100次,因为使用索引可以定位出的数据,这个过程的时间复杂度大概是O(1)
此处说的100次只是为了更好的计算和理解,实际可能就几次
翻译成代码可能是这样:
for x in range(100): # 循环100次 print(x in t1) # 一次定位
所以总计扫描次数:100+100=200次
这里引出两个概念
小表驱动大表, 小表为驱动表,大表为被驱动表
inner join
时,优化器一般会优先选择小表做驱动表, 排在前面的表并不一定就是驱动表。left join
时,左表是驱动表,右表是被驱动表right join
时,右表时驱动表,左表是被驱动表嵌套循环连接 Nested-Loop Join(NLJ) 算法
一次一行循环地从第一张表(驱动表)中读取行,在这行数据中取到关联字段,根据关联字段在另一张表(被驱动表)里取出满足条件的行,然后取出两张表的结果合集。
使用索引字段关联查询的一般为NLJ算法
explain select * from t1 inner join t2 on t1.b = t2.b;
执行计划:
Extra列:Using join buffer:使用
join buffer
(BNL算法)
分析执行计划:
1、先全表扫描t2表(100条数据),将数据加载到join buffer
(内存)中
2、全表扫描t1表,逐一和join buffer
中的数据比对
3、返回满足条件的行
成本计算:
1、扫描t2表:100次
2、扫描t1表:1w次
3、在内存中比对次数:100*10000=100w次
翻译成代码可能是这样:
for i in range(100): # 循环100次 for j in range(10000) # 循环10000次
所以总计扫描次数为:100+10000=10100次,内存中数据比对次数为:100*1w=100w次
这个过程称为:基于块的嵌套循环连接Block Nested-Loop Join(BNL)算法
把驱动表的数据读入到join buffer
中,然后扫描被驱动表,把被驱动表每一行取出来跟join buffer
中的数据做对比。
join buffer
不够时怎么办?案例中t2表只有一百行数据,如果数据量很大时,比如t2表一共有1000行数据,join buffer
一次只能放800行时怎么办?
此时会使用分段放的策略:先放入800行到join buffer
,然后扫描t1表,比对完毕之后,将join buffer
清空,放入剩余的200行,再次扫描t1表,再比对一次。
也就是说:此时会多扫描一次t1表,如果2次都放不下,就再多扫描一次,以此类推。
join查询中一般有两种算法:
NLJ算法比BNL算法性能更高
关联查询的优化方式:
left join
和right join
是要注意。join buffer
,避免多次扫描被驱动表。NLJ算法性能这么好,为什么非索引字段关联时不使用这种算法呢?
这是因为NLJ算法采用的是磁盘扫描方式:先扫驱动表,取出一行数据,通过该数据的关联字段到被驱动表中查找,这个过程是使用索引查找的,非常快。
如果非索引字段采用这种方式,那么通过驱动表的数据的关联字段,到被驱动表中查找时,由于无法使用索引,此时走的是全表扫描。
比如驱动表有100条数据,那么就要全表扫描被驱动表100次,被驱动表有1w条数据,那么就是磁盘IO:100*1w=100w次,这个过程是非常慢的。
in和exist的优化只有一个原则:小表驱动大表
in:当B表的数据集小于A表的数据集时,in优于exists
select * from A where id in (select id from B)
即in中的表为小表
exist: 当A表的数据集小于B表的数据集时,exists优于in
select * from A where exists (select 1 from B where B.id = A.id)
即外层的表为小表
关于count这里就不详细说明了,因为各种用法效率都差不多。
字段有索引:count(*)≈count(1)>count(字段)≈count(主键 id)
字段无索引:count(*)≈count(1)>count(主键 id)>count(字段)
关于索引部分到这里就差不多了,总结一下索引设计原则
先写代码,再根据情况建索引
一般来说,都是都没代码写完之后,才能明确哪些字段会用到索引,但我也发现大部人写完代码就不管了。所以如果在设计时可以初步知道哪些字段可以建立索引,那么可以在设计表时就建好索引,写完代码再做调整
尽量让联合索引覆盖大部分业务
一个表不要建立太多的索引,因为MySQL维护索引也是需要耗费性能的,所以尽量让一到三个联合索引就覆盖业务里面的sql查询条件
不要在小基数的字段上建索引
如果在小基数的字段上建立索引是没有意义的,如性别,一张1千万数据的表,对半分的话500w男,500w女,筛选不出什么。
字符串长度过长的索引可以取部分前缀建立索引
字段过长的话也会导致索引占用的磁盘空间比较大,如varcahr(255), 这个时候可以取部分前缀建立索引,如前20个字符。但要注意的是,这样会导致排序失效,因为只取了前20个字符串,索引只能保证大范围的有序。
也可以在后期根据一定的计算规则计算最佳索引长度:distinct(left(字段,长度))/count约等于1
后期可以根据慢sql日志继续优化索引
随意业务的迭代,查询条件也会发生改变,此时可以根据慢sql持续优化索引
可以建唯一索引,尽量建唯一索引
where条件和order by冲突时时,优先取where的条件建索引
因为筛选出数据后,一般数据量比较少,排序的成本不大,所以优先让数据更快的筛选出来。
这篇文章继续讨论索引优化,如果没有看过上篇文章MySQL索引优化一一定看了再看这篇。
你别问我:阿紫,那你怎么不写到一篇文章里呀?
好吧,其实没有什么特别的理由,单纯是累了,想休息休息接着写。
好了继续继续。
这篇文章的主要内容:讨论一下OrderBy、Join、In、Exist相关的原理。
order by同样遵循最左前缀法则,只有当order by的字段是最左字段或者跟随where条件的字段时,才能使用索引排序
排序排序,关键就在于:有序
如以下联合索引:name_age_position
name字段是天然有序的,name值相同时,age是有序的,age相同时,position是有序的。
那应该怎么判断sql使用了索引排序呢?
如以下sql
explain select id from employees order by name;
Extra列:
Using index:使用覆盖索引
Using filesort:将用文件排序而不是索引排序,数据较小时从内存排序,否则需要在磁盘完成排序。
只要没有Extra列出现use filesort,那么就是用的索引排序
再看看使用文件排序的sql
explain select id from employees order by age;
注意,使用了索引是使用了索引,文件排序是文件排序,这是两码事。
比如你使用了索引进行查找数据,但是查找出的数据是用的文件排序。
接下来看看一些案例
sql1
explain select * from employees where name = 'zhangsan18' order by age,position;
索引排序,age跟在name字段后,position跟在age字段后
sql2
explain select * from employees where name = 'zhangsan18' order by position,age;
文件排序,因为该sql是先使用position字段排序,再使用age字段排序,而position字段在name相同时依旧是无序的。
sql3
explain select * from employees where name = 'zhangsan18' and age = 18 order by position,age;
索引排序,position跟在age后,是有序的,而orderby后的age其实会被优化成常量,因为是通过age=10
查询出的数据
sql4
explain select * from employees where name = 'zhangsan18' order by age asc,position desc;
文件排序,虽然age字段可以用索引排序,但是position字段逆序排序。
可能会不太好理解,这里结合图说明一下
索引是先通过age字段排序,然后对age字段相同的记录,进行position逆序排序,最终查询出的结果是这样的
所以position字段需使用文件排序。
sql5
select * from employees where name = 'zhangsan18' order by age desc,position desc;
索引排序,因为age,position字段都是逆序的,相当于是索引上从右往左遍历
sql6
explain select * from employees where name > 'zhangsan18' order by age,position;
文件排序,因为name走范围查询,age字段走不了索引了。同上篇索引优化一中sql5的分析
sql7
explain select * from employees where name >= 'zhangsan18' order by age,position;
依旧是文件排序,如果你看了上文,你可能又会有疑惑了:age字段不是会走索引吗?咋是文件排序勒?
这里再强调一遍:走索引是走索引,排序是排序。
没错,在name=zhangsan18
时,age,position是有序的,可以使用索引排序。
但是在name>zhangsan18
时,age,position是无序的,需要使用文件排序。
15,25,16,33:无序的对吧
好了,关于排序的案例就到这里,更多的案例就还是由你自己去探索吧
文件排序分为单路排序和双路排序
一次性取出满足条件行的所有字段,然后在sort buffer
中进行排序
首先根据相应的条件取出相应的排序字段和可以直接定位行数据的行id(主键),然后在sort buffer
中进行排序,排序完后需要再次取回其它需要的字段。
MySQL通过比较系统变量max_length_for_sort_data
(默认1024字节)的大小和需要查询的字段总大小来判断使用哪种排序模式。
max_length_for_sort_data
,那么使用单路排序模式max_length_for_sort_data
,那么使用双路排序模式。1、如果可以使用索引排序,尽量使用索引排序,但是实在没有办法进行索引排序也不要勉强,优先对where筛选语句做索引优化,因为筛选出的数据往往是很少的,排序成本很低。
2、如果没有办法使用文件排序,服务器内存又充足的情况下,那么可以适当调整下max_length_for_sort_data
,让MySQL使用单路排序,这样可以减少回表,效率会好一些。
索引优化这四个字说实话我认为其实挺难理解的。看到这四个字我脑门上是:????
索引还要优化吗?调优SQL一般来说不就是看它有没有走索引,没走索引给它加上索引就好了吗?
嗯,所以你是怎么给它加索引的?
看SQL应该怎么走索引撒!
那SQL是怎么走索引的呢?又是怎么判断这条SQL会不会走索引呢?
我:…, 咱今天就来分析分析!
要是你还不了解MySQL底层的数据结构,建议你先看看MySQL数据结构
我们一般要优化的都是复杂SQL,而复杂SQL一般走的都是联合索引,说到联合索引的匹配规则,就逃不开这个:最左前缀法则
最左前缀法则即为:索引的匹配从最左边的字段开始,匹配成功才能往右继续匹配下一个字段。
不理解?没关系,我们先来看看这个联合索引:name_age_position
联合索引是以三个字段name
,age
,position
组成,并且创建该索引时字段顺序为name、age、positon。
那么该索引就会以这样的方式排序(索引就是排好序的高效的数据结构)
如上图所示,从zhangsan18
到zhangsan100
是顺序的,而name都为zhangsan18
的三个结点中,age
又是从小到大排序,age
相同时position
也是从小到大排序。
请你一定要把这个数据结构牢记于心,忘了就看看
现在通过这个联合索引再来解析一下最左前缀法则:在索引匹配时,必须先能够匹配name字段(最左边的),才能继续匹配age字段(下一个), age字段匹配成功了才能匹配position字段。
为什么?
因为联合索引中的最左边字段是有序的,而第二个字段是在第一个字段相同的情况下有序,第三个字段是在第二个字段相同的情况下有序。
如果你想要用age字段直接在联合索引中查找数据,对不起,找不到,因为age字段中联合索引中是无序的。
你把第一行name字段遮掉看看age字段的情况:18,18,20,15,25,16,33。无序的对吧。
还是有点迷惑?没关系,我们再来通过案例分析分析。
什么是走索引?就是看索引会不会起到作用,能够起到作用就叫走了索引,没有起到作用就叫没走索引。
表结构:
CREATE TABLE `employees` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(24) NOT NULL DEFAULT '' COMMENT '姓名', `age` int(11) NOT NULL DEFAULT '0' COMMENT '年龄', `position` varchar(20) NOT NULL DEFAULT '' COMMENT '职位', `hire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时\r\n间', PRIMARY KEY (`id`), KEY `idx_name_age_position` (`name`,`age`,`position`) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=100001 DEFAULT CHARSET=utf8 COMMENT='员工记录表';
sql1
explain select * from employees where name > 'zhangsan18'
name字段是联合索引最左边的字段,所以会走索引
sql2
explain select * from employees where age = 18
age字段并非联合索引最左边的字段,在索引中无序,故不走索引,全表扫描
sql3
explain select * from employees where name = 'zhangsan18' and age = 20;
name字段和age字段都会走索引,因为在name字段相同时,age字段是有序的, 所以此时age也可以走索引。
以上图为例,当定位到zhangsan18
时,可以直接定位到age=20
这条数据,不需要从age=18
的地方遍历寻找,所以索引对age字段也起到作用了。
你现在明白什么是最左前缀法则了吧,还不明白就私信我吧[叹气.jpg]。
现在,我们再来通过一些sql继续深挖这最左前缀法则
。
sql4
explain select * from employees where age = 20 and name = 'zhangsan18';
和sql3相同,name和age都会走索引,最左前缀和你sql语句的位置无关,mysql在执行时会自动调整位置,也就是改成name = 'zhangsan18' and age = 20
sql5
explain select * from employees where name > 'zhangsan18' and age = 20;
只有name字段会走索引,age不会走索引,因为此时mysql的查询逻辑是定位到name=zhangsan18
最右边的一条数据,然后通过叶子结点的指针向右扫描遍历,索引对age字段未起到作用。如图
explain结果:
sql6
explain select * from employees where name >= 'zhangsan18' and age = 20;
和sql5差不多,唯一的区别就是name是大于等于。此时name和age都会走索引。
现在,我估计你一定晕了,网上不是说范围查找会导致索引失效吗?怎么还走了age字段。
这样,我把sql这样写:
explain select * from employees where (name = 'zhangsan18' and age = 20) or (name > 'zhangsan18' and age = 20);
name = 'zhangsan18' and age = 20
部分:name和age都会走索引,这个没问题吧?
name > 'zhangsan18' and age = 20
部分:name走索引,age不走索引,这个也我没问题吧?
合起来就是name和age都会走索引,因为name = 'zhangsan18' and age = 20
时age要走索引。
还是迷惑?那梳理下流程。
mysql执行时先定位到name=zhangsan18
, 然后由于后面还有个age=20
条件,所以会直接定位到这里
然后再往右扫描name>zhangsan18
的记录, 你告诉我这个过程有没有用上age字段的索引?用上了吧,所有age字段也会走索引,也仅仅是这个时候会走索引,后面name>zhangsan18
的还是不走索引。
sql7
explain select * from employees where name like 'zhangsan18%' and age = 10
name和age都会走索引,和sql6一样理解就好。
sql8
explain select * from employees where name between 'zhangsan18' and 'zhangsan50' and age = 10
name和age都会走索引
到这里,你对最左前缀法则应该会有个深刻的认识了,更多的想法,就由你自己去探索啦
MySQL在5.6之后加了一个优化:索引下推,可以在索引遍历过程中,对索引中包含的所有字段先做判断,过滤掉不符合条件的记录之后再回表,可以有效的减少回表次数
拿这条sql举例:
explain select * from employees where name > 'zhangsan18' and age = 20;
这条sqlname
字段走索引,age
不走索引,在没有索引下推时,查询逻辑是这样的:
1、存储引擎通过联合索引找到name > 'zhangsan18'
的记录
2、然后使用联合索引存储的主键进行回表操作,查询出所有数据
3、将数据返回给Server层
4、Server层判断这条记录的age
是否为20, 是则返回给客户端,否则丢弃
这里就有个优化点,在第一步用联合索引找到name > 'zhangsan18'
的记录时,能不能直接判断age
是否为20?如果是再进行后面的步骤。
哎,你觉得能不能?
能!age字段本来就在联合索引里面,直接判断就完事了~
所以,这就是索引下推。简单吧~
]]>