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

性能优化-如何爽玩多线程来开发

前言

多线程大家肯定都不陌生,理论滚瓜烂熟,八股天花乱坠,但是大家有多少在代码中实践过呢?很多人在实际开发中可能就用用@Async,new Thread()。线程池也很少有人会自己去建,默认的随便用用。在工作中大家对于多线程开发,大多是用在异步,比如发消息,但是对于提效这块最重要的优势却很少有人涉及。因此本篇文章会结合我自己的工作场景带大家去发掘项目中的多线程场景,让你的代码快如闪电。
最后会有我对自己三个多月来写的十篇原创文章的个人总结,希望读者大大能耐心看完本篇,一定会有所收获。

多线程普及

多线程解决了什么问题?带来了什么问题?

Cpu为了均衡与内存的速度差异,增加了缓存–导致了可见性问题
操作系统增加了进程和线程,分时复用CPU,进而均衡CPU与IO设备的速度差异–导致了原子性问题
编译程序优化指令排序(JVM指令重排序)–导致了有序性问题

可见性问题–线程A修改共享变量,修改后CPU缓存中的数据没有及时同步到内存,线程B读取了内存中老数据
原子性问题–多个线程增加数据,有几个线程挂了,这数据就少了
有序性问题–经典的对象创建三步,堆中分配内存–>初始化–>变量指向内存地址,如果排序重排会出现132,导致没有初始化的对象被创建

JVM提供了什么工具去解决线程不安全问题?Java代码有哪些实现思路?

JVM提供了三个关键词,synchronizedvolatilefinalJMM(线程操作内存规范,例如8个happen before原则)
Java代码实践可从三方面入手

  • 同步:synchronized和ReentrantLock
  • 非同步:CAS(CPU原语,依赖硬件)
  • 线程安全:局部变量(虚拟机栈或者本地方法栈,线程私有)和ThreadLocal(本地线程变量副本,空间换安全,每个线程一份)

如何开启线程?

基础的Thread、runable、callable,进阶的ThreadExecutor和Future,以及JDK8的终极武器CompletableFuture

线程间如何协作?

基础

  1. volatile和synchronized关键字
    1. volatile关键字用来修饰共享变量,保证了共享变量的可见性,任何线程需要读取时都要到内存中读取(确保获得最新值)。
    2. synchronized关键字确保只能同时有一个线程访问方法或者变量,保证了线程访问的可见性和排他性。
  2. 等待/通知机制–指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B 调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而 执行后续操作
  3. 管道输入/输出流
    1. 管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要 用于线程之间的数据传输,而传输的媒介为内存。
    2. 管道输入/输出流主要包括了如下4种具体实现:PipedOutputStream、PipedInputStream、 PipedReader和PipedWriter,前两种面向字节,而后两种面向字符。
  4. join()方法–如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才 从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(long millis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时 时间里没有终止,那么将会从该超时方法中返回。
  5. ThreadLocal–即线程本地变量(每个线程都有自己唯一的一个哦),是一个以ThreadLocal对象为键、任意对象为值的存储结构。底层是一个ThreadLocalMap来存储信息,key是弱引用,value是强引用,所以使用完毕后要及时清理(尤其使用线程池时)。

进阶
有JDK5开始提供的Semaphore(信号量)、CyclicBarrier、CountDownLatch以及JDK8的CompletableFuture

场景实战

多线程处理场景

并行聚合处理数据

以下案例主要运用CompletableFuture.allOf()方法,将原本串行的操作改为并行。本案例相对比较常规,算是CompletableFuture的基本操作,其他特性就不一一介绍了。

AtomicReference<List<SellOrderList>> orderLists = new AtomicReference<>();
AtomicReference<List<InternationalSalesList>> salesLists = new AtomicReference<>();
AtomicReference<Map<String, BigDecimal>> productMap = new AtomicReference<>();
.........
//逻辑A
CompletableFuture<Void> orderListCom =CompletableFuture.runAsync(() -> {orderLists.set(sellOrderListService.lambdaQuery().ge(SellOrderList::getOrderCreateDate, startDate).le(SellOrderList::getOrderCreateDate, endDate).eq(SellOrderList::getIsDelete, 0).list());});
CompletableFuture<Void> productCom = CompletableFuture.runAsync(() -> {//逻辑B});
CompletableFuture<Void> euLineCom = CompletableFuture.runAsync(() -> {//逻辑C});
//汇总线程操作
CompletableFuture.allOf(orderListCom, productCom, euCloudCom).handle((res, e) -> {if (e != null) {log.error("客户订单定时任务聚合数据异常", e);} else {try {//获取全部数据后处理数据aggregateData(customerList, saleMonth, orderLists, salesLists, productMap, euLineList, asLineList,euCloudList, asCloudList, itemMap, deliveryMap, parities);} catch (Exception ex) {log.error("客户订单处理数据异常", ex);}}return null;
});

修改for循环为并行操作

这里借鉴了parallelStream流的思路,将串行的for循环分割成多个集合后,对分割后的集合进行循环。这应该是最普遍的多线程应用场景了,需要注意的是线程池需要自定义大小、不安全的集合例如ArrayList并行add时需要加锁,加好日志就完事了。

@Autowired
@Qualifier("ioDenseExecutor")
private ThreadPoolTaskExecutor ioDense;
//自建线程池,ForkJoinPool默认的太小,一般是逻辑CPU数量-1
int logicCpus = Runtime.getRuntime().availableProcessors();
ForkJoinPool forkJoinPool = new ForkJoinPool(logicCpus * 80);
//指定集合大小,避免频繁扩容
List<RedundantErpSl> slAddList = new ArrayList<>(50000);
//谷歌提供工具类切分集合--import com.google.common.collect.Lists; 
List<List<SlErpDTO>> partition = Lists.partition(slErpList, 1000);
int finalLastStatus = lastStatus;
CompletableFuture<Void> handle = CompletableFuture.allOf(partition.stream().map(addPartitionList ->CompletableFuture.runAsync(() -> {for (SlErpDTO slErp : addPartitionList) {//TODO 逻辑处理synchronized (slAddList) {//ArrayList线程不安全,多线程会出现数据覆盖,体现为数据丢失slAddList.add(sl);}}}, ioDense)).toArray(CompletableFuture[]::new)).whenComplete((res, e) -> {if (e != null) {log.error("多线程组装数据失败", e);} else {try {//进一步处理循环后的结果slService.batchSchedule(versionNum, slAddList);} catch (Exception ex) {log.error("批量插入失败", ex);}}});
handle.join();

List转Map解除嵌套循环

一个常见的场景,当我们需要判断当前数据的唯一值是否在数据库存在从而判断是新增还是修改时,常规做法是:

for(X x:待新增或者修改的数据){String xKey=x.getKey();//key为当前数据的唯一值for(Y y:数据库中的数据){if(Objects.equals(xKey,yKey)){update;//相等就修改        }else{add;//不等就新增        }} 
} 

循环套循环,时间复杂度O(n²),性能肯定是比较差的,那怎么改造呢?如果能确定key是唯一值,就拿优化神器Map来操作,算法里面常见的优化手段。我们把唯一值key作为键,输出对象作为值那么上面的代码就可以改造为:

public Map<String, SafeRule> list2Map() {List<SafeRule> ruleList = lambdaQuery().eq(SafeRule::getIsDelete, 0).list();return ruleList.stream().collect(Collectors.toMap(e -> e.getBigClass() + e.getSmallClass(),
Function.identity()));
}
for(X x:待新增或者修改的数据){String xKey=x.getKey();//key为当前数据的唯一值Map<String, SafeRule> map=list2Map();if(map.get(xKey)!=null){update;//相等就修改        }else{add;//不等就新增        }} 
} 

引入Map之后,第二个嵌套的循环就干掉了,换成了单次循环来组装Map,时间复杂度降为O(n)。此方法仅适用于每行数据拥有唯一值,不然stream在组装时会提示重复key。
修改Map遍历为并行操作
既然for循环能转换,那么map遍历必然也能通过多线程改造。

/*** 将map切成段--工具类** @param splitMap 被切段的map* @param splitNum 每段的大小*/
public static <k, v> List<Map<k, v>> mapSplit(Map<k, v> splitMap, int splitNum) {if (splitMap == null || splitNum <= 0) {List<Map<k, v>> list = new ArrayList<>();list.add(splitMap);return list;}Set<k> keySet = splitMap.keySet();Iterator<k> iterator = keySet.iterator();int i = 1;List<Map<k, v>> total = new ArrayList<>();Map<k, v> tem = new HashMap<>();while (iterator.hasNext()) {k next = iterator.next();tem.put(next, splitMap.get(next));if (i == splitNum) {total.add(tem);tem = new HashMap<>();i = 0;}i++;}if (!CollectionUtils.isEmpty(tem)) {total.add(tem);}return total;
}
//代码示例
Map<String, List<BudgetErpDTO>> materialMap = materialList.parallelStream().collect(Collectors.groupingBy(e -> e.getInvOrgId() + "-" + e.getItemId() + "-" + e.getVendorId()));List<Map<String, List<BudgetErpDTO>>> mapList = MapUtil.mapSplit(materialMap, 50);CompletableFuture<Void> handle =CompletableFuture.allOf(mapList.stream().map(splitMap -> CompletableFuture.runAsync(() -> {splitMap.forEach((identity, list) -> {//业务操作});}, ioDense)).toArray(CompletableFuture[]::new)).exceptionally(e -> {log.error("多线程组装数据失败", e);return null;});

上面提供了一个切分的工具类,以及Map改造的代码,总体还是非常简单,思路和for循环的改造是差不多的

多线程新增

我个人在开发中会使用一些小工具来提高开发效率,接下来公开一个我常用的批量插入的小工具,这个小工具最开始是同事给我的,然后我做了优化和扩充,主要是扩充了多线程以及service块的代码。

总览

该工具类用于生成复制可用的代码,这里需要提前指定一些固定变量。除了entity和serviceName需要根据实际情况变化之外,方法名和参数名可以不变。生成了四个方法,分别是mapper类中的方法、mapper.xml中的foreach批量插入代码、普通无事务的多线程批量插入代码、多线程事务代码

//批量方法名,对应mapper和xml中id
String methodName = "batchSchedule";
//mapper参数名称
String paramName = "addList";
//实际代码里面的service命名
String serviceName = "baseInfoService";
Class<?> entity = BudgetBase.class;
//批量插入
printMapper(entity.getSimpleName(), methodName, paramName);
printXml(entity, methodName, paramName);
//普通多线程批量插入,无事务
printSave(entity.getSimpleName(), serviceName, paramName, 1000);
//多线程事务,慎用
printAddTransaction(entity.getSimpleName(), paramName, 1000);

mapper方法

xml批量插入语句

多线程批量插入

这个多线程插入其实就是我上面多线程处理场景中for循环改造的变种,将集合拆分进行并行批量插入

if (CollectionUtils.isNotEmpty(addList)) {List<List<BudgetBase>> partition = Lists.partition(addList, 1000);CompletableFuture.allOf(partition.stream().map(addPartitionList ->CompletableFuture.runAsync(() -> baseInfoService.getBaseMapper().batchSchedule(addPartitionList))).toArray(CompletableFuture[]::new)).exceptionally(e -> {log.error("多线程处理异常", e);return null;});}

花里胡哨-多线程事务提交

这个才是本文的重点,接下来我会详细介绍我在开发中遇到的坑和知识点,敲黑板了啊,重点来了!
我写的这个多线程事务本质就是根据2PC理论手写了一个分布式事务,涉及到多线程、Spring事务、ThreadLocal、LockSupport这些知识点,在线上一定要慎重使用,最好不用,可作炫技用,秀就完了。

深刻理解Spring事务、ThreadLocal

从头说起,既然是多线程事务,那自然不能使用注解@Transactional去开启事务,Spring事务采用ThreadLocal来做线程隔离,ThreadLocalMap内部key为当前线程的ThreadLocal对象,也可以当作以当前线程为key,value也是个map,看源码可以知道,map里面key为数据源,value为数据库连接。

当然上来看源码,肯定认识不够深刻,接下来是一段错误代码示范,充分展示了理解上面那段话的重要性。我的第一次失败就是如下一段代码,首先肯定是能运行的,不能运行的例子我就不给了,先来看看这段代码。

//存储事务集合
List<TransactionStatus> traStatusList = new ArrayList<>();
//最外部更新或者删除时手动创建一个新事务
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
TransactionStatus statusStart = transactionManager.getTransaction(def);
traStatusList.add(statusStart);
//外部DML操作
lambdaUpdate().set(RedundantErpSl::getIsDelete, 1).set(RedundantErpSl::getUpdateTime, new Date()).eq(RedundantErpSl::getVersionNum, versionNumber).eq(RedundantErpSl::getIsDelete, 0).update();
List<List<RedundantErpSl>> partition = Lists.partition(RedundantErpSlList, 1000);
try {CompletableFuture<Void> future = CompletableFuture.allOf(partition.stream().map(addPartitionList ->CompletableFuture.runAsync(() -> {//Spring事务内部由ThreadLocal存储事务绑定信息,因此需要每个线程新开一个事务DefaultTransactionDefinition defGo = new DefaultTransactionDefinition();defGo.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);defGo.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);TransactionStatus statusGo = transactionManager.getTransaction(defGo);//ArrayList线程不安全,多线程会出现数据覆盖,体现为数据丢失synchronized (traStatusList) {traStatusList.add(statusGo);}getBaseMapper().batchSchedule(addPartitionList);})).toArray(CompletableFuture[]::new)).exceptionally(e -> {log.error("批量导入出现异常", e);//向外抛出异常,确保最外面catch回滚throw new PtmException(e.getMessage());});future.join();for (TransactionStatus status : traStatusList) {transactionManager.commit(status);}
} catch (Exception e) {log.error("批量导入出现异常回滚开始", e);for (TransactionStatus status : traStatusList) {transactionManager.rollback(status);}
}

先说说这个错误例子我当时开发的思路,手动开启事务后,在每个线程操作开始的时候都创建一个事务,Spring事务传播级别用的TransactionDefinition.PROPAGATION_REQUIRES_NEW,即默认创建新事务。隔离级别一开始没改,然后我就尝试着操作了一下,好家伙批量新增的时候直接锁了。
查看正在锁的事务 SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;

查看等待锁的事务 SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;

异常如下图,锁超时异常

第一次看见下图这个错的时候,我是疑惑的,没有当回事,以为是多数据源的问题。我项目里有直连Oracle和MySQL两种关系型数据库,当时怀疑是多数据源事务没有正确解绑导致的问题。
:::info
PS:事实上这个坑给足了我提示,根本原因就是多线程事务解绑失败,但是我理解出现了偏差,为后文埋下了伏笔。
:::

我当时一看有锁,这我能惯着,马上修改事务隔离级别为TransactionDefinition.ISOLATION_READ_COMMITTED读已提交(MySQL默认事务隔离级别为可重复读,这里下调了一级,总共四级)。顺利插入但还是报上面这个错,错误位置是下面这个循环提交时报的,第二次循环的时候一定会报错。

for (TransactionStatus status : traStatusList) {transactionManager.commit(status);
}

当时一度以为是多数据源的问题,但是Debug后发现resource里面只有一个数据源key,解绑一次后就没了,第二个循环解绑的时候就报上面这个错,因为找不到可以解绑的key了。我就很疑惑,为啥就一个数据源key,我不是在别的线程开了事务嘛,按理说开了多少个线程就有多少个事务,这个问题困扰了我大概一天左右的时间。然后我想到了Spring事务的实现原理ThreadLocal,然后联想到我的多线程开启事务,再看到我在主线程里面进行傻叉循环解绑,我瞬间为梦想窒息。
所以破案了,我在主线程是操作不了子线程事务,这也是代码报key找不到的原因,因为用主线程做key在ThreadLocal里肯定是拿不到子线程信息的,只能拿到主线程自己的。

多线程事务提交方案

因此解决方案就很简单,子线程的事务自己操作,那么多线程事务处理哪家强,JDK里找CompletableFuture!当然这里使用CountDownLatch也是可行的,网上也有案例。多线程事务在处理逻辑上其实和分布式事务很像,因此我这里采用2PC的思想,一阶段所有子线程全部开启事务并执行SQL,然后阻塞等待,二阶段判断是否全部成功,是就唤醒所有线程提交事务,否就全部回滚。

-----------需要注入Bean,一个是Spring Boot事务管理,一个是线程池-----------
@Autowired
private PlatformTransactionManager transactionManager;
@Autowired
@Qualifier("ioDenseExecutor")
private ThreadPoolTaskExecutor ioDense;
-----------多线程事务新增操作-----------
private void batchSchedule(List<BudgetBase> addList) {if (!CollectionUtils.isEmpty(addList)) {//定义局部变量,是否成功、顺序标识、等待线程队列AtomicBoolean isSuccess = new AtomicBoolean(true);AtomicInteger cur = new AtomicInteger(1);List<Thread> unfinishedList = new ArrayList<>();//切分新增集合List<List<BudgetBase>> partition = Lists.partition(addList, 1000);int totalSize = partition.size();//多线程处理开始CompletableFuture<Void> future =CompletableFuture.allOf(partition.stream().map(addPartitionList -> CompletableFuture.runAsync(() -> {//Spring事务内部由ThreadLocal存储事务绑定信息,因此需要每个线程新开一个事务DefaultTransactionDefinition defGo = new DefaultTransactionDefinition();defGo.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);TransactionStatus statusGo = transactionManager.getTransaction(defGo);int curInt = cur.getAndIncrement();try {log.info("当前是第{}个线程开始启动,线程名={}", curInt, Thread.currentThread().getName());baseInfoService.getBaseMapper().batchSchedule(addPartitionList);log.info("当前是第{}个线程完成批量插入,开始加入等待队列,线程名={}", curInt, Thread.currentThread().getName());//ArrayList线程不安全,多线程会出现数据覆盖,体现为数据丢失synchronized (unfinishedList) {unfinishedList.add(Thread.currentThread());}log.info("当前是第{}个线程已加入队列,开始休眠,线程名={}", curInt, Thread.currentThread().getName());notifyAllThread(unfinishedList, totalSize, false);LockSupport.park();if (isSuccess.get()) {log.info("当前是第{}个线程提交,线程名={}", curInt, Thread.currentThread().getName());transactionManager.commit(statusGo);} else {log.info("当前是第{}个线程回滚,线程名={}", curInt, Thread.currentThread().getName());transactionManager.rollback(statusGo);}} catch (Exception e) {log.error("当前是第{}个线程出现异常,线程名={}", curInt, Thread.currentThread().getName(), e);transactionManager.rollback(statusGo);isSuccess.set(false);notifyAllThread(unfinishedList, totalSize, true);}}, ioDense)).toArray(CompletableFuture[]::new));future.join();}}
private void notifyAllThread(List<Thread> unfinishedList, int totalSize, boolean isForce) {if (isForce || unfinishedList.size() >= totalSize) {log.info("唤醒当前所有休眠线程,线程数={},总线程数={},是否强制={}", unfinishedList.size(), totalSize, isForce);for (Thread thread : unfinishedList) {log.info("当前线程={}被唤醒", thread.getName());LockSupport.unpark(thread);}}}

方案详解

为什么用LockSupport的park()和unpark()而不用Thread.sleep()、Object.wait()、Condition.await()?

  1. 更简单,不需要获取锁,能直接阻塞线程。
  2. 更直观,以thread为操作对象更符合阻塞线程的直观定义;
  3. 更精确,可以准确地唤醒某一个线程(notify随机唤醒一个线程,notifyAll唤醒所有等待的线程);
  4. 更灵活 ,unpark方法可以在park方法前调用

第4点很重要,如果不能提前使用unpark()的话,按照代码逻辑最后一个线程会被永久阻塞。

为什么要自建线程池?

CompletableFuture默认的线程池ForkJoinPool.commonPool()偏向于计算密集型任务处理,核心线程数和逻辑CPU数少1,对于多线程事务这种IO密集型任务来说核心线程数偏少。并且上述方法在操作中都是阻塞线程,无法一次性开启全部线程的话,会导致notifyAllThread方法无法执行,老线程阻塞新线程无法开启,就尬住了。
ForkJoinPool基于工作窃取算法,所以最适合的是计算密集型任务,这里我们开启一个参数调整为IO密集型(多核心少队列)的ThreadPoolTaskExecutor线程池即可。

注意MySQL/Druid等数据库的最大连接数

使用多线程的时候也别忘了调整其他组件的最大连接数。Druid线程池这个代码配置可以调,MySQL5.7默认151得用配置文件调整。MySQL最大连接数调整的方法之前从零开始的SQL修炼手册-实战篇有讲解过,欢迎读者们翻翻我之前写的干货。

真实的批量提交

Mybatis与JDBC批量插入MySQL数据库性能测试及解决方案
先贴一个大佬的文章,里面大概讲了Mybatis批量插入和JDBC批量+手动事务的优劣,结论就是小于1W用Mybatis,小于10W且大于1W用JDBC,大于10W必须数据分批。当然我上面多线程做的分批导入肯定是不安全的操作,如果要一次性导入的话根据数据量判断用大佬文章中的代码案例即可。

多线程事务提交方案–使用须知


如果此时数据库连接池配置较小,比如spring.datasource.druid.max-active=10(Druid配置最大连接数为10)。但是我在使用多线程提交时,分批次数为20,那么开了10个之后达到上线就会一直卡住,原因是老的线程挂起不会释放,新的线程因为线程池满了无法创建。因此在使用该方案时一定要估算数据量,分好合适的大小,连接池和数据库的最大连接数也要注意是否匹配。

相关文章:

性能优化-如何爽玩多线程来开发

前言 多线程大家肯定都不陌生&#xff0c;理论滚瓜烂熟&#xff0c;八股天花乱坠&#xff0c;但是大家有多少在代码中实践过呢&#xff1f;很多人在实际开发中可能就用用Async&#xff0c;new Thread()。线程池也很少有人会自己去建&#xff0c;默认的随便用用。在工作中大家对…...

非关系型数据库-----------Redis的主从复制、哨兵模式

目录 一、redis群集有三种模式 1.1主从复制、哨兵、集群的区别 1.1.1主从复制 1.1.2哨兵 1.1.3集群 二、主从复制 2.1主从复制概述 2.2主从复制的作用 ①数据冗余 ②故障恢复 ③负载均衡 ④高可用基石 2.3主从复制流程 2.4搭建redis主从复制 2.4.1环境准备 2.4…...

使用docx4j转换word为pdf处理中文乱码问题

word转pdf 实现方法 mavendocx4j版本自己酌情升级 实现方法 import org.docx4j.Docx4J; import org.docx4j.fonts.IdentityPlusMapper; import org.docx4j.fonts.Mapper; import org.docx4j.fonts.PhysicalFonts; import org.docx4j.openpackaging.packages.WordprocessingMLP…...

【引子】C++从介绍到HelloWorld

C从介绍到HelloWorld 一、C的介绍1. 简介2. 应用场景3. C的标准![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/e3efb0f207f647729b92c0b5bcd4b330.png)4. C的运行过程 二、Visual Studio的安装1. 什么是Visual Studio2. Visual Studio的安装 三、完成HelloWorld1.…...

Django检测到会话cookie中缺少HttpOnly属性手工复现

一、漏洞复现 会话cookie中缺少HttpOnly属性会导致攻击者可以通过程序(JS脚本等)获取到用户的cookie信息&#xff0c;造成用户cookie信息泄露&#xff0c;增加攻击者的跨站脚本攻击威胁。 第一步&#xff1a;复制URL&#xff1a;http://192.168.43.219在浏览器打开&#xff0c;…...

2024数字城市建设博览会:一站式平台,满足多元需求

2024数字城市建设博览会&#xff1a;引领未来城市发展的风向标 2024年&#xff0c;一场前所未有的盛会——数字城市建设博览会暨交流大会&#xff0c;将在雄安这座未来之城拉开帷幕。本次大会不仅是数字经济全产业链的精英集结&#xff0c;更是一场汇聚了众多优质项目和丰富客…...

iOS 17.5系统或可识别并禁用未知跟踪器,苹果Find My技术应用越来越合理

苹果公司去年与谷歌合作&#xff0c;宣布将制定新的行业标准来解决人们日益关注的跟踪器隐私问题。苹果计划在即将发布的 iOS 17.5 系统中加入这项提升用户隐私保护的新功能。 科技网站 9to5Mac 在苹果发布的 iOS 17.5 开发者测试版内部代码中发现了这项反跟踪功能的蛛丝马迹…...

关于搭建elk日志平台

我这边是使用docker compose进行的搭建 所以在使用的时候 需要自行提前安装docker以及dockercompose环境 或者从官网下载对应安装包也可以 具体文章看下一章节&#xff1a;【ELK】搭建elk日志平台&#xff08;使用docker-compose&#xff09;&#xff0c;并接入springboot项目...

【全套源码教程】基于SpringBoot+MyBatis+Vue的流浪动物救助网站的设计与实现

目录 前言 需求分析 可行性分析 技术实现 后端框架&#xff1a;Spring Boot 持久层框架&#xff1a;MyBatis 前端框架&#xff1a;Vue.js 数据库&#xff1a;MySQL 功能介绍 前台界面功能介绍 动物领养及捐赠 宠物论坛 公告信息 商品页面 寻宠服务 个人中心 购…...

Word wrap在计算机代表的含义(自动换行)

“Word wrap”是一个计算机术语&#xff0c;用于描述文本处理器在内容超过容器边界时自动将超出部分转移到下一行的功能。在多种编程语言和文本编辑工具中&#xff0c;都有实现这一功能的函数或选项。 在编程中&#xff0c;例如某些编程语言中的wordwrap函数&#xff0c;能够按…...

室友打团太吵?一条命令让它卡死

「作者主页」&#xff1a;士别三日wyx 「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;更多干货&#xff0c;请关注专栏《网络安全自学教程》 SYN Flood 1、hping3实现SYN Flood1.1、主机探测1.2、扫描端…...

RabbitMQ3.13.x之八_RabbitMQ中数据文件和目录位置

RabbitMQ3.13.x之_RabbitMQ中数据文件和目录位置 文章目录 RabbitMQ3.13.x之_RabbitMQ中数据文件和目录位置1. 概述2. 覆盖位置1. 路径和目录名称限制2.所需的文件和目录权限 3. 环境变量4. Linux、macOS、BSD上的默认位置5. Windows上的默认位置6. 通用二进制构建默认值 1. 概…...

仿抖音短视频直播带货刷一刷商城社交电商源码系统小程序APP开发

系统功能介绍 一、短视频与社交功能 短视频浏览与互动 用户可以浏览仿抖音风格的短视频&#xff0c;包括评论、点赞、进入视频发布者的主页&#xff0c;以及加关注等功能。系统会显示用户关注的好友列表&#xff0c;方便用户快速查看好友发布的视频。用户还可以浏览同城视频&…...

Vue - 你知道Vue组件中的data为什么是一个函数吗

难度级别:中高级及以上 提问概率:80% 在Vue项目中,App.vue下的每个子组件都会生成一个单独的Vue实例对象,但这些子对象都是通过通过vue.extend方法创建而来的,也就是说我们平时在项目中所定义的Vue组件,都有一个相同的父类对象。这样也就…...

MUX VLAN

目录 原理概述 实验目的 实验内容 实验拓扑 1.基本配置 2.使用Hybrid端口实现网络需求 3.使用Mux VLAN实现网络需求 原理概述 在实际的企业网络环境中&#xff0c;往往需要所有的终端用户都能够访问某些特定的服务器&#xff0c;而用户之间的访问控制规则则比较复杂。在…...

漫谈:“标准”是一种幻觉 C++语言标准的意义

初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github&#xff1a;codetoys&#xff0c;所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的&#xff0c;可以在任何平台上使用。 “标准”这个词很迷惑&#xf…...

【Wbpack原理】基础流程解析,实现 mini-webpack

【Wbpack原理】基础流程解析&#xff0c;实现 mini-webpack ⛄&#xff1a;webpack 对前端同学来说并不陌生&#xff0c;它是我们学习前端工程化的第一站&#xff0c;在最开始的 vue-cli 中我们就可以发现它的身影。我们的 vue/react 项目是如何打包成 js 文件并在浏览器中运行…...

Debian 安装 python 3.9.6

安装相关依赖 sudo apt update sudo apt install build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libsqlite3-dev libreadline-dev libffi-dev curl libbz2-dev 下载Python 源码 wget https://mirrors.aliyun.com/python-release/source/Py…...

搜索二维矩阵2 合并两个有序链表

240. 搜索二维矩阵 II - 力扣&#xff08;LeetCode&#xff09; class Solution { public:bool searchMatrix(vector<vector<int>>& matrix, int target) {int i matrix.size() - 1, j 0;while(i > 0 && j < matrix[0].size()){if(matrix[i][j…...

深入Tauri开发——从环境搭建到项目构建

深入Tauri开发——从环境搭建到项目构建 开启你的Tauri桌面应用开发之旅&#xff08;续&#xff09; 经过上一篇文章的基础介绍&#xff0c;现在让我们更进一步&#xff0c;详细阐述如何在Windows和macOS平台上顺利搭建Tauri应用所需的开发环境&#xff0c;并指导您从创建项目…...

Redis 和 Mysql 数据库数据如何保持一致性

Redis 和 Mysql 数据库数据如何保持一致性 保持Redis和MySQL数据库数据一致性是一个常见且重要的问题&#xff0c;特别是在使用Redis作为MySQL数据库的缓存层时。以下是几种常用的保证二者数据一致性的策略和方法&#xff1a; 双写一致性&#xff08;同步更新&#xff09;&…...

探索7个MAMP本地开发环境的高效替代软件

什么是本地开发环境 本地开发环境是Web开发环境中的一种类型&#xff0c;它是指开发者自己的计算机上配置的一套用于开发和测试网站或应用程序的软件集合。这套环境使得开发者可以在本地计算机上构建和测试网站&#xff0c;而无需实时部署到服务器。 创建本地开发环境有两种方…...

靡语IT:Bootstrap 简介

1.1 Bootstrap 简介&#xff1a;什么是 Bootstrap? Bootstrap 是一个用于快速开发 Web 应用程序和网站的前端框架。Bootstrap是前端开发中比较受欢迎的框架&#xff0c;简洁且灵活。它基于HTML、CSS和JavaScript&#xff0c;HTML定义页面元素&#xff0c;CSS定义页面布局&…...

亚马逊店铺引流:海外云手机的利用方法

在电商业务蓬勃发展的当下&#xff0c;亚马逊已经成为全球最大的电商平台之一&#xff0c;拥有庞大的用户群和交易量。在激烈的市场竞争中&#xff0c;如何有效地吸引流量成为亚马逊店铺经营者所关注的重点。海外云手机作为一项新兴技术工具&#xff0c;为亚马逊店铺的流量引导…...

10 Python进阶:MongoDB

MongoDb介绍 MongoDB是一个基于分布式架构的文档数据库&#xff0c;它使用JSON样式的数据存储&#xff0c;支持动态查询&#xff0c;完全索引。MongoDB是NoSQL数据库的一种&#xff0c;主要用于处理大型、半结构化或无结构化的数据。以下是MongoDB数据库的一些关键特点和优势&a…...

Leetcode 142. 环形链表 II和Leetcode 242. 有效的字母异位词

文章目录 Leetcode 142. 环形链表 II题目描述C语言题解解题思路 Leetcode 242. 有效的字母异位词题目描述C语言题解和思路解题思路 Leetcode 142. 环形链表 II 题目描述 给定一个链表的头节点 head &#xff0c;返回链表开始入环的第一个节点。 如果链表无环&#xff0c;则返…...

【嵌入式DIY实例】-MODBUS串行通信

MODBUS串行通信 文章目录 MODBUS串行通信1、什么是RS-4852、MAX485 TTL转RS485转换器3、硬件准备4、代码实现4.1 主机和从机之间简单通信4.2 主/从机之间LED控制在本文中,我们将介绍如何使用 MAX485 MODBUS 在Arduino之间进行串行通信。 我们将使用 Arduino nano 板和 MODBUS …...

入门用Hive构建数据仓库

在当今数据爆炸的时代&#xff0c;构建高效的数据仓库是企业实现数据驱动决策的关键。Apache Hive 是一个基于 Hadoop 的数据仓库工具&#xff0c;可以轻松地进行数据存储、查询和分析。本文将介绍什么是 Hive、为什么选择 Hive 构建数据仓库、如何搭建 Hive 环境以及如何在 Hi…...

【计算机网络】会话层

负责维护两个会话主机之间链接的建立、管理和终止&#xff0c;以及数据的交换。 会话控制&#xff1a;决策该由谁来传递数据 令牌管理&#xff1a;禁止双方同时执行一个关键动作 同步功能&#xff1a;在一个长的传输过程中设置一些断点&#xff0c;以便系统崩溃后能恢复至崩…...

springboot实现七牛云的文件上传下载

一&#xff1a;依赖包 <dependency><groupId>com.qiniu</groupId><artifactId>qiniu-java-sdk</artifactId><qiniu-java-sdk.version>7.7.0</qiniu-java-sdk.version></dependency>二:具体实现 RestController RequestMapping…...