【Java EE初阶七】多线程案例(生产者消费者模型)

1. 阻塞队列

        队列是先进先出的一种数据结构;

        阻塞队列,是基于队列,做了一些扩展,适用于多线程编程中;

阻塞队列特点如下:

        1、是线程安全的

        2、具有阻塞的特性

                2.1、当队列满了时,就不能往队列里放数据,就会阻塞等待,等队列中的数据出队列后,导致队列没满时,才能放数据。

                2.2、当当队列空了时,就不能从队列里拿数据,就会阻塞等待,等有数据进入队列后,导致队列不为空时,才能拿数据。

        由于阻塞队列的用处非常大,基于阻塞队列的功能,我们就可以实现多线程案例的第三种案例~ 生产消费者模型(其实描述的就是一种多线程编程的方法),引入生产者消费者模型(尤其是后端开发),生产者往队列中写入数据,消费者从队列中消费数据;

        阻塞队列总的来说就是由于前后执行顺序的线程由于一方面的速度过快,另外一方面的速度过慢,而导致整体的执行顺序出现不流畅的画面(快的线程为了使自己的产出能被另外一方面合理的消化),该方面线程不得不阻塞,等待另外一方面将产能消化之后,继续执行线程,制造产能;

2. 生产者消费者模型

        生产者消费者模型是一种很朴素的概念,描述的是一种多线程编程的方法。

 2.1 引入生产者消费者模型的意义

        2.1.1 解耦合

        引入该模型,就可以更好的做到“解耦合”(把代码的耦合程度,从高将到低-->就称为解耦合)

        在实际开发中,会涉及到 “分布式系统” ,服务器的整个功能不是由一个服务器实现的,而是由多个服务器组成,各自实现各自的一部分功能,再通过网络通信,把这些服务器联系起来,最终完成整个服务器的功能。典型分布式例子通过下图来进行简单的讲解:

        如上图所示,在该模型中入口服务器A与B、入口服务器A与C服务器的联系是密切相关的,请求要经过入口服务器A,才能传达给B、C服务器,即B、C服务器拿到想要的数据,再返回给入口服务器A,通过入口服务器A,再把响应传给客户端。

        但是如果请求突然骤升,这时超过入口服务器A接收请求的峰值,这时入口服务器A就挂了,入口服务器A挂了后,B、C服务器拿不到请求,也会挂掉,这就体现了入口服务器A和B、C服务器的耦合性比较高。

        当然如果B或C挂了的话,A大概率也会挂;

        当我们在入口服务器A和B、C服务器之间引入阻塞队列时,如下图所示:

        如上图所示,如果入口服务器A挂了,但是阻塞队列中还有请求的数据,至少不会因入口服务器挂A了,B、C服务器也挂了

        故此,入口服务器A和B、C、D服务器的耦合性也就降低了。

        上述描述的阻塞队列,并非是简单的数据结构,而是基于这个数据结构实现的服务器程序,且被部署到单独的主机上来;

2.1.2 削峰填谷

       如上图所示:当客户端这边的请求突然骤增时,入口服务器A一般来说是比较能抗压的,但是也是有极限的,这时我们引入阻塞队列,可以把这些请求数据都放进阻塞队列中,形成一个缓冲区,如此一来,即使外面的请求达到了峰值,也是由阻塞队列来承担,这样就形成了削峰填谷的效果。

        关于阻塞队列和消息队列的区别:

        阻塞队列:数据结构

        消息队列:基于阻塞队列实现服务器程序

3. 手敲代码模拟实现阻塞队列

3.1 了解阻塞队列

      java标准库提供了现成的阻塞队列这一数据结构,如下图所示:

        

        阻塞队列是基于队列扩展而来的,且在阻塞队列中,put是在具备阻塞功能的入队列操作,take方法是带阻塞功能的出队列操作,阻塞队列没有提供带有阻塞功能获取首元素的方法;

        java自带的阻塞队列的代码实现如下:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(10);
        blockingQueue.put("smallye");
        String s1 = blockingQueue.take();
        System.out.println("第一个打印:s1 = " + s1);
        s1 = blockingQueue.take();
        System.out.println("第二个打印:s1 = " + s1);
    }
}

        结果如下:

        问题分析:

        主要是线程卡住了,当进行第二次出队列时,由于当前阻塞队列是空的,所以要等进行阻塞等待,当有元素入队列时,我们才能进行出队列操作。

3.2 实现阻塞队列

        我们尝试实现一个阻塞队列,要求达到与标准库中的队列有着类似的效果;

        步骤如下:

        1、先实现普通队列

        2、再加上线程安全

        3、再加上阻塞功能

3.2.1 先实现普通队列

        代码如下所示:

// 为了简单, 不写作泛型的形式. 考虑存储的元素就是单纯的 String
class MyBlockingQueue {
    private String elems[] = null;
    private int head = 0;//记录头结点
    private  int tail = 0;//记录尾结点
    private int size = 0;//队列元素个数
    //构造方法,定义队列的容量大小
    public MyBlockingQueue(int capacity) {
        this.elems = new String[capacity];
    }
    //入队列
    public void put(String elem) {
        //判断容量满了没,满了就不能入队列,要阻塞等待
        if(size >= this.elems.length) {
            //阻塞等待,先不写,先实现普通功能的队列
            return;
        }
        //入队列
        elems[tail] = elem;
        tail++;
        //因为是循环队列,所以要判断尾巴有没有超过容量大小下标,超过了就要从0开始了
        if(tail > elems.length) {
            tail = 0;
        }
        //队列元素要++
        size++;
    }
    
    //出队列
    public String take() {
        String elem = null;
        //要判断队列是不是空的,空就不能出队列了,要阻塞等待
        if(size == 0) {
            //阻塞等待,因为是先实现普通队列的功能,所以后面再补充
            return null;
        }
        elem = elems[head];
        head++;
        //因为是循环队列,所以要判断头结点有没有超过容量大小下标,超过了就要0开始了
        if(head >= elems.length) {
            head = 0;
        }
        //出队列后,队列元素要--
        size--;
        return elem;
    }
}

        测试代码及结果如下:

public class Main {
    public static void main(String[] args) {
        MyBlockingQueue blockingQueue = new MyBlockingQueue(10);
        blockingQueue.put("smallye");
        String s1 = blockingQueue.take();
        System.out.println("第一个打印:s1 = " + s1);
    }
}

3.2.2 再加上线程安全

        对于不线程安全的代码我们要进行加锁操作,首先针对的就是写操作,该部分的代码块肯定是要加锁的,因为多线程同时执行写操作,会导致线程不安全,如下图所示:

        下面,我们讨论一下这两个代码要不要加锁,以take为例,如下图所示:

        当前代码里面的队列为空,但是依旧执行出队列的逻辑,所以我们判断条件也应该加锁;

        以put为例,如下图所示:

        当前代码里面的队列已经满了,但是依旧执行入队列的逻辑;

        修改后代码如下:

class MyBlockingQueue {
    Object locker = new Object();
    private String elems[] = null;
    private int head = 0;//记录头结点
    private  int tail = 0;//记录尾结点
    private int size = 0;//队列元素个数
    //构造方法,定义队列的容量大小
    public MyBlockingQueue(int capacity) {
        this.elems = new String[capacity];
    }
    //入队列
    public void put(String elem) {
        synchronized (locker) {
            //判断容量满了没,满了就不能入队列,要阻塞等待
            if(size >= this.elems.length) {
                //阻塞等待,先不写,先实现普通功能的队列
                return;
            }
            //因为这些都是写操作,也有读操作,多线程并发执行时,写操作是线程不安全的,要把这些打包成一个原子,加锁
            synchronized (locker) {
                //入队列
                elems[tail] = elem;
                tail++;
                //因为是循环队列,所以要判断尾巴有没有超过容量大小下标,超过了就要从0开始了
                if(tail > elems.length) {
                    tail = 0;
                }
                //队列元素要++
                size++;
            }
        }
    }
 
