当前位置: 首页 > news >正文

Linux驱动开发(速记版)--并发与竞争

第十八章 并发与竞争

18.1 并发与竞争

18.1.1 并发

        早期计算机 CPU单核心时,由于 CPU执行速度快于I/O操作,常因等待 I/O而空闲

        为提高 CPU利用率,引入了并发执行理论。并发通过算法在CPU执行I/O等待时切换至其他任务,使多个任务看似同时执行,实际上CPU快速切换任务执行,从而提高了整体效率。

18.1.2 并行

        并发适用于单核CPU,通过任务切换模拟同时执行;

        并行针对多核CPU,每个核心独立执行不同任务,实现真正的同时运行,提高执行效率。

18.1.3 并发+并行

        实际场景中,CPU核心数少于任务数时,会同时出现并发(单核内任务切换)和并行(多核间任务独立执行)。

        例如,双核CPU执行四个任务时,每核并行执行一个任务,同时每核内部还需并发处理其他任务。

        为简化讨论,后续章节将并发与并行统称为并发。

18.1.4 竞争

        并发访问共享资源时可能导致的不一致性称为“竞争条件”。

        这种竞争由以下主要原因造成:

        多线程并发:在Linux等多任务操作系统中,多个线程(或进程)可能同时执行,它们尝试访问和修改同一资源时,若未进行适当同步,便会产生竞争条件。

        中断处理:系统中断(如硬件中断)能够打断当前正在执行的程序,如果中断服务例程(ISR)也访问了被打断程序正在使用的共享资源,且未做适当同步,将导致数据竞争。

        抢占式调度:在支持抢占式调度的系统中(如Linux 2.6及以后版本),高优先级任务可以中断低优先级任务的执行。若两者均操作同一共享资源,且未采取同步措施,则会发生竞争。

        多处理器并发:在多处理器(SMP)系统中,不同处理器核心可能并发执行不同线程,这些线程若同时访问同一共享资源而未同步,将引发跨核心的竞争条件。

        简而言之,并发访问共享资源时,若未加同步控制,多线程、中断处理、抢占式调度及多处理器并发均可能导致竞争条件的发生。

18.1.5 共享资源的保护

        共享资源是多个实体可访问的资源,为避免竞争需同步保护。

        比如同一个驱动里面,写两个方法,一个方法延时4秒,另一个方法延时2秒,两个方法读取同一个变量。装载驱动以后,用write方法写驱动两次,由于驱动装载后以文件形式存在,属于共享资源,会导致竞争条件,出现数据不一致。

第十九章 原子操作

19.1 原子操作

        在 Linux内核中,原子操作是确保数据在并发环境下一致性的关键机制。原子操作意味着操作在执行过程中不会被中断,从而避免了竞争条件。

19.1.1 原子整型操作

        在Linux内核中,atomic_tatomic64_t 分别用于32位和64位系统的原子整形操作。

        原子整型操作基于 内存屏障来确保原子性,

        Load Barrier 和 Store Barrier即读屏障和写屏障。

typedef struct { int counter; } atomic_t;  
#ifdef CONFIG_64BIT  
typedef struct { long counter; } atomic64_t;  
#endif
ATOMIC_INIT(int i);                          //初始化原子变量,并返回原子整型变量。
int atomic_read(atomic_t *v);                //读取原子变量的值。
void atomic_set(atomic_t *v, int i);         //设置原子变量的值。
void atomic_add(int i, atomic_t *v);         //原子地将i加到v上。
void atomic_sub(int i, atomic_t *v);         //原子地从v中减去i。
void atomic_inc(atomic_t *v);                //原子地将v加1。
void atomic_dec(atomic_t *v);                //原子地将v减1。
int atomic_dec_return(atomic_t *v);          //原子地将v减1,并返回新值。
int atomic_inc_return(atomic_t *v);          //原子地将v加1,并返回新值。
int atomic_sub_and_test(int i, atomic_t *v); //原子地从v中减去i,如果结果为0则返回真。
int atomic_dec_and_test(atomic_t *v);        //原子地将v减1,如果结果为0则返回真。
int atomic_inc_and_test(atomic_t *v);        //原子地将v加1,如果结果为0则返回真。
int atomic_add_negative(int i, atomic_t *v); //原子地将i加到v上,如果结果为负则返回真。

