Redis用于全局ID生成器、分布式锁的解决方案
全局ID生成器
每个店铺都可以发布优惠卷
当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增id就存在一些问题:
1.id的规律性太明显
2.受单表数据量的限制
全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:
为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其他信息
ID的组成部分
1.符号位:1bit,永远为0;
2.时间戳:31bit,以秒为单位,可以使用69年
3.序列号:32bit,秒内的计数器,支持每秒产生2⋀32个不同的ID
RedisIdWorker
@Component
public class RedisIdWorker {/*** 开始时间戳*/private static final long Begin_TIMESTAMP = 1640995200L;/*** 序列号的位数*/private static final int COUNT_BITS = 32;private StringRedisTemplate stringRedisTemplate;public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}public long nextId(String keyPrefix) {//1.生成时间戳LocalDateTime now = LocalDateTime.now();long nowSecond = now.toEpochSecond(ZoneOffset.UTC);long timestamp = nowSecond - Begin_TIMESTAMP;//2.生成序列号//2.1获取当前日期。精确到天String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));//2.2自增长long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);//3.拼接并返回return timestamp << COUNT_BITS | count;}
}
private ExecutorService es = Executors.newFixedThreadPool(500);@Resourceprivate RedisIdWorker redisIdWorker;@Testvoid testIdWorker() throws InterruptedException {CountDownLatch latch = new CountDownLatch(300);Runnable task = () -> {for (int i = 0; i < 100; i++) {long id = redisIdWorker.nextId("order");System.out.println("id=" + id);}latch.countDown();};long begin = System.currentTimeMillis();for (int i = 0; i < 300; i++) {es.submit(task);}latch.await();long end = System.currentTimeMillis();System.out.println("time" + (end - begin));}
全局唯一ID生成策略
1.UUID
2.Redis自增
3.snowflake算法
4.数据库自增
Redis自增ID策略
1.每天一个key,方便统计订单量
2.ID构造是时间戳+计数器
实现优惠券秒杀下单
每个店铺都可以发布优惠券,分为平价劵。平价券可以任意购买,而特价券需要秒杀抢购
表关系如下:
tb_voucher:优惠券的基本信息,优惠金额,使用规则等
tb_seckill_vouvher:优惠券的库存、开始抢购时间,结束抢购时间。特价优惠券才需要填写这些信息
实现优惠券秒杀下单
在VoucherController实现了一个接口,可以实现添加秒杀优惠券、
@RestController
@RequestMapping("/voucher")
public class VoucherController {@Resourceprivate IVoucherService voucherService;/*** 新增普通券* @param voucher 优惠券信息* @return 优惠券id*/@PostMappingpublic Result addVoucher(@RequestBody Voucher voucher) {voucherService.save(voucher);return Result.ok(voucher.getId());}/*** 新增秒杀券* @param voucher 优惠券信息,包含秒杀信息* @return 优惠券id*/@PostMapping("seckill")public Result addSeckillVoucher(@RequestBody Voucher voucher) {voucherService.addSeckillVoucher(voucher);return Result.ok(voucher.getId());}
}
用户可以在这些店铺页面中抢购这些优惠券
下单时需要判断两点
1.秒杀是否开始或结束,如果尚未开始或已经结束则无法下单
2.库存是否充足,不足则无法下单
超卖问题
超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁
乐观锁
乐观锁的关键是判断之前查询得到的数据是否被修改过,常见的方式有两种
超卖总结
超卖这样的线程安全问题,解决方案有哪些
1.悲观锁:添加同步锁,让线程串行执行
-
优点:简单粗暴
-
缺点:性能一般
2.乐观锁:不加锁,在更新时判断是否有其他线程在修改
-
优点:性能好
-
存在成功率低的问题
一人一单
需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单
VoucherOrderServiceImpl
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Overridepublic Result seckillVoucher(Long voucherId) {//1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);//2.判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {//尚未开始return Result.fail("秒杀尚未开始");}//3.判断秒杀是否结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {//秒杀已经结束return Result.fail("秒杀已经结束");}//4.判断库存是否充足if (voucher.getStock() < 1) {//库存不足return Result.fail("库存不足");}Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()) {//获取代理对象IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);}}@Transactionalpublic Result createVoucherOrder(Long voucherId) {//5.一人一单Long userId = UserHolder.getUser().getId();//5.1查询订单Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();//5.2判断是否存在if (count>0) {//用户已经购买过了return Result.fail("用户已经购买过一次!");}//6.扣减库存boolean success = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).gt("stock",0).update();if (!success) {//扣减失败return Result.fail("库存不足");}//7.创建订单VoucherOrder voucherOrder = new VoucherOrder();//7.1订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);//7.2用户idLong userid = UserHolder.getUser().getId();voucherOrder.setUserId(userid);//7.3代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);//7.返回订单idreturn Result.ok(orderId);}
}
一人一单的并发安全问题
通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了
1.我们将服务启动两份,端口分别是8081和8082:
2.然后修改nginx的conf目录下的nginx.cong文件,配置反向代理和负载均衡:
现在用户节点会在这两个节点上负载均衡,再次测试下是否存在线程安全问题
分布式锁
什么是分布式锁
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁
分布式锁的实现
分布式锁的核心是实现多进程之间互斥,而满足这一节点的方式有很多种,常见的有三种:
基于Redis的分布式锁
实现分布式锁需要实现的两个基本方法:
1.获取锁:
-
互斥:确保只能有一个线程获取锁
-
非阻塞:尝试一次,成功返回true,失败返回false
2.释放锁
- 手动释放
基于Redis实现分布式锁的初级版本
ILock
SimpleRedisLock
public class SimpleRedisLock implements ILock {private String name;private StringRedisTemplate stringRedisTemplate;private static final String KEY_PREFIX = "lock:";public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean tryLock(long timeoutSec) {long threadId = Thread.currentThread().getId();//获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);}@Overridepublic void unlock() {//释放锁stringRedisTemplate.delete(KEY_PREFIX + name);}
}
基于Redis实现分布式锁的初级版本存在问题分析
1,业务阻塞超时或业务未执行完释放其他线程的锁
2.释放锁之前判断是否是该线程的锁
改进Redis的分布式锁
需求:修改之前的分布式锁实现,满足:
1.在获取锁时存入线程标识(可以用UUID表示)
2.在释放锁时先获取锁中的线程标识,判断是否与当前线程标识一致
-
如果一致则释放锁
-
如果不一致不释放锁
SimpleRedisLock
public class SimpleRedisLock implements ILock {private String name;private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}private static final String KEY_PREFIX = "lock:";private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";@Overridepublic boolean tryLock(long timeoutSec) {//获取线程标识String threadId = ID_PREFIX + Thread.currentThread().getId();//获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);}@Overridepublic void unlock() {//获取线程标识String threadId = ID_PREFIX + Thread.currentThread().getId();//获取锁中的标识String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);//判断标识是否一致if (threadId.equals(id)) {//释放锁stringRedisTemplate.delete(KEY_PREFIX + name);}}
}
改进Redis的分布式锁后存在问题分析
锁判断和锁释放不是原子性
Redis的Lua脚本
Redist提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:
https://www.runoob.com/lua/lua-tutorial.html
这里重点介绍Redis提供的调用函数,语法如下:
例如我们要执行set name jack,则脚本是这样:
例如,我们要先执行set name Rose,再执行get name,则脚本如下
写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:
例如,我们要执行redis.call(‘set’,‘name’,‘jack’)这个脚本,语法如下:
如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其他参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:
释放锁的业务流程是这样的:
1.获取锁中的线程标示
2.判断是否与指定的标示(当前线标示)一致
3.如果一致则释放锁(删除)
4.如果不一致则什么都不做
利用Lua脚本来表示则是这样的:
再次改进Redis的分布式锁
需求:基于Lua脚本实现分布式锁的释放逻辑
提示:RedisTemplate调用Lua脚本的API如下:
unlock.lua
-- 比较线程标示与锁中的标示是否一致
if (redis.call('get', 'KEYS[1]') == 'ARGV[1]') then-- 释放锁 del keyreturn redis.call('del', KEYS[1])
end
return 0
SimpleRedisLock
public class SimpleRedisLock implements ILock {private String name;private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}private static final String KEY_PREFIX = "lock:";private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;static {UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);}@Overridepublic boolean tryLock(long timeoutSec) {//获取线程标识String threadId = ID_PREFIX + Thread.currentThread().getId();//获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);}@Overridepublic void unlock() {//调用lua脚本stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX + name),ID_PREFIX + Thread.currentThread().getId());}
}
基于Redis实现的分布式锁总结
1.基于Redis的分布式锁实现思路:
-
利用set nx ex获取锁,并设置过期时间,保存线程标示
-
释放锁时先判断线程标示是否与自己一致,一致则删除锁
2.特性:
-
利用set nx满足互斥性
-
利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
-
利用Redis集群保证高可用和高并发性
基于Redis的分布式锁优化
基于setnx实现的分布式锁存在下面问题:
Redisson
Redisson是一个在Redis的基础上实现java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现
官网地址:https://redisson.org
GitHub地址:https://github.com/redisson/redisson
Redisson入门
1.引入依赖
2.配置Redisson客户端
3.使用Redisson的分布式锁
Redisson可重入锁原理
获取锁的Lua脚本
释放锁的Lua脚本
Redisson分布式锁原理
1.可重入:利用hash结构记录线程id和重入次数
2.可重试:利用信号量和PubSub和功能实现等待、唤醒、获取锁失败的重试机制
3.超时续约:利用watchDog,每隔一段时间,重置超时时间
Redisson分布式锁主从一致性问题
分布式锁总结
1.不可重入Redis分布式锁:
-
原理:利用setnx的互斥性,利用ex避免死锁,释放锁时,判断线程标示
-
缺陷:不可重入、无法重试,锁超时失效
2.可重入的Redis分布式锁
-
原理:利用hash结构,记录线程和重入次数,利用watchDog延续时间,利用信号量控制锁重试等待
-
缺陷:redis宕机引起锁失效问题
3.Redisson的multiLock
-
原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功
-
缺陷:运维成本高,失效复杂
视频地址
相关文章:

Redis用于全局ID生成器、分布式锁的解决方案
全局ID生成器 每个店铺都可以发布优惠卷 当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增id就存在一些问题: 1.id的规律性太明显 2.受单表数据量的限制 全局ID生成器,是一种在分布式系…...

OpenTex 企业内容管理平台
OpenText 企业内容管理平台 将内容服务与领先应用程序集成,弥合内容孤岛、加快信息流并扩大治理 什么是内容服务集成? 内容服务集成通过将内容管理平台与处于流程核心的独立应用程序和系统连接起来,支持并扩展了 ECM 的传统优势。 最好的内…...

【0基础学爬虫】爬虫基础之数据存储
大数据时代,各行各业对数据采集的需求日益增多,网络爬虫的运用也更为广泛,越来越多的人开始学习网络爬虫这项技术,K哥爬虫此前已经推出不少爬虫进阶、逆向相关文章,为实现从易到难全方位覆盖,特设【0基础学…...

Redis与本地缓存组合使用(IT枫斗者)
Redis与本地缓存组合使用 前言 我们开发中经常用到Redis作为缓存,将高频数据放在Redis中能够提高业务性能,降低MySQL等关系型数据库压力,甚至一些系统使用Redis进行数据持久化,Redis松散的文档结构非常适合业务系统开发…...

手把手教你学习IEC104协议和编程实现 十 故障事件与复位进程
故障事件 目的 在IEC104普遍应用之前,据我了解多个协议,再综合自动化协议中,有这么一个概念叫“事故追忆”,意思是当变电站出现事故的时候,不但要记录事故的时间,还需记录事故前后模拟量的数据,从而能从一定程度上分析事故产生的原因,这个模拟量就是和今天讲解的故障…...

浅析分布式理论的CAP
大家好,我是易安! 今天让我们来聚焦于分布式系统架构中的重要理论——CAP理论。在分布式系统中,可用性和数据一致性是两个至关重要的因素,而CAP理论就是在这两者之间提供了一种权衡的原则,帮助我们在设计分布式系统时进…...

使用 TensorFlow 构建机器学习项目:6~10
原文:Building Machine Learning Projects with TensorFlow 协议:CC BY-NC-SA 4.0 译者:飞龙 本文来自【ApacheCN 深度学习 译文集】,采用译后编辑(MTPE)流程来尽可能提升效率。 不要担心自己的形象&#x…...

使用 LXCFS 文件系统实现容器资源可见性
使用 LXCFS 文件系统实现容器资源可见性一、基本介绍二、LXCFS 安装与使用1.安装 LXCFS 文件系统2.基于 Docker 实现容器资源可见性3.基于 Kubernetes 实现容器资源可见性前言:Linux 利用 Cgroup 实现了对容器资源的限制,但是当在容器内运行 top 命令时就…...

SQL LIMIT
SQL LIMIT SQL LIMIT子句简介 要检索查询返回的行的一部分,请使用LIMIT和OFFSET子句。 以下说明了这些子句的语法: SELECT column_list FROMtable1 ORDER BY column_list LIMIT row_count OFFSET offset;在这个语法中, row_count确定将返…...

OpenCV实战之人脸美颜美型(六)——磨皮
1.需求分析 有个词叫做“肤若凝脂”,直译为皮肤像凝固的油脂,形容皮肤洁白且光润,这是对美女的一种通用评价。实际生活中我们的皮肤多少会有一些毛孔、斑点等表现,在观感上与上述的“光润感”相反,因此磨皮也成为美颜算法中的一项基础且重要的功能。让皮肤变得更加光润,就…...

Java技术栈—重装系统后不重新安装也能正常使用的设置方式
声明: 最近在重装电脑,重装完后,开发工具会有些功能使用不了,在这做个记录!这里是 JAVA 技术栈 问题描述: git 右键无菜单 111 git git 右键无菜单 参考文章:注册表修复git右键无菜单 git …...

智驾升级!ADB+AFS「起势」
目前,乘用车前大灯已经完成从传统卤素、氙气到LED的转型升级,高工智能汽车研究院监测数据显示,2022年中国市场(不含进出口)乘用车前装标配LED前大灯搭载率达到75.99%,同比2021年提高约7个百分点。 而相比而…...

算法记录 | Day27 回溯算法
39.组合总和 思路: 1.确定回溯函数参数:定义全局遍历存放res集合和单个path,还需要 candidates数组 targetSum(int)目标和。 startIndex(int)为下一层for循环搜索的起始位置。 2.终止条件…...

性能测试总结-根据工作经验总结还比较全面
性能测试总结性能测试理论性能测试的策略基准测试负载测试稳定性测试压力测试并发测试性能测试的指标响应时间并发数吞吐量资源指标性能测试流程性能测试工具JMeter基本使用元件构成线程组jmeter的分布式使用jmeter测试报告常用插件性能测试的计算1.根据请求数明细数据计算满足…...