    //出队列
    public String take() {
        String elem = null;
        //因为这些都是写操作,也有读操作,多线程并发执行时,写操作是线程不安全的,要把这些打包成一个原子,加锁
        synchronized (locker) {
            //要判断队列是不是空的,空就不能出队列了,要阻塞等待
            if(size == 0) {
                //阻塞等待,因为是先实现普通队列的功能,所以后面再补充
                return null;
            }
            elem = elems[head];
            head++;
            //因为是循环队列,所以要判断头结点有没有超过容量大小下标,超过了就要0开始了
            if(head >= elems.length) {
                head = 0;
            }
            //出队列后,队列元素要--
            size--;
            return elem;
        }
    }
}

3.2.3 再加上阻塞功能

        我们给put要加上阻塞功能,就要在这条件判断上加上wait,我们用locker的对象给他wait,而且wait必须要在synchronized内使用,这里的locker正好能对应上;当这个队列满时,就阻塞等待,等take方法拿走一个数据时,才被唤醒,加上阻塞功能后的代码如下:

class MyBlockingQueue {
    Object locker = new Object();
    private String elems[] = null;
    private int head = 0;//记录头结点
    private  int tail = 0;//记录尾结点
    private int size = 0;//队列元素个数
    //构造方法,定义队列的容量大小
    public MyBlockingQueue(int capacity) {
        this.elems = new String[capacity];
    }
    //入队列
    public void put(String elem) throws InterruptedException {
        synchronized (locker) {
            //判断容量满了没,满了就不能入队列,要阻塞等待
            if (size >= this.elems.length) {
                //阻塞等待,先不写,先实现普通功能的队列
                synchronized (locker) {
                    locker.wait();
                }
            }
            //因为这些都是写操作,也有读操作,多线程并发执行时,写操作是线程不安全的,要把这些打包成一个原子,加锁
            synchronized (locker) {
                //入队列
                elems[tail] = elem;
                tail++;
                //因为是循环队列,所以要判断尾巴有没有超过容量大小下标,超过了就要从0开始了
                if(tail > elems.length) {
                    tail = 0;
                }
                //队列元素要++
                size++;
                locker.notify();
            }
        }
    }
 
    //出队列
    public String take() throws InterruptedException {
        String elem = null;
        //因为这些都是写操作,也有读操作,多线程并发执行时,写操作是线程不安全的,要把这些打包成一个原子,加锁
        synchronized (locker) {
            //要判断队列是不是空的,空就不能出队列了,要阻塞等待
            if (size == 0) {
                //阻塞等待,因为是先实现普通队列的功能,所以后面再补充
                synchronized (locker) {
                    locker.wait();
                }
            }
            elem = elems[head];
            head++;
            //因为是循环队列,所以要判断头结点有没有超过容量大小下标,超过了就要0开始了
            if(head >= elems.length) {
                head = 0;
            }
            //出队列后,队列元素要--
            size--;
            locker.notify();
            return elem;
        }
    }
}

        当我们进行阻塞wait时,一定要在适当的条件下notify,如下图所示:

        代码讲解:

        当put时,队列满了时就要阻塞等待,等take队列后,就会唤醒put操作,接着put就能入队列了;

        如果队列不满也不空时,每次put和take都会notify一次,其实不会有影响,因为就算没有其他线程在等待,唤醒也没有事,不会对程序造成啥影响。而且我们的代码,一定是要么满,要么空,要么不满也不空。

        但是,如果有两个线程同时put,现在队列是满的,A线程先阻塞,B线程也阻塞,这时有第三个线程take一次,把A线程的wait唤醒了,等A执行到下面的notify,A线程里put的notify就会唤醒B线程里的wait,但是因为A线程put了,和第三个线程的take一取一放抵消了,此时队列还是满的;因为A线程里的put把B线程里的wait唤醒了,这时已经是满了的队列还往里放元素,就造成了线程安全问题。

        解决方案:把条件判断if换成while循环语句,不是只判断一次,当有其他线程把wait唤醒后,还要再判断一次这个队列是不是满的或者是空的,如果不是满的或者不是空的,才释放这个wait,不然就要继续wait,如此该问题也就解决了。

最终代码:
 