19.1.2 原子位操作

        原子位操作是直接对内存中的位进行操作,无需 atomic_t结构。

void set_bit(int nr, void *p);           //将p地址的第nr位置1。
void clear_bit(int nr, void *p);         //将p地址的第nr位清零。
void change_bit(int nr, void *p);        //将p地址的第nr位翻转。
int test_bit(int nr, void *p);           //获取p地址的第nr位的值。
int test_and_set_bit(int nr, void *p);   //将p地址的第nr位置1,并返回原值。
int test_and_clear_bit(int nr, void *p); //将p地址的第nr位清零,并返回原值。
int test_and_change_bit(int nr, void *p);//将p地址的第nr位翻转,并返回原值。

        使用了原子操作后,同一时间只允许一个应用来打开设备节点,以此防止共享资源竞争的产生。第二次打开同一个设备节点会报错。

第二十章 自旋锁

        原子操作只能对整形变量或者位进行保护,对于结构体或者其他类型的共享资源,就轮到自旋锁的出场了。

        自旋锁是一种用于保护共享资源的非阻塞锁机制。当某个线程尝试获取已被占用的锁时,该线程会持续消耗CPU资源,不断尝试获取锁,而不是被挂起。这种机制适用于锁持有时间非常短的场景,以避免线程切换的开销。

        内核中以 spinlock_t 结构体来表示自旋锁,定义在“内核源码/include/linux/spinlock_types.h” 文件中。

DEFINE_SPINLOCK();        //定义并初始化自旋锁。
spin_lock_init();                   //初始化自旋锁。
spin_lock();                          //尝试获取锁,若失败则持续自旋。
spin_unlock();                      //释放锁。 
spin_trylock();                      //尝试获取锁,若失败则立即返回0。
spin_is_locked();                 //检查锁是否被占用。

//定义并初始化自旋锁。
DEFINE_SPINLOCK(spinlock_t lock); //初始化自旋锁。
spin_lock_init(spinlock_t *lock);//尝试获取锁,若失败则持续自旋。
spin_lock(spinlock_t *lock);    //释放锁。 
spin_unlock(spinlock_t *lock);    //尝试获取锁,若失败则立即返回0
spin_trylock(spinlock_t *lock);  //检查锁是否被占用。
spin_is_locked(spinlock_t *lock); 

使用步骤:

        申请锁:在访问共享资源前尝试获取自旋锁。

        临界区:获取锁后进入临界区执行操作。

        释放锁:操作完成后释放锁。

#include <linux/spinlock.h>  DEFINE_SPINLOCK(lock);  //lock在使用DEFINE_SPINLOCK时是直接定义并初始化的,无需别处定义
//spinlock_t lock = SPIN_LOCK_UNLOCKED;// SPIN_LOCK_UNLOCKED用在需要初始化的场合,返回未上锁的spinlock_tvoid increment_counter() {  spin_lock(&lock);  // 获取锁  counter++;         // 临界区操作  spin_unlock(&lock); // 释放锁  
}  // 在其他线程中调用 increment_counter() 来安全地增加 counter

        如果在驱动的open函数中上了自旋锁,则另一个进程没有拿到锁就会自旋等待。

第二十一章 自旋锁死锁

        死锁是多个进程(或线程)在执行过程中,因互相等待对方持有的资源而无法继续执行,从而导致无限期阻塞的现象。在自旋锁(spinlock)的上下文中,死锁可以发生在以下几种情况:

1. 进程阻塞导致的死锁

情况描述:

        进程A持有自旋锁,在内核态中被阻塞(如等待I/O操作或睡眠)。如果此时内核调度了进程B,而进程B也尝试获取同一个自旋锁,由于自旋锁的特性(忙等待),进程B会无限期地循环检查锁的状态,而进程A因阻塞无法释放锁,导致死锁。

解决办法:

        确保持有自旋锁的代码执行时间尽可能短。

        避免在持有自旋锁时调用可能导致进程阻塞的函数(如睡眠、等待I/O等)。

2. 中断导致的死锁

情况描述:

        进程A持有自旋锁时,CPU响应了一个中断,中断处理函数也尝试获取同一个自旋锁。如果中断被允许,则中断处理函数将自旋等待锁释放,但锁由于被进程A持有而无法释放(因为进程A在中断发生时被暂停),导致死锁。

解决办法:

        在获取自旋锁之前禁用中断(使用spin_lock_irq或spin_lock_irqsave)。

        在释放自旋锁后重新启用中断(使用spin_unlock_irq或spin_unlock_irqrestore)。

//禁止本地中断,并获取自旋锁
void spin_lock_irq(spinlock_t *lock) //激活本地中断,并释放自旋锁
void spin_unlock_irq(spinlock_t *lock) //保存中断状态,关闭中断并获取自旋锁。
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags) //用来存储函数调用前的中断状态
//恢复中断状态,打开中断并释放自旋锁
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) //用来存储函数调用前的中断状态//关闭下半部,获取自旋锁
void spin_lock_bh(spinlock_t *lock) 
//打开下半部,获取自旋锁
void spin_unlock_bh(spinlock_t *lock) 

第二十二章 信号量

        信号量是操作系统中用于同步和互斥的机制,它通过一个全局变量(信号量值)来控制对共享资源的访问。信号量可以是计数型(允许多个线程同时访问)或二值型(只允许一个线程访问)。信号量的值不能为负,当值为0时,访问资源的线程需等待。访问资源时信号量减一,访问完成后加一。

        相比于自旋锁,信号量具有休眠特性,适用于长时间占用资源的场合,但不能用于中断处理。在短时间访问资源的场景下,自旋锁效率更高。

        同时使用自旋锁和信号量时,应先获取信号量,再获取自旋锁,以避免在持有自旋锁时进入睡眠状态

        Linux内核中,信号量由 semaphore结构体表示,包含锁、计数器和等待队列。

/*信号量结构体*/
struct semaphore { atomic_t count;             // 信计数值  raw_spinlock_t lock;        // 自旋锁  struct list_head wait_list; // 等待该信号量的线程队列  
};  // 注意:raw_spinlock_t 是Linux内核中用于低级自旋锁的类型,  
// 它与通用的 spinlock_t 不同,后者在中断上下文中可能不安全。  

DEFINE_SEMAPHORE();    //定义并初始化信号量为1。
sema_init();                           //初始化信号量,设置其值为val。
down();                                  //阻塞式获取信号量,无法被中断。
down_interruptible();             //可中断地获取信号量,允许被信号中断。   
down_trylock();                     //获取信号量,拿不到则返回非0值,不阻塞。
up();                                      //释放信号量。

//定义并初始化信号量为1。
DEFINE_SEMAPHORE(name);  //初始化信号量,设置其值为val。
sema_init(struct semaphore *sem, int val); //阻塞式获取信号量,无法被中断。
down(struct semaphore *sem);   //可中断地获取信号量,允许被信号中断。            
down_interruptible(struct semaphore *sem); //尝试获取信号量,获取不到则返回非0值,不阻塞。
down_trylock(struct semaphore *sem);   //释放信号量。
up(struct semaphore *sem);                 
#include <linux/semaphore.h>  struct semaphore sem;  // 初始化信号量  
sema_init(&sem, 1); // 例如,设置为二值信号量  // 访问共享资源  
void access_resource(void) {  if (down_interruptible(&sem)) {  // 获取信号量失败,可能是被信号中断  return;  }  // 访问共享资源...  // 释放信号量  up(&sem);  
}

第二十三章 互斥锁

        互斥锁确保多线程访问共享资源时互斥,即一次只有一个线程能访问。锁有两种状态:锁定和非锁定。线程访问资源前需先尝试锁定锁,若锁已被占用则线程等待;锁释放后,等待的线程继续执行。

        在Linux内核中,互斥锁通过 mutex结构体表示,定义在include/linux/mutex.h中。它包含了一些关键的成员,如锁的所有者、等待锁的线程队列等。

/*互斥锁结构体*/
struct mutex {  atomic_long_t owner;       //锁持有者spinlock_t wait_lock;      //自旋锁struct list_head wait_list;//等待该信号量的线程对列// 其他调试和性能优化相关字段  
};

DEFINE_MUTEX();        //定义并初始化一个互斥锁变量
mutex_init();                    //初始化一个互斥锁
mutex_lock();                   //尝试获取锁,如果锁被占用则休眠
mutex_unlock();               //释放锁        
mutex_is_locked();          //判断锁是否被占用

//定义并初始化一个互斥锁变量
DEFINE_MUTEX(name);//初始化一个互斥锁
void mutex_init(struct mutex *lock);//尝试获取锁,如果锁被占用则休眠
void mutex_lock(struct mutex *lock);//释放锁
void mutex_unlock(struct mutex *lock)//判断锁是否被占用
int mutex_is_locked(struct mutex *lock) 
#include <linux/mutex.h>  DEFINE_MUTEX(my_mutex);  void thread_function(void) {  mutex_lock(&my_mutex);  // 访问或修改共享资源  // ...  mutex_unlock(&my_mutex);  
}  // 假设在多个线程或任务中调用 thread_function

相关文章:

Linux驱动开发(速记版)--并发与竞争

第十八章 并发与竞争 18.1 并发与竞争 18.1.1 并发 早期计算机 CPU单核心时&#xff0c;由于 CPU执行速度快于I/O操作&#xff0c;常因等待 I/O而空闲。 为提高 CPU利用率&#xff0c;引入了并发执行理论。并发通过算法在CPU执行I/O等待时切换至其他任务&#xff0c;使多个任…...

AI赋能,数字技术服务平台促进产业协同发展

在当今数字化浪潮席卷全球的时代&#xff0c;数字技术服务平台应运而生&#xff0c;成为推动各行业发展的强大引擎。数字技术服务平台是一个汇聚了众多先进数字技术和资源的综合性服务体系。它就像是一个功能强大的百宝箱&#xff0c;为用户提供了全方位的数字技术支持。 在这…...

RabbitMQ下载安装运行环境搭建

RabbitMQ运行环境搭建 1、Erlang及RabbitMQ安装版本的选择2、下载安装Erlang2.1、下载Erlang2.2、安装Erlang2.2.1、安装Erlang前先安装Linux依赖库2.2.2、解压Erlang压缩包文件2.2.3、配置2.2.4、编译2.2.5、安装2.2.6、验证erlang是否安装成功 3、RabbitMQ下载安装3.1、下载3…...

Redis过期时间删除策略详解

文章目录 Redis过期时间删除策略详解一、引言二、Redis过期键删除策略1、定时删除2、惰性删除3、定期删除 三、Redis实际采用的策略1、惰性删除Java 伪代码 2、定期删除Java 伪代码 四、总结 Redis过期时间删除策略详解 一、引言 在许多应用程序中&#xff0c;我们经常需要缓…...

mysql数据库的基本管理

目录 一.数据库的介绍 二.mariadb的安装 三.软件基本信息 四.数据库开启 五.数据库的安全初始化 六.数据库的基本管理 七.数据密码管理 八.用户授权 九.数据库的备份 十.web控制器 一.数据库的介绍 1.什么是数据库 数据库就是个高级的表格软件 2.常见数据库 Mysql Oracl…...

根据现有html里的元素上面动态创建el-tooltip组件并显示的几种方式

1、 在这个示例中&#xff0c;我们创建了一个 ref 引用来指向我们想要附加 Tooltip 的 DOM 元素。然后在 onMounted 生命周期钩子中&#xff0c;我们创建了 Tooltip 组件的实例&#xff0c;并将其挂载到一个新创建的 DOM 元素上。我们还在触发元素上添加了 mouseenter 和 mouse…...

【C++篇】迈入新世界的大门——初识C++(上篇)

文章目录 前言 下篇已出&#xff1a;【C篇】迈入新世界的大门——初识C(下篇&#xff09; C发展历史 C起源 与C语言一样&#xff0c;C也是在贝尔实验室诞生的&#xff0c;Bjarne Stroustrup于20世纪80年代在这里开发出了这种语言。Stroustrup比较关系的是让C更有用&#xff0…...

啥?Bing搜索古早BUG至今未改?

首先&#xff0c;大家先看下面的一个数学公式。 Γ ( z ) ∫ 0 ∞ t z − 1 e − t d t . \Gamma(z) \int_0^\infty t^{z-1}e^{-t}dt\,. Γ(z)∫0∞​tz−1e−tdt. 看不懂&#xff1f;没关系&#xff0c;因为我也看不懂 这不是谈论的重点。 当你把鼠标光标移到公式的最开头&…...