类型断言[as语法 | <> 语法
TypeScript中的类型断言[as语法 | <> 语法] https://huaweicloud.csdn.net/638f0fbbdacf622b8df8e283.html?spm1001.2101.3001.6650.1&utm_mediumdistribute.pc_relevant.none-task-blog-2~default~CTRLIST~activity-1-107633405-blog-122438115.2…...
barret reduction原理详解及硬件优化
背景介绍 约减算法,通常应用在硬件领域,因为模运算mod是一个除法运算,在硬件中实现速度会比乘法慢的多,并且还会占用大量资源,因此需要想办法用乘法及其它简单运算来替代模运算。模约减算法可以利用乘法、加法和移位等…...

NLP / LLMs中的Temperature 是什么?
ChatGPT, GPT-3, GPT-3.5, GPT-4, LLaMA, Bard等大型语言模型的一个重要的超参数 大型语言模型能够根据给定的上下文或提示生成新文本,由于神经网络等深度学习技术的进步,这些模型越来越受欢迎。可用于控制生成语言模型行为的关键参数之一是Temperature …...

c#快速入门~在java基础上,知道C#和JAVA 的不同即可
☺ 观看下文前提:如果你的主语言是java,现在想再学一门新语言C#,下文是在java基础上,对比和java的不同,快速上手C#,当然不是说学C#的前提是需要java,而是下文是从主语言是java的情况下ÿ…...

nginx--基本配置
目录 1.安装目录 2.文件详解 2.编译参数 3.Nginx基本配置语法 1./etc/nginx/nginx.conf 2./etc/nginx/conf.d/default.conf 3.启动重启命令 4.设置404跳转页面 1./etc/nginx/conf.d/default.conf修改 2. 重启 5.最前面内容模块 6.事件模块 1.安装目录 # etc cd …...

R语言中apply系列函数详解
文章目录applylapply, sapply, vapplyrapplytapplymapplyR语言系列: 编程基础💎循环语句💎向量、矩阵和数组💎列表、数据帧排序函数💎apply系列函数 R语言的循环效率并不高,所以并不推荐循环以及循环嵌套…...

红黑树探险:从理论到实践,一站式掌握C++红黑树
红黑树揭秘:从理论到实践,一站式掌握C红黑树引言为什么需要了解红黑树?红黑树在现代C编程中的应用场景树与平衡二叉搜索树树的基本概念:二叉搜索树的定义与性质:平衡二叉搜索树的特点与需求:红黑树基础红黑…...

CDH6.3.2大数据集群生产环境安装(七)之PHOENIX组件安装
添加phoenix组件 27.1. 准备安装资源包 27.2. 拷贝资源包到相应位置 拷贝PHOENIX-1.0.jar到/opt/cloudera/csd/ 拷贝PHOENIX-5.0.0-cdh6.2.0.p0.1308267-el7.parcel.sha、PHOENIX-5.0.0-cdh6.2.0.p0.1308267-el7.parcel到/opt/cloudera/parcel-repo 27.3. 进入cm页面进行分发、…...

【C++要笑着学】搜索二叉树 (SBTree) | K 模型 | KV 模型
C 表情包趣味教程 👉 《C要笑着学》 💭 写在前面:半年没更 C 专栏了,上一次更新还是去年九月份,被朋友催更很久了hhh 本章倒回数据结构专栏去讲解搜索二叉树,主要原因是讲解 map 和 set 的特性需要二叉搜索…...

微信小程序开发 | 小程序开发框架
小程序开发框架7.1 小程序模块化开发7.1.1 模块7.1.2 模板7.1.3 自定义组件7.1.4插件7.2 小程序基础样式库—WeUI7.2.1 初识WeUI7.2.2【案例】电影信息展示7.3 使用vue.js开发小程序7.3.1 初识mpvue7.3.2 开发工具7.3.3 项目结构7.3.4【案例】计数器7.4 小程序组件化开发框架7.…...

气候系统设计
基础概念 一个星球(例如地球)的气候系统主要是一些基本参数基于公转周期(年)和自转周期(日)的变化,这其中会有两个变化因素:地理位置(经纬度)和天气变化&…...

如何使用Thymeleaf给web项目中的网页渲染显示动态数据?
编译软件:IntelliJ IDEA 2019.2.4 x64 操作系统:win10 x64 位 家庭版 服务器软件:apache-tomcat-8.5.27 目录一. 什么是Thymeleaf?二. MVC2.1 为什么需要MVC?2.2 MVC是什么?2.3 MVC和三层架构之间的关系及工…...

01 | 电机常用语
1 电机常用术语 1.1 原点 原点是指步进电机在驱动直线运动机构时的起始点。 1.2 点动 点动是电动机控制方式中的一种。 点动由于在这一控制回路中没有自保,也没有并接其它的自动装置,只是按下控制回路的启动按钮,主回路才通电,松开启动按钮,主回路就没电了。最典型的是…...

Leetcode.2601 质数减法运算
题目链接 Leetcode.2601 质数减法运算 Rating : 1779 题目描述 给你一个下标从 0 开始的整数数组 nums,数组长度为 n 。 你可以执行无限次下述运算: 选择一个之前未选过的下标 i ,并选择一个 严格小于 nums[i]的质数 ppp &…...

DP7416国产192K数字音频接收芯片兼容替代CS8416
目录192K 数字音频应用DP7416简介芯片特性192K 数字音频应用 采样率192khz,能将192,000hz以下的频率都录下来,而且对声波每秒连续采样192,000次。在回放的时候,这192,000个采样点按顺序播放,从而还原原来的声音。 过采样技术除…...

全球土壤湿度数据获取方法
土壤湿度亦称土壤含水率,表示土壤干湿程度的物理量。是土壤含水量的一种相对变量。通常用土壤含水量占干土重的百分数是示,亦称土壤质量湿度,如用土壤水分容积占土壤总容积的百分数表示,则称土壤容积湿度。通常说的土壤湿度&#…...