class MyBlockingQueue {
    Object locker = new Object();
    private String elems[] = null;
    private int head = 0;//记录头结点
    private  int tail = 0;//记录尾结点
    private int size = 0;//队列元素个数
    //构造方法,定义队列的容量大小
    public MyBlockingQueue(int capacity) {
        this.elems = new String[capacity];
    }
    //入队列
    public void put(String elem) throws InterruptedException {
        synchronized (locker) {
            //判断容量满了没,满了就不能入队列,要阻塞等待
            while (size >= this.elems.length) {
                //阻塞等待,先不写,先实现普通功能的队列
                synchronized (locker) {
                    locker.wait();
                }
            }
            //因为这些都是写操作,也有读操作,多线程并发执行时,写操作是线程不安全的,要把这些打包成一个原子,加锁
            synchronized (locker) {
                //入队列
                elems[tail] = elem;
                tail++;
                //因为是循环队列,所以要判断尾巴有没有超过容量大小下标,超过了就要从0开始了
                if(tail > elems.length) {
                    tail = 0;
                }
                //队列元素要++
                size++;
                locker.notify();
            }
        }
    }
 
    //出队列
    public String take() throws InterruptedException {
        String elem = null;
        //因为这些都是写操作,也有读操作,多线程并发执行时,写操作是线程不安全的,要把这些打包成一个原子,加锁
        synchronized (locker) {
            //要判断队列是不是空的,空就不能出队列了,要阻塞等待
            while (size == 0) {
                //阻塞等待,因为是先实现普通队列的功能,所以后面再补充
                synchronized (locker) {
                    locker.wait();
                }
            }
            elem = elems[head];
            head++;
            //因为是循环队列,所以要判断头结点有没有超过容量大小下标,超过了就要0开始了
            if(head >= elems.length) {
                head = 0;
            }
            //出队列后,队列元素要--
            size--;
            locker.notify();
            return elem;
        }
    }
}

        在实际开发中,生产者消费者模型,往往是多个生产者,多个消费者;这里的生产者和消费者往往不仅仅是一个线程,也可能是一个独立的服务器,甚至是一组服务器程序。生产者消费者模型,最核心的部分还是阻塞队列,可以使用synchronized和wait / notify 达到线程安全与阻塞。

3.3 实现生产者消费者模型

        代码如下:

package thread;

// 为了简单, 不写作泛型的形式. 考虑存储的元素就是单纯的 String
class MyBlockingQueue {
    private String[] elems = null;
    private int head = 0;
    private int tail = 0;
    private int size = 0;

    // 准备锁对象, 如果使用 this 也可以.
    private Object locker = new Object();

    public MyBlockingQueue(int capacity) {
        elems = new String[capacity];
    }

    public void put(String elem) throws InterruptedException {
        // 锁加到这里和加到方法上本质一样的. 加到方法上是给 this 加锁. 此处是给 locker 加锁.
        synchronized (locker) {
            while (size >= elems.length) {
                // 队列满了.
                // 后续需要让这个代码能够阻塞.
                locker.wait();
            }
            // 新的元素要放到 tail 指向的位置上
            elems[tail] = elem;
            tail++;
            if (tail >= elems.length) {
                tail = 0;
            }
            size++;

            // 入队列成功之后唤醒
            locker.notify();
        }
    }

    public String take() throws InterruptedException {
        String elem = null;
        synchronized (locker) {
            while (size == 0) {
                // 队列空了.
                // 后续也需要让这个代码阻塞
                locker.wait();
            }
            // 取出 head 位置的元素并返回
            elem = elems[head];
            head++;
            if (head >= elems.length) {
                head = 0;
            }
            // 这个代码不要遗漏.
            size--;

            // 元素出队列成功之后, 加上唤醒
            locker.notify();
        }
        return elem;
    }
}

