origin
Recent work on a project, similar to a certain degree of cloud disk, additional customization, I was responsible for cloud disk related functions, the project with the cloud disk, assign permissions for project unit, the same project and all subdirectories have permission the user can operate all files at the same time, so it is easy to appear the concurrent operation, and the table structure design, Both files and folders have a path field, which stores the path of the parent folder, so that it is easy to retrieve, rename and move more trouble.
As follows, for example a classmate are moving under the project folder, C and b classmate also D under operating projects which under the xt file, it will appear problem, so need to be distributed lock control, armour when operating C folder, the folder C all child files and folder contains C the parent folder is locked, as shown in figure will be locked folder and the files are: A, C, C, C, t, D, E, d.t, where A, t and B are not locked, this is the case of movement, the following table lists the other cases.
Action object | operation | new | upload | Move folders | Move files | Copy folder | Copy the file | Rename folders | Rename file | Delete folders | Delete the file | Recycle bin removal |
---|---|---|---|---|---|---|---|---|---|---|---|---|
/A | new | Square root | Square root | x | Square root | x | Square root | x | Square root | x | Square root | / |
/A | upload | Square root | Square root | x | Square root | x | Square root | x | Square root | x | Square root | / |
/A->/B | Move folders | A * B * | A * B * | A * B * | A * B * | A) B) | A) B) | A * B * | (B) | A * B * | (B) | / |
a.txt->/B | Move files | B) | B) | B… | B) | B… | A square root B * | A * B * | (B) | B… | (B) | / |
/A->/B | Copy folder | B) | B) | B… | B) | B) | B) | B… | B) | B… | B) | / |
a.txt->/B | Copy the file | B) | B) | B… | B) | B) | B) | B… | B) | B… | B) | / |
/A | Rename folders | A… | A… | A… | A… | A square root | A square root | A… | A… | A… | A… | / |
a.txt | Rename file | / | / | x | x | x | x | Square root | x | x | x | / |
/A | Delete folders | x | x | x | x | x | x | x | x | x | x | / |
a.txt | Delete the file | x | x | x | x | x | x | x | x | x | x | / |
/ A, a.t xt | Recycle bin removal | / | / | / | / | / | / | / | / | / | / | * A * A |
Symbol description: √ : this operation can be performed. × : this operation cannot be performed. / : this operation does not affect each other
Overall explanation: For example, in the first line, it means: This folder for A, when the first person for the new operation, others at the same time to build, upload, move files, copy files, rename files, delete files is allowed, move folders, copy the folder folder, rename, delete folder is not allowed, the recycle bin removal and new operation is of no effect.
Thinking and research
The three common implementation methods of distributed lock: database, ZooKeeper/ETCD (temporary ordered node), Redis (setnx/ Lua script), each has its own advantages.
The database implements distributed locking
implementation
The principle is simple and easy to implement, create a lock table, store the locked resources, locked objects, resources to obtain the lock, obtain the lock time, etc., query whether there is a record of the resource when obtaining the lock, there is and has not expired time to obtain the lock failed, do not exist, insert a data and obtain the lock success; Releasing the lock is even easier by deleting the lock data.
disadvantages
- Release lock A deadlock occurs when data is deleted
advantages
- Implement a simple
Zookeeper /etcd implements distributed locks
For details, see ZooKeeper Summary
Redis implements distributed locking
See Redis summary
Distributed folder lock implementation process
It is difficult to cover all the complications based on the opening column, but it is necessary to implement the basic folder lock, so the Redis + Lua script is selected, and the code is as follows
Java get lock utility class
/** * Redis utility class */
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);
/** * get folder lock *@param redisTemplate
* @param lockProjectId
* @param lockKey
* @param requestValue
* @paramExpireTime Unit: second *@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;
}
/** * release folder lock *@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);
returnreleaseLockStatus; }}Copy the code
The lua script
Get folder lock
Ginseng explanation to
RequestKey = path of lock request, requestValue = value of lock request, UUID generated when lock request, ensure unlock person can only be the lock person, lockedKeys = key of hash table that stores all locks, here use constant plus item ID method. Ensure that all locks in a project are in a hash table, expireTime is the lock expiration time, nowTime is the current time, because the lua script to obtain the current time and the current time on the Redis server, may not be accurate.
Thinking that
First, check whether someone is operating the folder by GET key, if someone is operating, return 0 (failed to GET lock), otherwise GET all keys in the hash table where the item lock is stored, iterate over all keys, The string.find function of Lua script is used to compare whether the key and the requested key are contained or included. If the contained relationship exists and is not invalid, 0 is returned (failed to obtain the lock); otherwise, the lock can be obtained, the key and expiration time are set and stored in the hash table (the hash table stores the key and request time of the requested lock). Finally, return 1 (lock was acquired successfully).
For example, to request the lock of folder C under the project in the figure above, the request path is: project /A/C. When another person wants to operate folder D, the request path is: If the item /A/C/D contains the item /A/C key, the lock fails to be obtained. If the item /A/C/D contains the item /A/C before the expiration date, the lock fails to be obtained.
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
Copy the code
Release folder lock
Ginseng explanation to
RequestKey = path of lock request, requestValue = value of lock request, UUID generated when lock request, ensure unlock person can only be the lock person, lockedKeys = key of hash table that stores all locks, here use constant plus item ID method. Ensure that all locks for an item are in a hash table.
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
Copy the code
advantages
- Flexible, locking range can be arbitrary
requestKey
Change by change - Performance is good, in addition to the test of the first lua script cache time is long, the second time after the request results can be obtained in about 10ms
disadvantages
- Reliability depends on Redis
- Not a reentrant lock
- High maintenance costs, need to be familiar with redis 5 data structures and Lua scripts
conclusion
Unit self-test and test environment test can basically ensure that in most cases, multi-user concurrent operation files only one person can operate effectively, ensuring data security. After this practice, the distributed lock has a deeper understanding.
For more information, you can follow my personal blog: Yizhu Station
Also welcome to follow my official account: Yizhuxiaozhan, QR code: