1、分布式锁实现原理与最佳实践(一)
在单体的应用开发场景中涉及并发同步时,大家往往采用Synchronized(同步)或同一个JVM内Lock机制来解决多线程间的同步问题。而在分布式集群工作的开发场景中,就需要一种更加高级的锁机制来处理跨机器的进程之间的数据同步问题,这种跨机器的锁就是分布式锁。接下来本文将为大家分享分布式锁的最佳实践。
一、超卖问题复现
1.1 现象
存在如下的几张表:
商品表

订单表

订单item表

商品的库存为1,但是并发高的时候有多笔订单。
错误案例一:数据库update相互覆盖
直接在内存中判断是否有库存,计算扣减之后的值更新数据库,并发的情况下会导致相互覆盖发生:
@Transactional(rollbackFor = Exception.class)
public Long createOrder() throws Exception {Product product = productMapper.selectByPrimaryKey(purchaseProductId);// ... 忽略校验逻辑//商品当前库存Integer currentCount = product.getCount();//校验库存if (purchaseProductNum > currentCount) {throw new Exception("商品" + purchaseProductId + "仅剩" + currentCount + "件,无法购买");}// 计算剩余库存Integer leftCount = currentCount - purchaseProductNum;// 更新库存product.setCount(leftCount);product.setGmtModified(new Date());productMapper.updateByPrimaryKeySelective(product);Order order = new Order();// ... 省略 SetorderMapper.insertSelective(order);OrderItem orderItem = new OrderItem();orderItem.setOrderId(order.getId());// ... 省略 Setreturn order.getId();
}
错误案例二:扣减串行执行,但是库存被扣减为负数
在 SQL 中加入运算避免值的相互覆盖,但是库存的数量变为负数,因为校验库存是否足够还是在内存中执行的,并发情况下都会读到有库存:
@Transactional(rollbackFor = Exception.class)
public Long createOrder() throws Exception {Product product = productMapper.selectByPrimaryKey(purchaseProductId);// ... 忽略校验逻辑//商品当前库存Integer currentCount = product.getCount();//校验库存if (purchaseProductNum > currentCount) {throw new Exception("商品" + purchaseProductId + "仅剩" + currentCount + "件,无法购买");}// 使用 set count = count - #{purchaseProductNum,jdbcType=INTEGER}, 更新库存productMapper.updateProductCount(purchaseProductNum,new Date(),product.getId());Order order = new Order();// ... 省略 SetorderMapper.insertSelective(order);OrderItem orderItem = new OrderItem();orderItem.setOrderId(order.getId());// ... 省略 Setreturn order.getId();
}
错误案例三:使用 synchronized 实现内存中串行校验,但是依旧扣减为负数
因为我们使用的是事务的注解,synchronized加在方法上,方法执行结束的时候锁就会释放,此时的事务还没有提交,另一个线程拿到这把锁之后就会有一次扣减,导致负数。
@Transactional(rollbackFor = Exception.class)
public synchronized Long createOrder() throws Exception {Product product = productMapper.selectByPrimaryKey(purchaseProductId);// ... 忽略校验逻辑//商品当前库存Integer currentCount = product.getCount();//校验库存if (purchaseProductNum > currentCount) {throw new Exception("商品" + purchaseProductId + "仅剩" + currentCount + "件,无法购买");}// 使用 set count = count - #{purchaseProductNum,jdbcType=INTEGER}, 更新库存productMapper.updateProductCount(purchaseProductNum,new Date(),product.getId());Order order = new Order();// ... 省略 SetorderMapper.insertSelective(order);OrderItem orderItem = new OrderItem();orderItem.setOrderId(order.getId());// ... 省略 Setreturn order.getId();
}
1.2 解决办法
从上面造成问题的原因来看,只要是扣减库存的动作,不是原子性的。多个线程同时操作就会有问题。
单体应用:使用本地锁 + 数据库中的行锁解决分布式应用:使用数据库中的乐观锁,加一个 version 字段,利用CAS来实现,会导致大量的 update 失败使用数据库维护一张锁的表 + 悲观锁 select,使用 select for update 实现使用Redis 的 setNX实现分布式锁使用zookeeper的watcher + 有序临时节点来实现可阻塞的分布式锁使用Redisson框架内的分布式锁来实现使用curator 框架内的分布式锁来实现
二、单体应用解决超卖的问题
正确示例:将事务包含在锁的控制范围内
保证在锁释放之前,事务已经提交。
//@Transactional(rollbackFor = Exception.class)
public synchronized Long createOrder() throws Exception {TransactionStatus transaction1 = platformTransactionManager.getTransaction(transactionDefinition);Product product = productMapper.selectByPrimaryKey(purchaseProductId);if (product == null) {platformTransactionManager.rollback(transaction1);throw new Exception("购买商品:" + purchaseProductId + "不存在");}//商品当前库存Integer currentCount = product.getCount();//校验库存if (purchaseProductNum > currentCount) {platformTransactionManager.rollback(transaction1);throw new Exception("商品" + purchaseProductId + "仅剩" + currentCount + "件,无法购买");}productMapper.updateProductCount(purchaseProductNum, new Date(), product.getId());Order order = new Order();// ... 省略 SetorderMapper.insertSelective(order);OrderItem orderItem = new OrderItem();orderItem.setOrderId(order.getId());// ... 省略 Setreturn order.getId();platformTransactionManager.commit(transaction1);
}
正确示例:使用synchronized的代码块
public Long createOrder() throws Exception {Product product = null;//synchronized (this) {//synchronized (object) {synchronized (DBOrderService2.class) {TransactionStatus transaction1 = platformTransactionManager.getTransaction(transactionDefinition);product = productMapper.selectByPrimaryKey(purchaseProductId);if (product == null) {platformTransactionManager.rollback(transaction1);throw new Exception("购买商品:" + purchaseProductId + "不存在");}//商品当前库存Integer currentCount = product.getCount();System.out.println(Thread.currentThread().getName() + "库存数:" + currentCount);//校验库存if (purchaseProductNum > currentCount) {platformTransactionManager.rollback(transaction1);throw new Exception("商品" + purchaseProductId + "仅剩" + currentCount + "件,无法购买");}productMapper.updateProductCount(purchaseProductNum, new Date(), product.getId());platformTransactionManager.commit(transaction1);}TransactionStatus transaction2 = platformTransactionManager.getTransaction(transactionDefinition);Order order = new Order();// ... 省略 SetorderMapper.insertSelective(order);OrderItem orderItem = new OrderItem();// ... 省略 SetorderItemMapper.insertSelective(orderItem);platformTransactionManager.commit(transaction2);return order.getId();
正确示例:使用Lock
private Lock lock = new ReentrantLock();public Long createOrder() throws Exception{ Product product = null;lock.lock();TransactionStatus transaction1 = platformTransactionManager.getTransaction(transactionDefinition);try {product = productMapper.selectByPrimaryKey(purchaseProductId);if (product==null){throw new Exception("购买商品:"+purchaseProductId+"不存在");}//商品当前库存Integer currentCount = product.getCount();System.out.println(Thread.currentThread().getName()+"库存数:"+currentCount);//校验库存if (purchaseProductNum > currentCount){throw new Exception("商品"+purchaseProductId+"仅剩"+currentCount+"件,无法购买");}productMapper.updateProductCount(purchaseProductNum,new Date(),product.getId());platformTransactionManager.commit(transaction1);} catch (Exception e) {platformTransactionManager.rollback(transaction1);} finally {// 注意抛异常的时候锁释放不掉,分布式锁也一样,都要在这里删掉lock.unlock();}TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);Order order = new Order();// ... 省略 SetorderMapper.insertSelective(order);OrderItem orderItem = new OrderItem();// ... 省略 SetorderItemMapper.insertSelective(orderItem);platformTransactionManager.commit(transaction);return order.getId();
}
三、常见分布式锁的使用
上面使用的方法只能解决单体项目,当部署多台机器的时候就会失效,因为锁本身就是单机的锁,所以需要使用分布式锁来实现。
3.1 数据库乐观锁
数据库中的乐观锁,加一个version字段,利用CAS来实现,乐观锁的方式支持多台机器并发安全。但是并发量大的时候会导致大量的update失败
3.2 数据库分布式锁
db操作性能较差,并且有锁表的风险,一般不考虑。
3.2.1 简单的数据库锁

select for update
直接在数据库新建一张表:

锁的code预先写到数据库中,抢锁的时候,使用select for update查询锁对应的key,也就是这里的code,阻塞就说明别人在使用锁。
// 加上事务就是为了 for update 的锁可以一直生效到事务执行结束
// 默认回滚的是 RunTimeException
@Transactional(rollbackFor = Exception.class)
public String singleLock() throws Exception {log.info("我进入了方法!");DistributeLock distributeLock = distributeLockMapper.selectDistributeLock("demo");if (distributeLock==null) {throw new Exception("分布式锁找不到");}log.info("我进入了锁!");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}return "我已经执行完成!";
}<select id="selectDistributeLock" resultType="com.deltaqin.distribute.model.DistributeLock">select * from distribute_lockwhere businessCode = #{businessCode,jdbcType=VARCHAR}for update
</select>
使用唯一键作为限制,插入一条数据,其他待执行的SQL就会失败,当数据删除之后再去获取锁 ,这是利用了唯一索引的排他性。
insert lock
直接维护一张锁表:
@Autowired
private MethodlockMapper methodlockMapper;@Override
public boolean tryLock() {try {//插入一条数据 insert intomethodlockMapper.insert(new Methodlock("lock"));}catch (Exception e){//插入失败return false;}return true;
}@Override
public void waitLock() {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}
}@Override
public void unlock() {//删除数据 deletemethodlockMapper.deleteByMethodlock("lock");System.out.println("-------释放锁------");
3.3 Redis setNx
Redis 原生支持的,保证只有一个会话可以设置成功,因为Redis自己就是单线程串行执行的。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring.redis.host=localhost
封装一个锁对象:
@Slf4j
public class RedisLock implements AutoCloseable {private RedisTemplate redisTemplate;private String key;private String value;//单位:秒private int expireTime;/*** 没有传递 value,因为直接使用的是随机值*/public RedisLock(RedisTemplate redisTemplate,String key,int expireTime){this.redisTemplate = redisTemplate;this.key = key;this.expireTime=expireTime;this.value = UUID.randomUUID().toString();}/*** JDK 1.7 之后的自动关闭的功能*/@Overridepublic void close() throws Exception {unLock();}/*** 获取分布式锁* SET resource_name my_random_value NX PX 30000* 每一个线程对应的随机值 my_random_value 不一样,用于释放锁的时候校验* NX 表示 key 不存在的时候成功,key 存在的时候设置不成功,Redis 自己是单线程,串行执行的,第一个执行的才可以设置成功* PX 表示过期时间,没有设置的话,忘记删除,就会永远不过期*/public boolean getLock(){RedisCallback<Boolean> redisCallback = connection -> {//设置NXRedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();//设置过期时间Expiration expiration = Expiration.seconds(expireTime);//序列化keybyte[] redisKey = redisTemplate.getKeySerializer().serialize(key);//序列化valuebyte[] redisValue = redisTemplate.getValueSerializer().serialize(value);//执行setnx操作Boolean result = connection.set(redisKey, redisValue, expiration, setOption);return result;};//获取分布式锁Boolean lock = (Boolean)redisTemplate.execute(redisCallback);return lock;}/*** 释放锁的时候随机数相同的时候才可以释放,避免释放了别人设置的锁(自己的已经过期了所以别人才可以设置成功)* 释放的时候采用 LUA 脚本,因为 delete 没有原生支持删除的时候校验值,证明是当前线程设置进去的值* 脚本是在官方文档里面有的*/public boolean unLock() {// key 是自己才可以释放,不是就不能释放别人的锁String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +" return redis.call(\"del\",KEYS[1])\n" +"else\n" +" return 0\n" +"end";RedisScript<Boolean> redisScript = RedisScript.of(script,Boolean.class);List<String> keys = Arrays.asList(key);// 执行脚本的时候传递的 value 就是对应的值Boolean result = (Boolean)redisTemplate.execute(redisScript, keys, value);log.info("释放锁的结果:"+result);return result;}
}每次获取的时候,自己线程需要new对应的RedisLock:
public String redisLock(){log.info("我进入了方法!");try (RedisLock redisLock = new RedisLock(redisTemplate,"redisKey",30)){if (redisLock.getLock()) {log.info("我进入了锁!!");Thread.sleep(15000);}} catch (InterruptedException e) {e.printStackTrace();} catch (Exception e) {e.printStackTrace();}log.info("方法执行完成");return "方法执行完成";
}
3.4 zookeeper 瞬时znode节点 + watcher监听机制
临时节点具备数据自动删除的功能。当client与ZooKeeper连接和session断掉时,相应的临时节点就会被删除。zk有瞬时和持久节点,瞬时节点不可以有子节点。会话结束之后瞬时节点就会消失,基于zk的瞬时有序节点实现分布式锁:
多线程并发创建瞬时节点的时候,得到有序的序列,序号最小的线程可以获得锁;
其他的线程监听自己序号的前一个序号。前一个线程执行结束之后删除自己序号的节点;
下一个序号的线程得到通知,继续执行;
以此类推,创建节点的时候,就确认了线程执行的顺序。

<dependency><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId><version>3.4.14</version><exclusions><exclusion><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId></exclusion></exclusions>
</dependency>
zk 的观察器只可以监控一次,数据发生变化之后可以发送给客户端,之后需要再次设置监控。exists、create、getChildren三个方法都可以添加watcher ,也就是在调用方法的时候传递true就是添加监听。注意这里Lock 实现了Watcher和AutoCloseable:
当前线程创建的节点是第一个节点就获得锁,否则就监听自己的前一个节点的事件:
/*** 自己本身就是一个 watcher,可以得到通知* AutoCloseable 实现自动关闭,资源不使用的时候*/
@Slf4j
public class ZkLock implements AutoCloseable, Watcher {private ZooKeeper zooKeeper;/*** 记录当前锁的名字*/private String znode;public ZkLock() throws IOException {this.zooKeeper = new ZooKeeper("localhost:2181",10000,this);}public boolean getLock(String businessCode) {try {//创建业务 根节点Stat stat = zooKeeper.exists("/" + businessCode, false);if (stat==null){zooKeeper.create("/" + businessCode,businessCode.getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);}//创建瞬时有序节点 /order/order_00000001znode = zooKeeper.create("/" + businessCode + "/" + businessCode + "_", businessCode.getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);//获取业务节点下 所有的子节点List<String> childrenNodes = zooKeeper.getChildren("/" + businessCode, false);//获取序号最小的(第一个)子节点Collections.sort(childrenNodes);String firstNode = childrenNodes.get(0);//如果创建的节点是第一个子节点,则获得锁if (znode.endsWith(firstNode)){return true;}//如果不是第一个子节点,则监听前一个节点String lastNode = firstNode;for (String node:childrenNodes){if (znode.endsWith(node)){zooKeeper.exists("/"+businessCode+"/"+lastNode,true);break;}else {lastNode = node;}}synchronized (this){wait();}return true;} catch (Exception e) {e.printStackTrace();}return false;}@Overridepublic void close() throws Exception {zooKeeper.delete(znode,-1);zooKeeper.close();log.info("我已经释放了锁!");}@Overridepublic void process(WatchedEvent event) {if (event.getType() == Event.EventType.NodeDeleted){synchronized (this){notify();}}}
}
3.5 zookeeper curator
在实际的开发中,不建议去自己“重复造轮子”,而建议直接使用Curator客户端中的各种官方实现的分布式锁,例如其中的InterProcessMutex可重入锁。
<dependency><groupId>org.apache.curator</groupId><artifactId>curator-recipes</artifactId><version>4.2.0</version><exclusions><exclusion><artifactId>slf4j-api</artifactId><groupId>org.slf4j</groupId></exclusion></exclusions>
</dependency>
@Bean(initMethod="start",destroyMethod = "close")
public CuratorFramework getCuratorFramework() {RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);CuratorFramework client = CuratorFrameworkFactory.newClient("localhost:2181", retryPolicy);return client;
}
框架已经实现了分布式锁。zk的Java客户端升级版。使用的时候直接指定重试的策略就可以。
官网中分布式锁的实现是在curator-recipes依赖中,不要引用错了。
@Autowired
private CuratorFramework client;@Test
public void testCuratorLock(){InterProcessMutex lock = new InterProcessMutex(client, "/order");try {if ( lock.acquire(30, TimeUnit.SECONDS) ) {try {log.info("我获得了锁!!!");}finally {lock.release();}}} catch (Exception e) {e.printStackTrace();}client.close();
}
3.6 Redission
重新实现了Java并发包下处理并发的类,让其可以跨JVM使用,例如CHM等。
3.6.1 非SpringBoot项目引入
https://redisson.org/
引入Redisson的依赖,然后配置对应的XML即可:
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.11.2</version><exclusions><exclusion><artifactId>slf4j-api</artifactId><groupId>org.slf4j</groupId></exclusion></exclusions>
</dependency>
编写相应的redisson.xml
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xmlns:redisson="http://redisson.org/schema/redisson"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsdhttp://redisson.org/schema/redissonhttp://redisson.org/schema/redisson/redisson.xsd
"><redisson:client><redisson:single-server address="redis://127.0.0.1:6379"/></redisson:client>
</beans>
配置对应@ImportResource(“classpath*:redisson.xml”)资源文件。
3.6.2 SpringBoot项目引入
或者直接使用springBoot的starter即可。
https://github.com/redisson/redisson/tree/master/redisson-spring-boot-starter
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.19.1</version>
</dependency>
修改application.properties即可:#spring.redis.host=
3.6.3 设置配置类
@Bean
public RedissonClient getRedissonClient() {Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");return Redisson.create(config);
}
3.6.4 使用
@Test
public void testRedissonLock() {RLock rLock = redisson.getLock("order");try {rLock.lock(30, TimeUnit.SECONDS);log.info("我获得了锁!!!");Thread.sleep(10000);} catch (InterruptedException e) {e.printStackTrace();}finally {log.info("我释放了锁!!");rLock.unlock();}
}
相关文章:
1、分布式锁实现原理与最佳实践(一)
在单体的应用开发场景中涉及并发同步时,大家往往采用Synchronized(同步)或同一个JVM内Lock机制来解决多线程间的同步问题。而在分布式集群工作的开发场景中,就需要一种更加高级的锁机制来处理跨机器的进程之间的数据同步问题&…...
Autosar通信实战系列03-NM模块要点及其配置介绍
本文框架 前言1. NM模块要点介绍1.1 NM基本功能介绍1.2 NM协同功能介绍2. NM配置2.1 NmGlobalConfig配置2.2 NmChannelConfigs配置前言 在本系列笔者将结合工作中对通信实战部分的应用经验进一步介绍常用,包括但不限于通信各模块的开发教程,代码逻辑分析,调测试方法及典型问…...
Golang模块管理功能
文章目录 1. Golang 包管理1.1 GOPATH 包管理1.2 Go vendor 包管理1.3 Go modules包管理2. Go Modules 应用实践2.1 Go modules关键信息2.1.1 go mod 命令行2.1.2 配置代理服务2.2 创建项目2.3 获取依赖包2.4 运行项目1. Golang 包管理 1.1 GOPATH 包管理 第一阶段: Golang初…...
从零构建属于自己的GPT系列1:文本数据预处理、文本数据tokenizer、逐行代码解读
🚩🚩🚩Hugging Face 实战系列 总目录 有任何问题欢迎在下面留言 本篇文章的代码运行界面均在PyCharm中进行 本篇文章配套的代码资源已经上传 从零构建属于自己的GPT系列1:文本数据预处理 从零构建属于自己的GPT系列2:语…...
scipy 笔记:scipy.spatial.distance
1 pdist 计算n维空间中观测点之间的成对距离。 scipy.spatial.distance.pdist(X, metriceuclidean, *, outNone, **kwargs) 1.1 主要参数 X一个m行n列的数组,表示n维空间中的m个原始观测点metric使用的距离度量out输出数组。如果非空,压缩的距离矩阵…...
java video audio encoder
引言 在现代互联网的时代,视频和音频已经成为人们生活中不可或缺的一部分。而在计算机科学中,视频和音频编码器则是将原始的视频和音频数据转换为可压缩格式的关键技术。在本文中,我们将探讨基于Java的视频和音频编码器的使用。 什么是视频…...
TypeScript 中声明类型的方法
1、使用:运算符来为变量和函数参数指定类型。例如: let num: number 5; function add(a: number, b: number): number {return a b; }2、使用 type 关键字来声明自定义类型别名。例如: type Point {x: number;y: number; };3、使用 interface 关键字…...
显示器校准软件BetterDisplay Pro mac中文版介绍
BetterDisplay Pro mac是一款显示器校准软件,可以帮助用户调整显示器的颜色和亮度,以获得更加真实、清晰和舒适的视觉体验。 BetterDisplay Pro mac软件特点 - 显示器校准:可以根据不同的需求和环境条件调整显示器的颜色、亮度和对比度等参数…...
Element UI 走马灯 实现鼠标滚动切换页面
鼠标滚动切换页面 elementui Carousel 走马灯鼠标滚轮事件实现 一、在轮播图外的盒子外添加鼠标滚轮事件,触发GoWheel函数。 wheel"goWheel"二、通过判断deltaY的数值来触发相应事件 它检查滚轮事件的deltaY属性是否大于0 event.deltaY当鼠标滚轮向下…...
在Docker上部署Springboot项目
在Docker上部署Springboot项目 ###1.安装docker 2.安装mysql 拉 Mysql 镜像 docker pull mysql:5.7.31运行 Mysql 5.7.31 第一次运行需要设置密码 docker run -d --name myMysql -p 9506:3306 -v /data/mysql:/var/lib/mysql -e MYSQL_ROOT_PASSWORD1234 mysql:5.7.31不是…...
2024中国眼博会,全国眼康与眼镜品牌加盟展会,北京眼健康展
立足北京,面向全球,2024第六届CEYEE中国眼博会,将以大规模的展览面积在4月与您相会; ——春天是万物复苏的季节,更是企业开拓市场,抓住春季发展机遇的重要时节;第六届CEYEE中国眼博会将在2024年…...
C++学习 --谓词
目录 1, 什么是谓词 1-1, 一元谓词 1-2, 二元谓词 1, 什么是谓词 返回bool类型的仿函数, 叫着谓词, 分为一元谓词和二元谓词 1-1, 一元谓词 operator()接收一个参数,叫着一元谓…...
Arkts深入了解运用 LazyForEach【鸿蒙专栏-17】
文章目录 深入了解 LazyForEach:数据懒加载LazyForEach概述接口描述IDataSource接口DataChangeListener接口使用限制和注意事项键值生成规则和组件创建规则首次渲染键值相同时错误渲染键值生成规则和组件创建规则首次渲染键值相同时错误渲染键值生成规则和组件创建规则首次渲染…...
如何让你的 Jmeter+Ant 测试报告更具吸引力?
引言 想象一下,你辛苦搭建了一个复杂的网站,投入了大量的时间和精力进行开发和测试。当你终于完成了测试并准备生成测试报告时,你可能会发现这个过程相当乏味,而对于其他人来说,它可能也不那么吸引人。 但是…...
游戏APP接入哪些广告类型
当谈到游戏应用程序(APP)接入广告时,选择适合用户体验和盈利的广告类型至关重要。游戏开发者通常考虑以下几种广告类型: admaoyan猫眼聚合 横幅广告: 这些广告以横幅形式显示在游戏界面的顶部或底部。它们不会打断游戏…...
Echarts地图registerMap使用的GeoJson数据获取
https://datav.aliyun.com/portal/school/atlas/area_selector 可以选择省,市,区。 也可以直接在地图上点击对应区域。 我的应用场景 我这里用到这个还是一个特别老的大屏项目,用的jq写的。显示中国地图边界区域 我们在上面的这个地区选择…...
【JavaEE】Java中的多线程 (Thread类)
作者主页:paper jie_博客 本文作者:大家好,我是paper jie,感谢你阅读本文,欢迎一建三连哦。 本文录入于《JavaEE》专栏,本专栏是针对于大学生,编程小白精心打造的。笔者用重金(时间和精力)打造&…...
python中具名元组的使用
collections.namedtuple是一个工厂函数,它可以用来构建一个带字段名的元组和一个有名字的类。 from collections import namedtuple City namedtuple(City2, name country population coordinates) tokyo City(Tokyo, JP, 36.933, (35.689722, 139.691667)) pr…...
【开题报告】基于SpringBoot的婚纱店试妆预约平台的设计与实现
1.选题背景 婚礼是人生中的重要时刻,而试妆是婚礼准备过程中不可或缺的一环。传统的婚纱店试妆预约方式通常需要亲自到店或通过电话预约,这样的方式可能存在一些问题。首先,用户需要花费时间和精力到店进行预约,对于忙碌的现代人…...
Qt 布局讲解及举例
Qt布局是一个用于管理窗口部件位置和大小的机制,它使得开发人员能够轻松地创建可伸缩、可调整大小的界面。在Qt中,布局管理器是一种用于自动调整窗口部件大小的机制,它可以根据窗口大小的变化自动调整部件的位置和大小。 Qt布局管理器通过使…...
质量体系的重要
质量体系是为确保产品、服务或过程质量满足规定要求,由相互关联的要素构成的有机整体。其核心内容可归纳为以下五个方面: 🏛️ 一、组织架构与职责 质量体系明确组织内各部门、岗位的职责与权限,形成层级清晰的管理网络…...
深度学习习题2
1.如果增加神经网络的宽度,精确度会增加到一个特定阈值后,便开始降低。造成这一现象的可能原因是什么? A、即使增加卷积核的数量,只有少部分的核会被用作预测 B、当卷积核数量增加时,神经网络的预测能力会降低 C、当卷…...
Python ROS2【机器人中间件框架】 简介
销量过万TEEIS德国护膝夏天用薄款 优惠券冠生园 百花蜂蜜428g 挤压瓶纯蜂蜜巨奇严选 鞋子除臭剂360ml 多芬身体磨砂膏280g健70%-75%酒精消毒棉片湿巾1418cm 80片/袋3袋大包清洁食品用消毒 优惠券AIMORNY52朵红玫瑰永生香皂花同城配送非鲜花七夕情人节生日礼物送女友 热卖妙洁棉…...
【分享】推荐一些办公小工具
1、PDF 在线转换 https://smallpdf.com/cn/pdf-tools 推荐理由:大部分的转换软件需要收费,要么功能不齐全,而开会员又用不了几次浪费钱,借用别人的又不安全。 这个网站它不需要登录或下载安装。而且提供的免费功能就能满足日常…...
动态 Web 开发技术入门篇
一、HTTP 协议核心 1.1 HTTP 基础 协议全称 :HyperText Transfer Protocol(超文本传输协议) 默认端口 :HTTP 使用 80 端口,HTTPS 使用 443 端口。 请求方法 : GET :用于获取资源,…...
uniapp 小程序 学习(一)
利用Hbuilder 创建项目 运行到内置浏览器看效果 下载微信小程序 安装到Hbuilder 下载地址 :开发者工具默认安装 设置服务端口号 在Hbuilder中设置微信小程序 配置 找到运行设置,将微信开发者工具放入到Hbuilder中, 打开后出现 如下 bug 解…...
Spring Boot + MyBatis 集成支付宝支付流程
Spring Boot MyBatis 集成支付宝支付流程 核心流程 商户系统生成订单调用支付宝创建预支付订单用户跳转支付宝完成支付支付宝异步通知支付结果商户处理支付结果更新订单状态支付宝同步跳转回商户页面 代码实现示例(电脑网站支付) 1. 添加依赖 <!…...
CSS3相关知识点
CSS3相关知识点 CSS3私有前缀私有前缀私有前缀存在的意义常见浏览器的私有前缀 CSS3基本语法CSS3 新增长度单位CSS3 新增颜色设置方式CSS3 新增选择器CSS3 新增盒模型相关属性box-sizing 怪异盒模型resize调整盒子大小box-shadow 盒子阴影opacity 不透明度 CSS3 新增背景属性ba…...
何谓AI编程【02】AI编程官网以优雅草星云智控为例建设实践-完善顶部-建立各项子页-调整排版-优雅草卓伊凡
何谓AI编程【02】AI编程官网以优雅草星云智控为例建设实践-完善顶部-建立各项子页-调整排版-优雅草卓伊凡 背景 我们以建设星云智控官网来做AI编程实践,很多人以为AI已经强大到不需要程序员了,其实不是,AI更加需要程序员,普通人…...
【java面试】微服务篇
【java面试】微服务篇 一、总体框架二、Springcloud(一)Springcloud五大组件(二)服务注册和发现1、Eureka2、Nacos (三)负载均衡1、Ribbon负载均衡流程2、Ribbon负载均衡策略3、自定义负载均衡策略4、总结 …...