public class ThreadDemo28 {
    public static void main(String[] args) throws InterruptedException {
        MyBlockingQueue queue = new MyBlockingQueue(1000);

        // 生产者
        Thread t1 = new Thread(() -> {
            int n = 1;
            while (true) {
                try {
                    queue.put(n + "");
                    System.out.println("生产元素 " + n);
                    n++;
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        // 消费者
        Thread t2 = new Thread(() -> {
            while (true) {
                try {
                    String n = queue.take();
                    System.out.println("消费元素 " + n);

                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t1.start();
        t2.start();
    }
}

        结果如下:

        如图所示,生产者消费者模型大抵是生产一个,消费一个,主要是生产之后消费者再消费;

ps:关于阻塞队列和生产着消费者模型的内容就到这里了,如果对你有帮助的话就请一键三连哦!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/298733.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MATLAB插值函数

一、MATLAB插值函数概览 1&#xff09;本节重点介绍的插值函数 MATLAB插值函数适用情况基础句式interp1 函数interp1 主要用于一维数据的插值interp1(x, y, x_interp, ‘linear’); 其中 x 和 y 是已知数据点&#xff0c;x_interp 是要插值的目标点。interp2 函数interp2 用于…

VS code的使用介绍

VS code的使用介绍 简介下载和安装常用的插件使用教程快捷键 集成Git未找到 Git。请安装 Git&#xff0c;或在 "git.path" 设置中配置。操作步骤打开文件夹初始化仓库文件版本控制状态提交文件到git打开git操作栏位 好用的插件ChineseDraw.io Integration实体关系 Gi…

SpringSecurity集成JWT实现后端认证授权保姆级教程-环境搭建篇

&#x1f341; 作者&#xff1a;知识浅谈&#xff0c;CSDN签约讲师&#xff0c;CSDN博客专家&#xff0c;华为云云享专家&#xff0c;阿里云专家博主 &#x1f4cc; 擅长领域&#xff1a;全栈工程师、爬虫、ACM算法 &#x1f492; 公众号&#xff1a;知识浅谈 &#x1f525;网站…

C++ UTF-8与GBK字符的转换 —基于Linux 虚拟机 (iconv_open iconv)

1、UTF-8 和 GBK 的区别 GBK&#xff1a;通常简称 GB &#xff08;“国标”汉语拼音首字母&#xff09;&#xff0c;GBK 包含全部中文字符。 UTF-8 &#xff1a;是一种国际化的编码方式&#xff0c;包含了世界上大部分的语种文字&#xff08;简体中文字、繁体中文字、英文、…

Android 15即将到来,或将推出5大新功能特性

Android15 OneUI电池优化 三星最近完成了对其所有设备的稳定版 One UI 6.0 更新的推出&#xff0c;引起了用户的极大兴奋。据新出现的互联网统计数据显示&#xff0c;即将发布的基于 Android 15 的 One UI 7 将通过优化电池和功耗来重新定义用户体验&#xff0c;这是一项具有突…

[AutoSar]基础部分 RTE 04 数据类型的定义及使用

目录 关键词平台说明一、数据类型分类二、Adt三、Idt四、Base 数据类型五、units六、compu methods七、data constraint 关键词 嵌入式、C语言、autosar、Rte 平台说明 项目ValueOSautosar OSautosar厂商vector芯片厂商TI编程语言C&#xff0c;C编译器HighTec (GCC) 一、数据…

FineBI实战(2):案例架构说明及数据准备

1 系统架构 基于MySQL搭建数据仓库基于Kettle进行数据处理帆软FineBI基于MySQL搭建的数据仓库进行数据分析 2 数据流程图 通过Kettle将MySQL业务系统数据库中&#xff0c;将数据抽取出来&#xff0c;然后装载到MySQL数据仓库中。编写SQL脚本&#xff0c;对MySQL数据仓库中的数…

时序预测 | Matlab实现EEMD-SSA-BiLSTM、EEMD-BiLSTM、SSA-BiLSTM、BiLSTM时序预测对比

时序预测 | Matlab实现EEMD-SSA-BiLSTM、EEMD-BiLSTM、SSA-BiLSTM、BiLSTM时间序列预测对比 目录 时序预测 | Matlab实现EEMD-SSA-BiLSTM、EEMD-BiLSTM、SSA-BiLSTM、BiLSTM时间序列预测对比预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现EEMD-SSA-BiLSTM、…

Java十种经典排序算法详解与应用

数组的排序 前言 排序概念 排序是将一组数据&#xff0c;依据指定的顺序进行排列的过程。 排序是算法中的一部分&#xff0c;也叫排序算法。算法处理数据&#xff0c;而数据的处理最好是要找到他们的规律&#xff0c;这个规律中有很大一部分就是要进行排序&#xff0c;所以需…

关于 LockWindowUpdate 的最终总结

经过前面两篇文章的”洗礼”&#xff0c;我想&#xff0c;你应该知道了在何种情况下应该使用 LockWindowUpdate。 但接下来我要告诉你的是为什么不能使用它&#xff0c;即使是用于它本身的预期目的。 让我们回到古老的旧时代&#xff0c;那个时候&#xff0c;LockWindowUpdate…

docker、docker-compose 离线安装、shell脚本一键安装、卸载

注&#xff1a;二进制包&#xff0c;与脚本在同级目录 docker 离线安装&#xff1a; 包下载&#xff1a;https://download.docker.com/linux/static/stable/x86_64/ docker_install.sh&#xff1a; #!/bin/bash# 指定 Docker 版本和文件名 DOCKER_VERSION"24.0.7" D…

【InternLM】书生-浦语大模型demo搭建服务接口部署本地映射

目录 前言一、InternLM大模型介绍1-1、大模型简介1-2、InternLM大模型简介1-2-1、InternLM-7B1-2-2、InternLM-20B 二、从0开始搭建InternLM-Chat-7B 智能对话 Demo2-0、环境搭建2-1、创建虚拟环境2-2、导入所需要的包2-3、模型下载2-4、代码克隆2-5、终端运行 三、服务器接口部…

真核微生物基因组质量评估工具EukCC的安装和详细使用方法

介绍&#xff1a; GitHub - EBI-Metagenomics/EukCC: Tool to estimate genome quality of microbial eukaryotes 安装&#xff1a; docker&#xff1a; docker pull microbiomeinformatics/eukcc 推荐conda 环境&#xff1a; conda install -c conda-forge -c bioconda …

Python+Torch+FasterCNN网络目标检测识别

程序示例精选 PythonTorchFasterCNN网络目标检测识别 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对《PythonTorchFasterCNN网络目标检测识别》编写代码&#xff0c;代码整洁&#xff0c;规…

Java-网络爬虫(二)

文章目录 前言一、WebMagic二、使用步骤1. 搭建 Maven 项目2. 引入依赖 三、入门案例四、核心对象&组件1. 核心对象SipderRequestSitePageResultItemsHtml&#xff08;Selectable&#xff09; 2. 四大组件DownloaderPageProcessorSchedulerPipeline 上篇&#xff1a;Java-网…

物联网的感知层、网络层与应用层分享

物联网的概念在很早以前就已经被提出&#xff0c;20世纪末期在美国召开的移动计算和网络国际会议就已经提出了物联网(Internet of Things)这个概念。 最先提出这个概念的是MIT Auto-ID中心的Ashton教授&#xff0c;他在研究RFID技术时&#xff0c;便提出了结合物品编码、互联网…

打造清晰的日志管理策略:如何在 NestJS 中集成 winston 高级日志系统

前言 在Web应用程序的开发过程中&#xff0c;日志管理是不可或缺的一部分。日志可以帮助我们了解应用程序的运行状态&#xff0c;监控系统行为&#xff0c;以及在出现问题时快速定位和解决问题。 对于使用NestJS框架的项目来说&#xff0c;集成一个高效、可扩展的日志系统尤为…

听GPT 讲Rust源代码--compiler(25)

File: rust/compiler/rustc_target/src/spec/mod.rs 在Rust的源代码中&#xff0c;rust/compiler/rustc_target/src/spec/mod.rs文件的作用是定义和实现有关目标平台的规范。 SanitizerSet是一个结构体&#xff0c;用于表示目标平台上存在的sanitizer集合。 TargetWarnings是一…

基于OpenCV的透视变换

基础概念 透视变换(Perspective Transformation)是仿射变换的一种非线性扩展,是将图片投影到一个新的视平面(Viewing Plane)&#xff0c;也称作投影映射(Projective Mapping)。 原理&#xff1a;将二维的图片投影到一个三维视平面上&#xff0c;然后再转换到二维坐标下&#…

Mongodb使用指定索引删除数据

回顾Mongodb删除语法 db.collection.deleteMany(<filter>,{writeConcern: <document>,collation: <document>,hint: <document|string>} ) 删除语法中&#xff0c;除了指定过滤器外&#xff0c;还可以指定写入策略&#xff0c;字符序和使用的索引。 …