ValueError: Out of range float values are not JSON compliant

可能原因一 可能原因二 数据里面有NaN...

【架构】NewSQL

文章目录 NewSQLTiDBTiDB 主要组件特点使用场景安装与部署 推荐阅读 NewSQL NewSQL是一种数据库管理系统(DBMS)的类别&#xff0c;它结合了NoSQL数据库的可扩展性和传统SQL数据库的事务一致性。具体来说&#xff0c;NewSQL数据库旨在解决传统关系型数据库在处理大规模并发事务…...

禁止吸烟监测系统 基于图像处理的吸烟检测系统 YOLOv7

吸烟是引发火灾的重要原因之一。烟头在未熄灭的情况下&#xff0c;其表面温度可达200℃-300℃&#xff0c;中心温度甚至能高达700℃-800℃。在易燃、易爆的生产环境中&#xff0c;如化工厂、加油站、仓库等&#xff0c;一个小小的烟头就可能引发灾难性的火灾&#xff0c;造成巨…...

《中国工程科学》

《中国工程科学》为工程科技战略咨询学术期刊&#xff0c;主要发布我国工程科技战略咨询研究成果&#xff0c;以及工程科技各领域前瞻性综合研究成果&#xff0c;为政府科学决策提供参谋、为行业科学发展提供指导、为相关学术研究提供参考。 一、2024年度征稿主题 本刊以专题…...

碳钢液动紧急切断阀QDY421F-16C DN200

在深入探讨碳钢液动紧急切断阀QDY421F-16C DN200的卓越性能与应用场景时&#xff0c;不得不提及其在化工、石油、天然气等高危行业中的核心地位。这款阀门以其高度的自动化控制能力和快速响应机制&#xff0c;成为了保障生产安全、防止介质泄漏的关键防线。 其内置的液动执行机…...

【C++】红黑树的封装——同时实现map和set

目录 红黑树的完善默认成员函数迭代器的增加 红黑树的封装红黑树模板参数的控制仿函数解决取K问题对Key的非法操作 insert的调整map的[]运算符重载 在list模拟实现一文中&#xff0c;介绍了如何使用同一份代码封装出list的普通迭代器和const迭代器。今天学习STL中两个关联式容器…...

Tableau|一入门

一 什么是BI工具 BI 工具即商业智能&#xff08;Business Intelligence&#xff09;工具&#xff0c;是一种用于收集、整理、分析和展示企业数据的软件系统&#xff0c;其主要目的是帮助企业用户更好地理解和利用数据&#xff0c;以支持决策制定。 主要功能&#xff1a; 1.数据…...

Android 12系统源码_输入系统(三)输入事件的加工和分发

前言 上一篇文章我们具体分析了InputManagerService的构造方法和start方法&#xff0c;知道IMS的start方法经过层层调用&#xff0c;最终会触发Navite层InputDispatcher的start方法和InputReader的start方法。InputDispatcher的start方法会启动一个名为InputDispatcher的线程&…...

【Elasticsearch系列廿二】特殊参数

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...

Java笔试面试题AI答之设计模式(2)

文章目录 6. 什么是单例模式&#xff0c;以及他解决的问题&#xff0c;应用的环境 &#xff1f;解决的问题应用的环境实现方式 7. 什么是工厂模式&#xff0c;以及他解决的问题&#xff0c;应用的环境 &#xff1f;工厂模式简述工厂模式解决的问题工厂模式的应用环境工厂模式的…...

54 循环神经网络RNN_by《李沐:动手学深度学习v2》pytorch版

系列文章目录 文章目录 系列文章目录循环神经网络使用循环神经网络的语言模型困惑度&#xff08;perplexity&#xff09;梯度剪裁 循环神经网络 使用循环神经网络的语言模型 输入“你”&#xff0c;更新隐变量&#xff0c;输出“好”。 困惑度&#xff08;perplexity&#xff…...

数据仓库-数据质量规范

一、 数据质量系统概述 1.1 数据质量管理系统1.2 数据质量建设流程1.3 数据质量标准二、 数据质量管理规则 2.1 数据校验规则列表 2.1.1 数据量2.1.2 数据量对比2.1.3 空值检查2.1.4 值域检查2.1.5 规范检查2.1.6 逻辑检查2.1.7 重复数据检查2.1.8 及时性检查...

PostgreSQL 17 发布了!非常稳定的版本

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 作者&#xff1a;IT邦德 中国DBA联盟(ACDU)成员&#xff0c;10余年DBA工作经验&#xff0c; Oracle、PostgreSQL ACE CSDN博客专家及B站知名UP主&#xff0c;全网粉丝10万 擅长主流Oracle、My…...

【Python】执行脚本的时,如何指定运行根目录,而不是指定脚本的父级目录

author: jwensh & gpt date: 2024.09.23 python 执行脚本的时&#xff0c;如何指定运行根目录&#xff0c;而不是指定脚本的父级目录 prompt&#xff1a;python 执行脚本的时候&#xff0c;如何指定他的运行根目录&#xff0c;而不是指定脚本的父级目录 在执行 Python 脚…...

JVM(HotSpot):程序计数器(Program Counter Register)

文章目录 一、内存结构图二、案例解读三、工作流程四、特点 一、内存结构图 二、案例解读 我们使用javap对字节码进行反编译&#xff0c;来看下程序计数器怎么体现的。 IDEA写一个简单的Java代码 反编译命令 javap -verbose InitTest.class $ javap -verbose InitTest.clas…...

等保托管怎么样,流程是什么样的?

随着信息技术的快速发展&#xff0c;网络安全问题愈发凸显。为了保护信息系统的安全&#xff0c;国家推出了网络安全等级保护制度&#xff08;简称“等保”&#xff09;&#xff0c;企业在面对这一制度的同时&#xff0c;也逐渐意识到等保托管的重要性。等保托管旨在通过专业的…...

【HTML】img标签和超链接标签

文章目录 img 标签src 属性alt 属性title 属性width/height 属性border 属性 超链接标签&#xff1a;a表格标签合并单元格 img 标签 img 是一个单标签 src 属性 img 标签必须搭配 src 使用&#xff08;指定图片的路径&#xff09; 相对路径&#xff1a; ./xxx.png./img/xxx.…...

智能PPT行业赋能用户画像

智能PPT市场在巨大的需求前景下&#xff0c;已吸引一批不同类型的玩家投入参与竞争。从参与玩家类型来看&#xff0c;不乏各类与PPT创作有关的上下游企业逐步向智能PPT赛道转型进入&#xff0c;也包括顺应生成式AI技术热潮所推出的创业企业玩家。当前&#xff0c;智能PPT赛道发…...

学习C++的第七天!

1.虚函数是在基类中用 virtual 关键字声明的函数&#xff0c;可以在派生类中被重写。纯虚函数是在虚函数的基础上&#xff0c;在基类中被初始化为 0 的函数&#xff0c;含有纯虚函数的类是抽象类&#xff0c;不能被实例化。 2.如果基类的析构函数不是虚函数&#xff0c;当通过…...

Java编程必备:五大高效工具与框架

作为一位Java程序员&#xff0c;在编写Java代码时&#xff0c;通常会使用多种工具和框架来提高开发效率、保证代码质量并简化开发流程。以下是五个常用的Java程序员工具和框架及其简要说明&#xff1a; 1. IntelliJ IDEA 主要功能&#xff1a;IntelliJ IDEA是一个强大的Java集…...

现代桌面UI框架科普及WPF入门1

现代桌面UI框架科普及WPF入门 文章目录 现代桌面UI框架科普及WPF入门桌面应用程序框架介绍过时的UI框架MFC (Microsoft Foundation Class)缺点 经典的UI框架**WinForms****QT****WPF** 未来的UI框架**MAUI****AvaloniaUI** WPF相对于Winform&#xff0c;QT&#xff0c;MFC的独立…...

in和like性能对比

场景: 有个问题表,有个渠道表,问题和渠道的关系是一对多 需要根据渠道查询问题,暂时两种思路 1:问题表荣誉渠道id,多个id拼接 2:设计问题和渠道关联关系表 首先,这两种是常用的设计思路,那么查询谁的速度快 问题表:造10w数据,渠道表造100条数据 结论 实测10次后,发现like耗…...