由浅入深详解四种分布式锁

在多线程环境下,为了保证数据的线程安全,锁保证同一时刻,只有一个可以访问和更新共享数据。在单机系统我们可以使用synchronized锁或者Lock锁保证线程安全。synchronized锁是Java提供的一种内置锁,在单个JVM进程中提供线程之间的锁定机制,控制多线程并发。只适用于单机环境下的并发控制:

但是如果想要锁定多个节点服务,synchronized就不适用于了:

想要在多个节点中提供锁定,在分布式系统并发控制共享资源,确保同一时刻只有一个访问可以调用,避免多个调用者竞争调用和数据不一致问题,保证数据的一致性

分布式锁就是控制分布式系统不同进程访问共享资源的一种锁的机制。不同进程之间调用需要保持互斥性,任意时刻,只有一个客户端能持有锁。

从单体锁到分布式锁,只不过是将锁的对象从一个进程的多个线程,转成多个进程。

共享资源包含:

  • 数据库
  • 文件硬盘
  • 共享内存

实现思路

分布式锁的加锁和解锁是使用不同的数值来表示不同的状态,比如0表示空闲状态。

  • 加锁

    • 加锁时,判断锁是否空闲,如果空闲,修改状态为1表示已加锁,返回成功。
    • 如果不为空闲状态0,则返回失败,表示没有获取到锁。
  • 解锁

    • 将锁状态修改为空闲状态0

以上的加锁和解锁操作,都要保证是一个原子操作

分布式锁特性

1. 互斥性

分布式锁最基本的特性,同一时刻只能一个节点服务拥有该锁,当有节点获取锁之后,其他节点无法获取锁,不同节点之间具有互斥性。

2. 超时机制

不考虑异常,正常情况下,请求获取锁之后,处理任务,处理完成之后释放锁。但是如果在处理任务发生服务异常,或者网络异常时,导致锁无法释放。其他请求都无法获取锁,变成死锁。

为了防止锁变成死锁,需要设置锁的超时时间。过了超时时间后,锁自动释放,其他请求能正常获取锁。

3. 自动续期

锁设置了超时机制后,如果持有锁的节点处理任务的时候过长超过了超时时间,就会发生线程未处理完任务锁就被释放了,其他线程就能获取到该锁,导致多个节点同时访问共享资源。对此,就需要延长超时时间。

开启一个监听线程,定时监听任务,监听任务线程还存活就延长超时时间。当任务完成、或者任务发生异常就不继续延长超时时间。

分布式实现

分布式主要有三种实现:

  • 数据库
  • Zookeeper
  • Redis

通过模拟客户下单操作。

  • 先判断库存是否充足
    • 如果充足,先扣库存,再新增订单。
    • 如果不足就提示库存不够。

先创建订单表和商品库存表:

--库存表-- 
create table t_product(`id` bigint(20) not null auto_increment,`name` varchar(64) not null comment "商品名",`store` int default 0 comment "库存",primary key(`id`)
)insert into `t_product` values (1, '红米手机', 100);-- 订单表 --
create table t_order(`id` bigint(20) not null auto_increment,`sn` varchar(64) not null comment '订单号',`num` int default null comment '数量',`price` int default null comment '单价',`product_id` bigint default null comment '商品id',`create_time` timestamp not null default CURRENT_TIMESTAMP comment '创建时间',primary key(`id`)
)

为了查询时防止幻读,我们还需要保证查询和插入是在同一个事务中。下单先判断是否有库存,有库存就减库存1,再新增订单。主要代码如下:

@Transactional
public void addOrder(Order order) throws Exception {Product product = productDao.selectById(order.getProductId());int store = product.getStore() - 1;if (store >= 0) {// 扣库存   product.setStore(store);productDao.updateByPrimaryKey(product);// 添加订单orderDao.insert(order);} else {throw new Exception("哎呦喂,库存不足");}
}

其中查询库存方法productDao.selectByIdSQL语句是:

select id,name,store from t_product where id = xxx

使用压测工具apache ab开启多个线程,请求50次:

ab -n 10 -c 2 http://127.0.0.1:8080/xxxx

压测结果:

库存剩余:72,订单数量:50

**新增了 50 条订单,库存只扣了 28 **。

这是因为在并发环境下,多个线程下单操作,前面的线程还未更新库存,后面的线程已经请求进来,并获取到了未更新的库存,后续扣减库存都不是扣减最近的库存。线程越多,扣减的库存越少。这就是在高并发场景下发生的超卖问题

1. 数据库实现分布式锁

Mysql数据库可以使用select xxx for update来实现分布式锁。

for update是一种行级锁,也叫排它锁。如果一条select语句后面加上for update,其他事务可以读取,但不能进进行更新操作。

将上面查询库存productDao.selectById方法的SQL语句后面加上for update:

select id,name,store from t_product where id = xxx for update

再使用apache ab开启多个线程,请求50次:

ab -n 10 -c 2 http://127.0.0.1:8080/xxxx

压测结果:

库存剩余:50,订单数量:50

数据库成功实现分布式锁

使用for update行级锁可以实现分布式锁,通过行级锁锁住库存,where后条件一定要走索引,不然会触发表锁,会降低MySQL的性能。

不过基于MySQL实现的分布式锁,存在性能瓶颈,在Repeatable read隔离级别下select for update操作是基于间隙锁锁实现,这是一种悲观锁,会存在线程阻塞问题。

当有大量的线程请求的情况下,大部分请求会被阻塞等待,后续的请求只能等前面的请求结束后,才能排队进来处理。

Zookeeper 实现分布式锁

数据库实现分布式锁存在性能瓶颈,无法支撑高并发的请求。可以使用Zookeeper实现分布式锁,Zookeeper提供一种分布式服务协调的中心化服务,而分布式锁的实现是基于Zookeeper的两个特性。

顺序临时节点:

Zookeeper 数据模型znode是以多层节点命名的空间,每个节点都用斜杠/分开的路径来表示,类似文件系统的目录。

节点类型分成持久节点临时节点,每个节点还可以标记有序性。一旦节点被标记为有序性,那整个节点就有自动递增的特点。利用以上的特性,创建一个持久节点作为父节点,在父节点下面创建一个临时节点,并标记该临时节点为有序性

Watch 机制:

Zookeeper 还提供了另一个重要的特性:Watch(事件监听器),在指定节点的上注册监听事件。当事件触发时,会将事件通知给对应的客户。

了解了Zookeeper的两个特性之后,那如何使用这两种特性来实现分布式锁呢?

首先,创建一个持久类型的父节点,当用户请求时,就在父节点创建临时类型的子节点,并标记临时节点为有序性。

建立子节点之后,对父节点下面所有临时节点进行排序,判断刚创建的临时节点是否是最小的节点,如果是最小的节点,就获取锁。如果不最小的节点,则等待锁,并且获取该节点上一个顺序节点,并为其注册监听事件,等待触发事件并获得锁。

当请求完毕后,删除该节点,并触发监听事件,下一个顺序节点获得锁,流程如下所示:

curator将上面实现分布式锁的思路封装好了,直接调用即可。

引入curator依赖:

<dependency><groupId>org.apache.curator</groupId><artifactId>curator-recipes</artifactId><version>4.3.0</version>
</dependency>

使用InterProcessMutex分布式可重入排它锁,一般流程如下:

InterProcessMutex lock = new InterProcessMutex(client, lockPath);
// 加锁
interProcessMutex.acquire();
// 执行代码xxxxxxx
// 解锁
interProcessMutex.release();

为了避免每次请求都要创建InterProcessMutex实例,创建InterProcessMutexbean:

private String address = "xxxxx";@Bean
public InterProcessMutex interProcessMutex() {CuratorFramework zkClient = getZkClient();String lockPath = "/lock";InterProcessMutex lock = new InterProcessMutex(zkClient,lockPath);return lock;
}private CuratorFramework getZkClient() {ExponentialBackoffRetry retry = new ExponentialBackoffRetry(1000,3,5000);CuratorFramework zkClient = CuratorFrameworkFactory.builder().connectString(address).sessionTimeoutMs(5000).connectionTimeoutMs(5000).retryPolicy(retry).build();zkClient.start();return zkClient;
}

在高并发场景下,多个用户请求系统,并获取临时节点顺序:

使用interProcessMutex获取锁和释放锁:

  • 获取锁 interProcessMutex.acquire()
  • 释放锁 interProcessMutex.release()

请求接口如下:

@RestController
public class Controller {@Autowiredprivate InterProcessMutex interProcessMutex;@GetMapping("/sec-kill")public String secKill() throws Exception {// 获取锁interProcessMutex.acquire();// 扣减库存,创建订单等操作....interProcessMutex.release();return "ok";}
}

如果获取锁之后,系统发生异常,系统就一直持有锁,后续请求也无法获取锁,导致死锁。需要设置锁超时机制,interProcessMutex.acquire添加超时时间:

interProcessMutex.acquire(watiTime,TimeUnit);

超时时间设置要根据业务执行时间来设定,不能太长,也不能太短。

Zookeeper一些特点

  • Zookeeper实现的分布式锁,相对数据库,性能有很大的提高。
  • Zookeeper配置集群,发生单点故障时、或者系统挂掉时,临时节点会因为 session 连接断开而自动删除。
  • 频繁的创建和删除节点,并且每个节点都有watch事件,对Zookeeper服务来说压力大。相对Redis的性能,还存在差距。

3. Redis 实现分布式锁

Redis 实现分布式锁,是最复杂的,但是也是性能最高的。

  • 加锁: SETNX key value 如果键不存在时,对键设值,返回1。如果键存在,不做任何操作,返回0setnx全称是set if not exist
  • 解锁: DEL key,通过删除key释放锁,删除键之后,其他线程可以争夺锁。

Redis也需要考虑超时问题,一般都是用SETNX + EXPIRE组合来实现超时设置,伪代码如下:

pubic boolean lock(Jedis jedis,String key,String value,long expireTime) {long flag = jedis.setnx(key,value);// 成功获取锁if(flag) {// 如果这里突然崩溃,无法设置过期时间,将发生死锁 jedis.expirt(key,expireTime);return true;}return false;
} 

通过setnx方法获取锁,如果key存在,就返回失败。如果不存在,就设值成功,设值成功之后,再通过expirt设置超时时间。

如果在设置超时时间和设置锁之间出现系统崩溃,此时没有给锁设置过期时间,将会出现死锁问题。

Redis 2.6.12版本后SETNX增加了过期时间参数:

pubic boolean lock(Jedis jedis,String key,String value,long expireTime) {long flag = jedis.setnx(key,value,expireTime);// 成功获取锁if(flag) {return true;}return false;
} 

解锁,需要删除键值即可,其他线程就能竞争锁了:

pubic void lock(Jedis jedis,String key) {jedis.del(key);
}

一般请求controlle如下:

@RestController
public class Controller {@Autowiredprivate InterProcessMutex interProcessMutex;@GetMapping("/sec-kill")public String secKill() throws Exception {// 获取锁lock();// 扣减库存,创建订单等操作....unLock();return "ok";}
}

Redis设置了超时时间后,就解决死锁的问题,但也会引发其他问题。

如果设置的超时时间比较短,而业务执行的时间比较长。比如超时时间设置5s,而业务执行需要10s,此时业务还未执行完,其他请求就会获取到锁,两个请求同时请求业务数据,不满足分布式锁的互斥性,无法保证线程的安全,如下流程所示:

超时解锁导致并发:

用户A先获取锁,还未执行完业务代码,此时已经过了超时时间,锁被释放。用户B获取到锁,此时用户A用户B并发执行业务数据,

锁误删除:

用户A执行完业务代码后,执行释放锁操作,而此时用户A已经被超时释放,锁被用户B持有,此时释放锁,就把用户B的锁误删了。

解决方案:

首先要将超时时间设置的长一些,满足业务执行的时间。如果系统对吞吐量要求比较严格,根据具体的业务的执行时间来设置超时时间,超时时间比业务执行时间长一些,超时时间不能设置太长也不能设置太短

针对锁误删除的问题。每个线程在获取锁时,设置一个的线程标识,比如UUID,作为唯一的标识,设置value值,在解锁时,先判断是是否是自己线程的标识,如果不是,就不做删除:

pubic void lock(Jedis jedis,String key,String value) { if (jedis.get(key).equals(value)) {jedis.del(key);}    
}

除了设置合理超时时间外,可能还有偶尔几个线程执行业务代码,因为网络环境执行时间变长。这时候就需要再加一个线程,定时执行,自动续期锁。

4.Redission 分布式锁

Redis虽然作为分布式锁来说,性能是最好的。但是也是最复杂的,上面总结Redis主要有下面几个问题:

  • 死锁
  • 设置超时后
    • 锁误删
    • 业务还继续执行,导致多个线程并发执行

线上都是用Redission实现分布式锁Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。Redisson是基于netty通信框架实现的,所以支持非阻塞通信,性能优于Jedis

Redisson分布式锁四层保护:

  • 防死锁
  • 防误删
  • 可重入
  • 自动续期

Redisson实现Redis分布式锁,支持单机和集群模式,

引入maven依赖:

<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.8.2</version>
</dependency>

添加Redission配置:

@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redissonClient() {Config config = new Config();// 单机模式config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("xxxx");// 集群模式/*config.useClusterServers().setScanInterval(2000) // 集群状态扫描间隔时间,单位是毫秒//可以用"rediss://"来启用SSL连接.addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001").addNodeAddress("redis://127.0.0.1:7002");*/       return Redisson.create(config);}}

使用Redission分布式锁,分成三个步骤:

  • 获取锁 redissonClient.getLock("lock")
  • 加锁 rLock.lock()
  • 解锁 rLock.unlock()

请求controlle示例如下:

@RestController
public class Controller {@Autowiredprivate RedissonClient redissonClient;@GetMapping("/sec-kill")public String secKill() throws Exception {// 获取锁RLock rLock = redissonClient.getLock("lock");// 加锁rLock.lock();// 执行业务数据// 解锁rLock.unlock();return "ok";}
}

Redission实现的分布式锁,直接调用,不需要锁异常、超时并发、锁删除等问题,它把处理上面的问题的代码都封装好了,直接调用即可。

Redlock 算法

在单机模式下,Redis发生单机故障,Redis master宕机了该怎么办?是否将锁转移到slave呢?答案是不行的,因为Redis复制是异步的,无法满足锁互斥性。

Redlock算法可以解决上面的问题,在集群模式下,Redission 使用Redlock算法,使用单机实例的方式顺序获取集群下的锁。如果请求超时,则认定该节点不可用。当获取锁的实例数超过半数时,则获取锁成功。如果获取锁失败,即没有获取超过半数的实例,那么久释放所有节点的锁。

Watch dog 看门狗机制

Redission通过看门狗的实现自动续期的功能,当分布式锁获取到锁后,对应的Redis宕机了,会出现死锁的状态,为避免出现这种状态,锁一般会设置一个过期时间。默认是30s,超过30s后,会自动释放锁。

Redission实例被关闭之前,不断的延长锁的有效期,拿到线程的锁如果没有完成业务操作,那么看门狗会一直延长锁的超时时间。默认情况下,每10s延长一次超时时间,续期时间是30s,可以通过Config.lockWatchdogTimeout来设定。

Redisson还提供了可以指定leaseTime参数的加锁方法来指定加锁的时间。超过这个时间后锁便自动解开了,不会延长锁的有效期。

总结

  • 分布式锁是由于单机锁无法满足分布式系统锁,在分布式环境下,需要分布式锁来控制共享内容,保证线程的安全。
  • 分布式满足几个特性
    • 互斥性
    • 超时释放锁
    • 自动续期
  • 分布式锁实现方式
    • Mysql使用排它锁,select xxxx for update。实现比较简单,但是数据库无法支撑大量请求访问,性能较差。
    • Zookeeper先创建一个持久类型的节点,当多个线程请求时,在持久类型节点创建顺序临时节点,先判断自己是否是最小节点,如果是持有锁,执行后续逻辑,如果不是就找到上一个顺序节点,并添加watch监听事件。线程处理结束后。触发监听事件,通知下一个节点获取锁。Zookeeper性能优于数据库,但是频繁的创建、删除节点并且创建watch监听,对服务器的压力也大。
    • 使用Redis实现分布式锁性能是最优,也是最复杂的。SETNX key value 获取锁,如果key存在,则成功获取锁,否则获取锁失败。使用del删除节点释放锁。复杂在于需要平衡超时时间锁续期。不设置超时时间,会发生死锁。设置了超时时间就可能出现业务处理时间大于超时时间,出现多个锁同时访问共享数据,以及锁误删的情况。解决方案是根据具体的业务时间设置合理的超时时间,锁误删的话要给每个线程设置一个唯一的id。此外,如果业务时间大于超时时间,开启线程定时续约时间。
    • 针对Redis实现分布式锁存在的问题Redisson提供了解决方案,Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid),Redisson是基于netty通信框架实现的,所以支持非阻塞通信,性能也比较高。Redisson有四种特性防死锁防误删可重入自动续期。支持单机和集群模式。自动续期是使用watch dog看门狗机制,在实例关闭前,不断地延长锁的有效期。默认10s延迟一次,延长时间为30s。 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

小程序的数据绑定和事件绑定

小程序的数据绑定 1.需要渲染的数据放在index.js中的data里 Page({data: {info:HELLO WORLD,imgSrc:/images/1.jpg,randomNum:Math.random()*10,randomNum1:Math.random().toFixed(2)}, }) 2.在WXML中通过{{}}获取数据 <view>{{info}}</view><image src"{{…

RocketMQ 5.0 架构解析:如何基于云原生架构支撑多元化场景

作者&#xff1a;隆基 本文将从技术角度了解 RocketMQ 的云原生架构&#xff0c;了解 RocketMQ 如何基于一套统一的架构支撑多元化的场景。 文章主要包含三部分内容。首先介绍 RocketMQ 5.0 的核心概念和架构概览&#xff1b;然后从集群角度出发&#xff0c;从宏观视角学习 R…

优测云服务平台|【压力测试功能升级】轻松完成压测任务

一、本次升级主要功能如下&#xff1a; 1.多份报告对比查看测试结果 2.报告新增多种下载格式 Word格式Excel格式 3.新增多种编排复杂场景的控制器 漏斗控制器并行控制器事务控制器仅一次控制器分组控制器集合点 4.新增概览页面&#xff0c;包含多种统计维度 二、报告对比…

开源语音聊天软件Mumble

网友 大气 告诉我&#xff0c;Openblocks在国内还有个版本叫 码匠&#xff0c;更贴合国内软件开发的需求&#xff0c;如接入了国内常用的身份认证&#xff0c;接入了国内的数据库和云服务&#xff0c;也对小程序、企微 sdk 等场景做了适配。 在 https://majiang.co/docs/docke…

类与对象(上)

类与对象&#xff08;上&#xff09; 一、面向过程和面向对象的区别二、类1、类的引入2、类的定义&#xff08;1&#xff09;类的基本定义&#xff08;2&#xff09;类的成员函数的定义方法 3、类的访问限定符4、封装5、驼峰法命名规则6、类的作用域7、类的实例化&#xff08;1…

金蝶软件实现导入Excel数据分录行信息到单据体分录行中

>>>适合KIS云专业版V16.0|KIS云旗舰版V7.0|K/3 WISE 14.0等版本<<< 金蝶软件中实现[导入Excel数据业务分录行]信息到[金蝶单据体分录]中,在采购订单|采购入库单|销售订单|销售出库单等类型单据中,以少量的必要字段在excel表格中按模板填列好,很方便快捷地从…

IntelliJ IDEA(简称Idea) 基本常用设置及Maven部署---详细介绍

一&#xff0c;Idea是什么&#xff1f; 前言&#xff1a; 众所周知&#xff0c;现在有许多编译工具&#xff0c;如eclipse&#xff0c;pathon, 今天所要学的Idea编译工具 Idea是JetBrains公司开发的一款强大的集成开发环境&#xff08;IDE&#xff09;&#xff0c;主要用于Java…

Rancher管理K8S

1 介绍 Rancher是一个开源的企业级多集群Kubernetes管理平台&#xff0c;实现了Kubernetes集群在混合云本地数据中心的集中部署与管理&#xff0c;以确保集群的安全性&#xff0c;加速企业数字化转型。Rancher 1.0版本在2016年就已发布&#xff0c;时至今日&#xff0c;Ranche…

CS:GO升级 Linux不再是“法外之地”

在前天的VAC大规模封禁中&#xff0c;有不少Linux平台的作弊玩家也迎来了“迟到”的VAC封禁。   一直以来&#xff0c;Linux就是VAC封禁的法外之地。虽然大部分玩家都使用Windows平台进行游戏。但实际上&#xff0c;使用Linux畅玩CS:GO的玩家也不在少数。 以前V社主要打击W…

06-微信小程序-注册程序-场景值

06-微信小程序-注册程序 文章目录 注册小程序参数 Object object案例代码 场景值场景值作用场景值列表案例代码 注册小程序 每个小程序都需要在 app.js 中调用 App 方法注册小程序实例&#xff0c;绑定生命周期回调函数、错误监听和页面不存在监听函数等。 详细的参数含义和使…

【LeetCode】543.二叉树的直径

题目 给你一棵二叉树的根节点&#xff0c;返回该树的 直径 。 二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。 两节点之间路径的 长度 由它们之间边数表示。 示例 1&#xff1a; 输入&#xff1a;root [1,2,3,4,5]…

高并发内存池(centralcache)[2]

Central cache threadcache是每个线程独享&#xff0c;而centralcache是多线程共享&#xff0c;需要加锁&#xff08;桶锁&#xff09;一个桶一个锁 解决外碎片问题&#xff1a;内碎片&#xff1a;申请大小超过实际大小&#xff1b;外碎片&#xff1a;空间碎片不连续&#x…

跨境电商ERP源码大揭秘,让你少走弯路

本文将深入介绍跨境电商ERP源码的重要性以及如何选择和应用它们&#xff0c;让你的电商业务更高效、顺畅。 跨境电商ERP源码的重要性 提升管理效率 跨境电商运营面临着众多挑战&#xff0c;如订单管理、库存追踪和财务报告等。跨境电商ERP源码能够集成这些功能&#xff0c;帮…

自动驾驶,一次道阻且长的远征|数据猿直播干货分享

‍数据智能产业创新服务媒体 ——聚焦数智 改变商业 在6月的世界人工智能大会上&#xff0c;马斯克在致辞中宣称&#xff0c;到2023年底&#xff0c;特斯拉便可实现L4级或L5级的完全自动驾驶&#xff08;FSD&#xff09;。两个月之后&#xff0c;马斯克又在X社交平台上发言&am…

java面试强基(16)

目录 clone方法的保护机制 Java中由SubString方法是否会引起内存泄漏&#xff1f; Java中提供了哪两种用于多态的机制? 程序计数器(线程私有) 如何判断对象是否是垃圾&#xff1f; clone方法的保护机制 clone0方法的保护机制在Object中是被声明为 protected的。以User…

1000元到3000元预算的阿里云服务器配置大全

1000元、2000元或3000元预算能够买的阿里云服务器配置有哪些&#xff1f;可以选择ECS通用算力型u1云服务器、ECS计算型c7或通用型g7实例规格&#xff0c;当然&#xff0c;如果选择轻量应用服务器的话&#xff0c;更省钱&#xff0c;阿里云百科分享1000-3000元预算能买的阿里云服…

三、Dubbo 注册中心

三、Dubbo 注册中心 3.1 注册中心概述 主要作用 动态加入&#xff1a;服务提供者通过注册中心动态地把自己暴露给其他消费者动态发现&#xff1a;消费者动态地感知新的配置、路由规则和新的服务提供者动态调整&#xff1a;注册中心支持参数的动态调整&#xff0c;新参数自动更…

如何用轻叶H5制作一份调查问卷

在营销落地页中&#xff0c;问卷类H5是一种制作简单&#xff0c;易于传播的落地页&#xff0c;通过精巧的设计和严密的逻辑设置&#xff0c;问卷类H5的投放效果也是不容小觑的。 问卷类H5在制作中有以下不可缺少的要素&#xff1a; 清晰的标题和简要的说明 标题应该简明扼要地…

用pytorch实现AlexNet

AlexNet经典网络由Alex Krizhevsky、Hinton等人在2012年提出&#xff0c;发表在NIPS&#xff0c;论文名为《ImageNet Classification with Deep Convolutional Neural Networks》&#xff0c;论文见&#xff1a;http://www.cs.toronto.edu/~hinton/absps/imagenet.pdf &#xf…