最近作一个项目,相似某度云盘,另外附加定制功能,本人负责云盘相关功能实现,这个项目跟云盘不一样的是,以项目为分配权限的单位,同一个项目及子目录全部有权限的用户能够同时操做全部文件,这样就很容易出现并发操做,并且表结构设计的时候,定下来文件和文件夹都有个path字段,存储的是所在父级文件夹路径,这样检索方便,重命名和移动比较麻烦。java
以下,例如甲同窗正在移动项目下C文件夹,而此时乙同窗也在操做项目下D下的d.txt文件,这样就会出现问题,因此须要分布式锁控制,甲在操做C文件夹的时候,C文件夹全部子文件和包含C文件夹的父文件夹都被锁住,如图将会被锁定的文件夹和子文件有:A、C、c.txt、D、E、d.txt,其中a.txt和B未被锁定,这个是移动的状况,以下表格列出其余状况.redis
操做对象 |
操做 |
新建 | 上传 | 移动文件夹 | 移动文件 | 复制文件夹 | 复制文件 | 重命名文件夹 | 重命名文件 | 删除文件夹 | 删除文件 | 回收站清除 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
/A |
新建 |
√ |
√ |
× |
√ |
× |
√ |
× |
√ |
× |
√ |
/ |
/A |
上传 |
√ |
√ |
× |
√ |
× |
√ |
× |
√ |
× |
√ |
/ |
/A->/B |
移动文件夹 |
A×B× | A×B× | A×B× |
A×B× |
A√B√ |
A√B√ |
A×B× |
A×B√ |
A×B× |
A×B√ |
/ |
a.txt->/B | 移动文件 |
B√ |
B√ |
B× |
B√ |
B× |
a√B× |
a×B× |
a×B√ |
B× |
a×B√ |
/ |
/A->/B |
复制文件夹 |
B√ |
B√ |
B× |
B√ |
B√ |
B√ |
B× |
B√ |
B× |
B√ |
/ |
a.txt->/B | 复制文件 |
B√ |
B√ |
B× |
B√ |
B√ |
B√ |
B× |
B√ |
B× |
B√ |
/ |
/A |
重命名文件夹 | A× |
A× |
A× |
A× |
A√ |
A√ |
A× |
A× |
A× |
A× |
/ |
a.txt |
重命名文件 |
/ |
/ |
× |
× |
× |
× |
√ |
× |
× |
× |
/ |
/A |
删除文件夹 |
× |
× |
× |
× |
× |
× |
× |
× |
× |
× |
/ |
a.txt |
删除文件 |
× |
× |
× |
× |
× |
× |
× |
× |
× |
× |
/ |
/A,a.txt | 回收站清除 |
/ |
/ |
/ |
/ |
/ |
/ |
/ |
/ |
/ |
/ |
A×a× |
符号解释:√:能够操做,×:不能够操做,/:互相不影响数据库
总体解释:例如第一行,意思是:对于A这个文件夹,当第一我的进行新建操做的时候,其余人同时进行新建、上传、移动文件、复制文件、重命名文件、删除文件是容许的,移动文件夹、复制文件夹、重命名文件夹、删除文件夹是不容许的,回收站清除和新建操做是互不影响的。缓存
分布式锁常见的三种实现方式:数据库、zookeeper/etcd(临时有序节点)、redis(setnx/lua脚本),各有千秋。安全
原理简单易实现,建立一张lock表,存储锁定的资源、上锁对象、获取锁的资源、获取锁时间等,获取锁时查询该资源是否存在记录,存在且未过失效时间则获取锁失败,不存在则插入一条数据而且获取锁成功;释放锁则更简单,删除锁数据便可。服务器
详见zookeeper总结数据结构
详见Redis总结并发
基于开文处所列状况,要覆盖全部复杂状况很难,可是实现基本的文件夹锁是必须的,故选择了redis+lua脚本,具体代码以下分布式
/**
* redis工具类
*/
public class RedisLockUtils {
static final Long SUCCESS = 1L;
static final String LOCKED_HASH = "cs:lockedHashKey";
static final String GET_LOCK_LUA_RESOURCE = "/lua/getFileLock.lua";
static final String RELEASE_LOCK_LUA_RESOURCE = "/lua/releaseFileLock.lua";
static final Logger LOG = LoggerFactory.getLogger(RedisLockUtils.class);
/**
* 获取文件夹锁
* @param redisTemplate
* @param lockProjectId
* @param lockKey
* @param requestValue
* @param expireTime 单位:秒
* @return
*/
public static boolean getFileLock(RedisTemplate redisTemplate, Long lockProjectId, String lockKey, String requestValue, Integer expireTime) {
LOG.info("start run lua script,{{}} start request lock",lockKey);
long start = System.currentTimeMillis();
DefaultRedisScript<String> luaScript =new DefaultRedisScript<>();
luaScript.setLocation(new ClassPathResource(GET_LOCK_LUA_RESOURCE));
luaScript.setResultType(String.class);
Object result = redisTemplate.execute(
luaScript,
Arrays.asList(lockKey, LOCKED_HASH + lockProjectId),
requestValue,
String.valueOf(expireTime),
String.valueOf(System.currentTimeMillis())
);
boolean getLockStatus = SUCCESS.equals(result);
LOG.info("{{}} cost time {} ms,request lock result:{}",lockKey,(System.currentTimeMillis()-start), getLockStatus);
return getLockStatus;
}
/**
* 释放文件夹锁
* @param redisTemplate
* @param lockProjectId
* @param lockKey
* @param requestValue
* @return
*/
public static boolean releaseFileLock(RedisTemplate redisTemplate, Long lockProjectId, String lockKey, String requestValue) {
DefaultRedisScript<String> luaScript =new DefaultRedisScript<>();
luaScript.setLocation(new ClassPathResource(RELEASE_LOCK_LUA_RESOURCE));
luaScript.setResultType(String.class);
Object result = redisTemplate.execute(
luaScript,
Arrays.asList(lockKey, LOCKED_HASH + lockProjectId),
requestValue
);
boolean releaseLockStatus = SUCCESS.equals(result);
LOG.info("{{}}release lock result:{}", lockKey, releaseLockStatus);
return releaseLockStatus;
}
}复制代码
requestKey
为请求锁的路径,requestValue
为请求锁的value,应为请求锁时生成的UUID
,确保解锁人只能为上锁人,lockedKeys
为存放全部锁的哈希表
的key,这里用常量加项目id的方式,确保一个项目的全部锁存在一个哈希表
里面,expireTime
为锁的过时时间,nowTime
为当前时间,因为lua脚本里面获取当前时间消耗性能且获取的是redis服务器上的当前时间,可能不许确。函数
首先,经过GET key
判断是否有人正在操做这个文件夹,如有人在操做则直接返回0(获取锁失败),不然获取存放该项目锁的哈希表里面的全部key,遍历全部key,经过lua脚本的string.find
函数对比该key和请求的key是否存在包含或被包含关系,若存在包含关系且未失效,则返回0(获取锁失败),不然则可获取锁,设置key和过时时间及存入哈希表(哈希表内存放请求锁的key和请求时间),最后返回1(获取锁成功)。
例如请求上图中项目下的C文件夹的锁,请求路径为:项目/A/C
,当另外一我的想操做D文件夹,请求路径为:项目/A/C/D
,此时查询到存储这个项目全部锁定key的哈希表
,里面包含项目/A/C
这个key,这两个key经过lua函数string.find
发现项目/A/C/D
包含项目/A/C
,且未到过时时间,则获取锁失败,不然获取锁成功。
local requestKey=KEYS[1]
local lockedKeys=KEYS[2]
local requestValue=ARGV[1]
local expireTime=ARGV[2]
local nowTime=ARGV[3]
if redis.call('get',requestKey)
then
return 0
end
local lockedHash = redis.call('hkeys',lockedKeys)
for i=1, #lockedHash do
if string.find(requestKey,lockedHash[i]) or string.find(lockedHash[i],requestKey)
then
local lockTime = redis.call('hget',lockedKeys,lockedHash[i])
if (nowTime-lockTime) >= expireTime * 1000
then
redis.call('hdel',lockedKeys,lockedHash[i])
else
return 0
end
end
end
redis.call('set',requestKey,requestValue)
redis.call('expire',requestKey,expireTime)
redis.call('hset',lockedKeys,requestKey,nowTime)
return 1复制代码
requestKey
为请求锁的路径,requestValue
为请求锁的value,应为请求锁时生成的UUID
,确保解锁人只能为上锁人,lockedKeys
为存放全部锁的哈希表
的key,这里用常量加项目id的方式,确保一个项目的全部锁存在一个哈希表
里面。
local requestKey=KEYS[1]
local lockedKeys=KEYS[2]
local requestValue=ARGV[1]
if redis.call('get', requestKey) == requestValue
then
redis.call('hdel', lockedKeys,requestKey)
return redis.call('del',requestKey)
else
return 0
end复制代码
requestKey
变化而变化 经过单元自测和测试环境测试基本能够确保多数状况下的多用户并发操做文件只有一人能进行有效操做,保证了数据的安全性。通过此次实践,对分布式锁有了更深刻的了解。