【分布式锁】04-使用Redisson实现ReadWriteLock原理
前言
关于读写锁,大家应该都了解JDK中的ReadWriteLock
, 当然Redisson也有读写锁的实现。
所谓读写锁,就是多个客户端同时加读锁,是不会互斥的,多个客户端可以同时加这个读锁,读锁和读锁是不互斥的
Redisson中使用RedissonReadWriteLock
来实现读写锁,它是RReadWriteLock
的子类,具体实现读写锁的类分别是:RedissonReadLock
和RedissonWriteLock
Redisson读写锁使用例子
还是从官方文档中找的使用案例:
RReadWriteLock rwlock = redisson.getReadWriteLock("tryLock");
RLock lock = rwlock.readLock();
// or
RLock lock = rwlock.writeLock();
// traditional lock method
lock.lock();
// or acquire lock and automatically unlock it after 10 seconds
lock.lock(10, TimeUnit.SECONDS);
// or wait for lock aquisition up to 100 seconds
// and automatically unlock it after 10 seconds
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
Redisson加读锁逻辑原理
public class RedissonReadLock extends RedissonLock implements RLock {
@Override
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"local mode = redis.call('hget', KEYS[1], 'mode'); " +
"if (mode == false) then " +
"redis.call('hset', KEYS[1], 'mode', 'read'); " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('set', KEYS[2] .. ':1', 1); " +
"redis.call('pexpire', KEYS[2] .. ':1', ARGV[1]); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (mode == 'read') or (mode == 'write' and redis.call('hexists', KEYS[1], ARGV[3]) == 1) then " +
"local ind = redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"local key = KEYS[2] .. ':' .. ind;" +
"redis.call('set', key, 1); " +
"redis.call('pexpire', key, ARGV[1]); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end;" +
"return redis.call('pttl', KEYS[1]);",
Arrays.<Object>asList(getName(), getReadWriteTimeoutNamePrefix(threadId)),
internalLockLeaseTime, getLockName(threadId), getWriteLockName(threadId));
}
}
客户端A(UUID_01:threadId_01)来加读锁
注:
以下文章中客户端A用:UUID_01:threadId_01标识
客户端B用:UUID_02:threadId_02标识
KEYS:
- KEYS1:
getName()
= tryLock - KEYS[2]:
getReadWriteTimeoutNamePrefix(threadId)
= {anyLock}:UUID_01:threadId_01:rwlock_timeout
ARGV:
- ARGV1: internalLockLeaseTime = 30000毫秒
- ARGV[2]: getLockName(threadId) = UUID_01:threadId_01
- ARGV[3]: getWriteLockName(threadId) = UUID_01:threadId_01:write
接着对代码中lua脚本一行行解读:
- hget anyLock mode 第一次加锁时是空的
- mode = false,进入if逻辑
- hset anyLock UUID_01:threadId_01 1
anyLock是hash结构,设置hash的key、value - set {anyLock}:UUID_01:threadId_01:rwlock_timeout:1 1
设置一个string类型的key value数据 - pexpire {anyLock}:UUID_01:threadId_01:rwlock_timeout:1 30000
设置key value的过期时间 - pexpire anyLock 30000
设置anyLock的过期时间
此时redis中存在的数据结构为:
anyLock: {
"mode": "read",
"UUID_01:threadId_01": 1
}
{anyLock}:UUID_01:threadId_01:rwlock_timeout:1 1
客户端A 第二次来加读锁
继续分析,客户端A已经加过读锁,此时如果继续加读锁会怎样处理呢?
- hget anyLock mode 此时mode=read,会进入第二个if判断
- hincrby anyLock UUID_01:threadId_01 1 此时hash中的value会加1,变成2
- set {anyLock}:UUID_01:threadId_01:rwlock_timeout:2 1
ind 为hincrby结果,hincrby返回是2 - pexpire anyLock 30000
- pexpire {anyLock}:UUID_01:threadId_01:rwlock_timeout:2 30000
此时redis中存在的数据结构为:
anyLock: {
“mode”: “read”,
“UUID_01:threadId_01”: 2
}
{anyLock}:UUID_01:threadId_01:rwlock_timeout:1 1
{anyLock}:UUID_01:threadId_01:rwlock_timeout:2 1
客户端B (UUID_02:threadId_02)第一次来加读锁
基本步骤和上面一直,加锁后redis中数据为:
anyLock: {
"mode": "read",
"UUID_01:threadId_01": 2,
"UUID_02:threadId_02": 1
}
{anyLock}:UUID_01:threadId_01:rwlock_timeout:1 1
{anyLock}:UUID_01:threadId_01:rwlock_timeout:2 1
{anyLock}:UUID_02:threadId_02:rwlock_timeout:1 1
这里需要注意一下:
为哈希表 key 中的域 field 的值加上增量 increment,如果 key 不存在,一个新的哈希表被创建并执行 HINCRBY 命令。
Redisson加写锁逻辑原理
Redisson中由RedissonWriteLock
来实现写锁,我们看下写锁的核心逻辑:
public class RedissonWriteLock extends RedissonLock implements RLock {
@Override
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"local mode = redis.call('hget', KEYS[1], 'mode'); " +
"if (mode == false) then " +
"redis.call('hset', KEYS[1], 'mode', 'write'); " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (mode == 'write') then " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"local currentExpire = redis.call('pttl', KEYS[1]); " +
"redis.call('pexpire', KEYS[1], currentExpire + ARGV[1]); " +
"return nil; " +
"end; " +
"end;" +
"return redis.call('pttl', KEYS[1]);",
Arrays.<Object>asList(getName()),
internalLockLeaseTime, getLockName(threadId));
}
}
还是像上面一样,一行行来分析每句lua脚本执行语义。
客户端A先加读写、再加写锁
KEYS和ARGV参数:
- hget anyLock mode,此时没人加锁,mode=false
- hset anyLock mode write
- hset anyLock UUID_01:threadId_01:write 1
- pexpire anyLock 30000
此时redis中数据格式为:
anyLock: {
"mode": "write",
"UUID_01:threadId_01:write": 1
}
此时再次来加写锁,直接到另一个if语句中:
- hexists anyLock UUID_01:threadId_01:write
- hincrby anyLock UUID_01:threadId_01:write 1
- pexpire anyLock pttl + 30000
此时redis中数据格式为:
anyLock: {
"mode": "write",
"UUID_01:threadId_01:write": 2
}
客户端A和客户端B,先后加读锁,客户端C来加写锁
读锁加完后,此时redis数据格式为:
anyLock: {
"mode": "read",
"UUID_01:threadId_01": 1,
"UUID_02:threadId_02": 1
}
{anyLock}:UUID_01:threadId_01:rwlock_timeout:1 1
{anyLock}:UUID_02:threadId_02:rwlock_timeout:1 1
客户端C参数为:
hget anyLock mode,mode = read,已经有人加了读锁,不是写锁,此时会直接执行:pttl
anyLock,返回一个anyLock的剩余生存时间
- hget anyLock mode,mode = read,已经有人加了读锁,不是写锁,所以if语句不会成立
- pttl anyLock,返回一个anyLock的剩余生存时间
客户端C加锁失败,就会不断的尝试重试去加锁
客户端A先加写锁、客户端B接着加读锁
加完写锁后此时Redis数据格式为:
anyLock: {
"mode": "write",
"UUID_01:threadId_01:write": 1
}
客户端B执行读锁逻辑参数为:
- KEYS1 = anyLock
- KEYS[2] = {anyLock}:UUID_02:threadId_02:rwlock_timeout
- ARGV1 = 30000毫秒
- ARGV[2] = UUID_02:threadId_02
- ARGV[3] = UUID_02:threadId_02:write
接着看下加锁逻辑:
image.png
如上图,客户端B加读锁会走到红框中的if逻辑:
- hget anyLock mode,mode = write
客户端A已经加了一个写锁 - hexists anyLock UUID_02:threadId_02:write,存在的话,如果客户端B自己之前加过写锁的话,此时才能进入这个分支
- 返回pttl anyLock,导致加锁失败
客户端A先加写锁、客户端A接着加读锁
还是接着上面的逻辑,继续分析:
- hget anyLock mode,mode = write
客户端A已经加了一个写锁 - hexists anyLock UUID_01:threadId_01:write,此时存在这个key,所以可以进入if分支
- hincrby anyLock UUID_01:threadId_01 1,也就是说此时,加了一个读锁
- set {anyLock}:UUID_01:threadId_01:rwlock_timeout:1 1,
- pexpire anyLock 30000
- pexpire {anyLock}:UUID_01:threadId_01:rwlock_timeout:1 30000
此时redis中数据格式为:
anyLock: {
"mode": "write",
"UUID_01:threadId_01:write": 1,
"UUID_01:threadId_01": 1
}
{anyLock}:UUID_01:threadId_01:rwlock_timeout:1 1
客户端A先加读锁、客户端A接着加写锁
客户端A加读锁后,redis中数据结构为:
anyLock: {
"mode": "read",
"UUID_01:threadId_01": 1
}
{anyLock}:UUID_01:threadId_01:rwlock_timeout:1 1
此时客户端A再来加写锁,逻辑如下:
image.png
此时客户端A先加的读锁,mode=read,所以再次加写锁是不能成功的
如果是同一个客户端同一个线程,先加了一次写锁,然后加读锁,是可以加成功的,默认是在同一个线程写锁的期间,可以多次加读锁
而同一个客户端同一个线程,先加了一次读锁,是不允许再被加写锁的
总结
显然还有写锁与写锁互斥的逻辑就不分析了,通过上面一些场景的分析,我们可以知道:
- 读锁与读锁非互斥
- 读锁与写锁互斥
- 写锁与写锁互斥
- 读读、写写 同个客户端同个线程都可重入
- 先写锁再加读锁可重入
- 先读锁再写锁不可重入
Redisson读写锁释放原理
Redission 读锁释放原理
不同客户端加了读锁 / 同一个客户端+线程多次可重入加了读锁
例如客户端A先加读锁,然后再次加读锁
最后客户端B来加读锁
此时Redis中数据格式为:
anyLock: {
"mode": "read",
"UUID_01:threadId_01": 2,
"UUID_02:threadId_02": 1
}
{anyLock}:UUID_01:threadId_01:rwlock_timeout:1 1
{anyLock}:UUID_01:threadId_01:rwlock_timeout:2 1
{anyLock}:UUID_02:threadId_02:rwlock_timeout:1 1
接着我们看下释放锁的核心代码:
public class RedissonReadLock extends RedissonLock implements RLock {
@Override
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
String timeoutPrefix = getReadWriteTimeoutNamePrefix(threadId);
String keyPrefix = getKeyPrefix(threadId, timeoutPrefix);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"local mode = redis.call('hget', KEYS[1], 'mode'); " +
"if (mode == false) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end; " +
"local lockExists = redis.call('hexists', KEYS[1], ARGV[2]); " +
"if (lockExists == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); " +
"if (counter == 0) then " +
"redis.call('hdel', KEYS[1], ARGV[2]); " +
"end;" +
"redis.call('del', KEYS[3] .. ':' .. (counter+1)); " +
"if (redis.call('hlen', KEYS[1]) > 1) then " +
"local maxRemainTime = -3; " +
"local keys = redis.call('hkeys', KEYS[1]); " +
"for n, key in ipairs(keys) do " +
"counter = tonumber(redis.call('hget', KEYS[1], key)); " +
"if type(counter) == 'number' then " +
"for i=counter, 1, -1 do " +
"local remainTime = redis.call('pttl', KEYS[4] .. ':' .. key .. ':rwlock_timeout:' .. i); " +
"maxRemainTime = math.max(remainTime, maxRemainTime);" +
"end; " +
"end; " +
"end; " +
"if maxRemainTime > 0 then " +
"redis.call('pexpire', KEYS[1], maxRemainTime); " +
"return 0; " +
"end;" +
"if mode == 'write' then " +
"return 0;" +
"end; " +
"end; " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; ",
Arrays.<Object>asList(getName(), getChannelName(), timeoutPrefix, keyPrefix),
LockPubSub.unlockMessage, getLockName(threadId));
}
}
客户端A来释放锁:
对应的KEYS和ARGV参数为:
KEYS1 = anyLock
KEYS[2] = redisson_rwlock:{anyLock}
KEYS[3] = {anyLock}:UUID_01:threadId_01:rwlock_timeout
KEYS[4] = {anyLock}
ARGV1 = 0
ARGV[2] = UUID_01:threadId_01
接下来开始执行操作:
- hget anyLock mode,mode = read
- hexists anyLock UUID_01:threadId_01,肯定是存在的,因为这个客户端A加过读锁
- hincrby anyLock UUID_01:threadId_01 -1,将这个客户端对应的加锁次数递减1,现在就是变成1,counter = 1
- del {anyLock}:UUID_01:threadId_01:rwlock_timeout:2,删除了一个timeout key
此时Redis中的数据结构为:
anyLock: {
"mode": "read",
"UUID_01:threadId_01": 1,
"UUID_02:threadId_02": 1
}
{anyLock}:UUID_01:threadId_01:rwlock_timeout:1 1
{anyLock}:UUID_02:threadId_02:rwlock_timeout:1 1
此时继续往下,具体逻辑如图:
image.png
- hlen anyLock > 1,就是hash里面的元素超过1个
- pttl {anyLock}:UUID_01:threadId_01:rwlock_timeout:1,此时获取那个timeout key的剩余生存时间还有多少毫秒,比如说此时这个key的剩余生存时间是20000毫秒
这个for循环的含义是获取到了所有的timeout key的最大的一个剩余生存时间,假设最大的剩余生存时间是25000毫秒
客户端A继续来释放锁:
此时客户端A执行流程还会和上面一直,执行完成后Redis中数据结构为:
anyLock: {
"mode": "read",
"UUID_02:threadId_02": 1
}
{anyLock}:UUID_02:threadId_02:rwlock_timeout:1 1
因为这里会走counter == 0
的逻辑,所以会执行"redis.call('hdel', KEYS[1], ARGV[2]); "
客户端B继续来释放锁:
客户端B流程也和上面一直,执行完后就会删除anyLock这个key
同一个客户端/线程先加写锁再加读锁
上面已经分析过这种情形,操作过后Redis中数据结构为:
anyLock: {
"mode": "write",
"UUID_01:threadId_01:write": 1,
"UUID_01:threadId_01": 1
}
{anyLock}:UUID_01:threadId_01:rwlock_timeout:1 1
此时客户端A来释放读锁:
- hincrby anyLock UUID_01:threadId_01 -1,将这个客户端对应的加锁次数递减1,现在就是变成1,counter = 0
- hdel anyLock UUID_01:threadId_01,此时就是从hash数据结构中删除客户端A这个加锁的记录
- del {anyLock}:UUID_01:threadId_01:rwlock_timeout:1,删除了一个timeout key
此时Redis中数据变成:
anyLock: {
"mode": "write",
"UUID_01:threadId_01:write": 1
}
Redisson写锁释放原理
先看下写锁释放的核心逻辑:
public class RedissonWriteLock extends RedissonLock implements RLock {
@Override
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"local mode = redis.call('hget', KEYS[1], 'mode'); " +
"if (mode == false) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
"if (mode == 'write') then " +
"local lockExists = redis.call('hexists', KEYS[1], ARGV[3]); " +
"if (lockExists == 0) then " +
"return nil;" +
"else " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call('hdel', KEYS[1], ARGV[3]); " +
"if (redis.call('hlen', KEYS[1]) == 1) then " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"else " +
// has unlocked read-locks
"redis.call('hset', KEYS[1], 'mode', 'read'); " +
"end; " +
"return 1; "+
"end; " +
"end; " +
"end; "
+ "return nil;",
Arrays.<Object>asList(getName(), getChannelName()),
LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
}
}
同一个客户端多次可重入加写锁 / 同一个客户端先加写锁再加读锁
客户端A加两次写锁释放:
此时Redis中数据为:
anyLock: {
"mode": "write",
"UUID_01:threadId_01:write": 2,
"UUID_01:threadId_01": 1
}
{anyLock}:UUID_01:threadId_01:rwlock_timeout:1 1
客户端A来释放锁KEYS和ARGV参数:
KEYS1 = anyLock
KEYS[2] = redisson_rwlock:{anyLock}
ARGV1 = 0
ARGV[2] = 30000
ARGV[3] = UUID_01:threadId_01:write
直接分析lua代码:
- 上面mode=write,后面使用hincrby进行-1操作,此时count=1
- 如果count>0,此时使用pexpire然后返回0
- 此时客户端A再来释放写锁,count=0
- hdel anyLock UUID_01:threadId_01:write
此时Redis中数据:
anyLock: {
"mode": "write",
"UUID_01:threadId_01": 1
}
{anyLock}:UUID_01:threadId_01:rwlock_timeout:1 1
后续还会接着判断,如果count=0,代表写锁都已经释放完了,此时hlen如果>1,代表加的还有读锁,所以接着执行:hset anyLock mode read
, 将写锁转换为读锁
最终Redis数据为:
anyLock: {
"mode": "read",
"UUID_01:threadId_01": 1
}
{anyLock}:UUID_01:threadId_01:rwlock_timeout:1 1
总结
Redisson陆续也更新了好几篇了,疫情期间宅在家里一直学习Redisson相关内容,这篇文章写了2天,从早到晚。
读写锁这块内容真的很多,本篇篇幅很长,如果学习本篇文章最好跟着源码一起读,后续还会继续更新Redisson相关内容,如有不正确的地方,欢迎指正!
申明
本文章首发自本人博客:https://www.cnblogs.com/wang-meng 和公众号:壹枝花算不算浪漫,如若转载请标明来源!
感兴趣的小伙伴可关注个人公众号:壹枝花算不算浪漫
【分布式锁】04-使用Redisson实现ReadWriteLock原理的更多相关文章
- [转帖]分布式锁-redLock And Redisson
分布式锁-redLock And Redisson 2019-03-01 16:51:48 淹不死的水 阅读数 372更多 分类专栏: 分布式锁 版权声明:本文为博主原创文章,遵循CC 4.0 B ...
- 基于redis的分布式锁实现方案--redisson
实例代码地址,请前往:https://gitee.com/GuoqingLee/distributed-seckill redis官方文档地址,请前往:http://www.redis.cn/topi ...
- Redis分布式锁的正确使用与实现原理
模拟一个电商里面下单减库存的场景. 1.首先在redis里加入商品库存数量. 2.新建一个Spring Boot项目,在pom里面引入相关的依赖. <dependency> <gro ...
- 来吧,展示!Redis的分布式锁及其实现Redisson的全过程
前言 分布式锁是控制分布式系统之间同步访问共享资源的一种方式. 在分布式系统中,常常需要协调他们的动作.如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要 ...
- Springboot分别使用乐观锁和分布式锁(基于redisson)完成高并发防超卖
原文 :https://blog.csdn.net/tianyaleixiaowu/article/details/90036180 乐观锁 乐观锁就是在修改时,带上version版本号.这样如果试图 ...
- 项目中用到了Redis分布式锁,了解一下背后的原理
前言 以前在学校做小项目的时候,用到Redis,基本也只是用来当作缓存.现在博主在某金融平台实习,发现Redis在生产中并不只是当作缓存这么简单.在我接触到的项目中,Redis起到了一个分布式锁的作用 ...
- Redisson实现Redis分布式锁的底层原理
一.写在前面 现在面试,一般都会聊聊分布式系统这块的东西.通常面试官都会从服务框架(Spring Cloud.Dubbo)聊起,一路聊到分布式事务.分布式锁.ZooKeeper等知识.所以咱们这篇文章 ...
- Redisson 实现分布式锁的原理分析
写在前面 在了解分布式锁具体实现方案之前,我们应该先思考一下使用分布式锁必须要考虑的一些问题. 互斥性:在任意时刻,只能有一个进程持有锁. 防死锁:即使有一个进程在持有锁的期间崩溃而未能主动释放锁, ...
- Redisson 实现分布式锁原理分析
Redisson 实现分布式锁原理分析 写在前面 在了解分布式锁具体实现方案之前,我们应该先思考一下使用分布式锁必须要考虑的一些问题. 互斥性:在任意时刻,只能有一个进程持有锁. 防死锁:即使有 ...
随机推荐
- Esp8266和HomeKit
Summary 没有找到合适的简单解决方案,将Esp8266控制的设备连接到HomeKit.所以参照EspEasy实现 HomeKit和Esp8266连接. 连接方式: Raspberry Zero ...
- php--0与空的判断
使用empty()函数判断,两者都是true $a=0; if(trim($a)=="") { echo '数字0'; }
- 吴裕雄--python编程:CGI编程
什么是CGI CGI 目前由NCSA维护,NCSA定义CGI如下: CGI(Common Gateway Interface),通用网关接口,它是一段程序,运行在服务器上如:HTTP服务器,提供同客户 ...
- MUI 提问框多个按钮的回调函数
var btns = new Array("按钮1", "按钮2"); mui.confirm("这是信息", "这是标题&quo ...
- python标准库:csv 模块
原文地址:http://www.bugingcode.com/blog/python_csv.html csv 模块被用来读取CSV格式(用逗号分割数值)的数据文件,CSV格式的文件经常在微软的Exc ...
- IDEA工具java.io.IOException: Could not find resource SqlMapConfig.xml
IDEA工具java.io.IOException: Could not find resource SqlMapConfig.xml 解决办法: 1.删掉pom.xml文件的这行代码 <pac ...
- Python---12函数式编程------12.3匿名函数&装饰器&偏函数
一.匿名函数 当我们在传入函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便. 在Python中,对匿名函数提供了有限支持.还是以map()函数为例,计算f(x)=x2时,除了定义一个f( ...
- 【最简单的vim教程】vim学习笔记-基础操作
说明 C-字母 = Ctrl + 字母 char = 任意字符 开始编辑 insert 按键 功能 说明 i(I) insert 当前位置插入(当前行前) a(A) append 当前字符后面插入(当 ...
- Flutter调研(1)-Flutter基础知识
工作需要,因客户端有部分页面要使用flutter编写,需要QA了解一下flutter相关知识,因此,做了flutter调研,包含安装,基础知识与demo编写,第二部分是安装与环境配置. —— Flut ...
- 脚本化处理linux云服务器第二硬盘初始化
#!/usr/bin/bash # 可以带参数 method=$ size=$ mydir=$ [ $#== ]&&{ echo -e "Missing parameter! ...