1. Several conditions must be met to use distributed locks:
1. The system is a distributed system (the key is distributed , a single machine can be implemented using ReentrantLock or synchronized code blocks)
2. Shared resources (each system accesses the same resource, and the carrier of the resource may be a traditional relational database or NoSQL)
3 , Synchronous access (that is, there are many processes accessing the same shared resource at the same time. Without synchronous access, who cares whether you compete for resources or not)
2. Application scenario examples
The deployment architecture of the management background (multiple tomcat servers redis [multiple tomcat servers access one redis] mysql [multiple tomcat servers access mysql on one server]) meets the conditions for using distributed locks. Multiple servers need to access the resources of the redis global cache. Problems will occur if distributed locks are not used. Look at the following pseudo code:
long N=0L; //N从redis获取值 if(N<5){ N++; //N写回redis }
The main functions of the above code are:
Get the value N from redis, perform a boundary check on the value N, increment it by 1, and then write N back to redis. This application scenario is very common, such as flash sales, global incremental ID, IP access restrictions, etc.
In terms of IP access restrictions, malicious attackers may initiate unlimited access, and the amount of concurrency is relatively large. The boundary check of N is unreliable in a distributed environment, because N read from redis may already be dirty. data.
Traditional locking methods (such as Java's synchronized and Lock) are useless, because this is a distributed environment, and the firefighters who are fighting this synchronization problem are helpless. In this critical period of life and death, distributed locks finally come into play.
Distributed locks can be implemented in many ways, such as zookeeper, redis.... Either way, the basic principle remains the same: a state value is used to represent the lock, and the occupation and release of the lock are identified by the state value.
Here we mainly talk about how to use redis to implement distributed locks.
3. Use the setNX command of redis to implement distributed locks
1. Implementation principle
Redis is a single-process single-thread mode and uses a queue The mode turns concurrent access into serial access, and there is no competition between multiple clients' connections to Redis. The SETNX command of redis can easily implement distributed locks.
2. Basic command analysis
1) setNX (SET if Not eXists)
Syntax:
SETNX key value
Set the value of key to value, when And only if key does not exist.
If the given key already exists, SETNX does not take any action.
SETNX is the abbreviation of "SET if Not eXists" (if it does not exist, then SET)
Return value:
Set successfully, return 1.
Failed to set, return 0.
Example:
redis> EXISTS job # job 不存在 (integer) 0 redis> SETNX job "programmer" # job 设置成功 (integer) 1 redis> SETNX job "code-farmer" # 尝试覆盖 job ,失败 (integer) 0 redis> GET job # 没有被覆盖 "programmer"
So we execute the following command
SETNX lock.foo
If 1 is returned, the client obtains the lock and sets the key value of lock.foo to the time The value indicates that the key is locked and the client can finally release the lock via DEL lock.foo.
If 0 is returned, it means that the lock has been obtained by another client. At this time, we can return first or retry and wait for the other party to complete or wait for the lock to time out.
2) getSET
Syntax:
GETSET key value
Set the value of the given key to value and return the old value of key.
When key exists but is not of string type, an error is returned.
Return value:
Return the old value of the given key.
When key has no old value, that is, when key does not exist, nil is returned.
3) get
Syntax:
GET key
Return value:
When key does not exist, return nil, otherwise, return the value of key.
If key is not a string type, then an error is returned
4. Solve the deadlock
The above locking logic has a problem: if a The client holding the lock fails or crashes and cannot release the lock. How to solve the problem?
We can judge whether this has happened through the timestamp corresponding to the lock key. If the current time is greater than the value of lock.foo, it means that the lock has expired and can be reused.
When this happens, you cannot simply delete the lock through DEL and then SETNX again (to be reasonable, the operation of deleting the lock should be performed by the owner of the lock. You only need to wait for it here. timeout), when multiple clients detect that the lock has timed out, they will try to release it. A race condition may occur here. Let us simulate this scenario:
C0 operation has timed out. , but it still holds the lock. C1 and C2 read lock.foo and checked the timestamp, and found that they timed out one after another.
C1 sends DEL lock.foo
C1 sends SETNX lock.foo and succeeds.
C2 sends DEL lock.foo
C2 sends SETNX lock.foo and succeeds.
In this way, C1 and C2 both get the lock! Big problem!
Fortunately, this problem can be avoided. Let's take a look at how the C3 client does it:
C3 sends SETNX lock.foo to obtain Lock, since C0 still holds the lock, Redis returns a 0 to C3
C3发送GET lock.foo 以检查锁是否超时了,如果没超时,则等待或重试。
反之,如果已超时,C3通过下面的操作来尝试获得锁:
GETSET lock.foo
通过GETSET,C3拿到的时间戳如果仍然是超时的,那就说明,C3如愿以偿拿到锁了。
如果在C3之前,有个叫C4的客户端比C3快一步执行了上面的操作,那么C3拿到的时间戳是个未超时的值,这时,C3没有如期获得锁,需要再次等待或重试。留意一下,尽管C3没拿到锁,但它改写了C4设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。
注意:为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。
五、代码实现
expireMsecs 锁持有超时,防止线程在入锁以后,无限的执行下去,让锁无法释放
timeoutMsecs 锁等待超时,防止线程饥饿,永远没有入锁执行代码的机会
注意:项目里面需要先搭建好redis的相关配置
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * Redis distributed lock implementation. * * @author zhengcanrui */ public class RedisLock { private static Logger logger = LoggerFactory.getLogger(RedisLock.class); private RedisTemplate redisTemplate; private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100; /** * Lock key path. */ private String lockKey; /** * 锁超时时间,防止线程在入锁以后,无限的执行等待 */ private int expireMsecs = 60 * 1000; /** * 锁等待时间,防止线程饥饿 */ private int timeoutMsecs = 10 * 1000; private volatile boolean locked = false; /** * Detailed constructor with default acquire timeout 10000 msecs and lock expiration of 60000 msecs. * * @param lockKey lock key (ex. account:1, ...) */ public RedisLock(RedisTemplate redisTemplate, String lockKey) { this.redisTemplate = redisTemplate; this.lockKey = lockKey + "_lock"; } /** * Detailed constructor with default lock expiration of 60000 msecs. * */ public RedisLock(RedisTemplate redisTemplate, String lockKey, int timeoutMsecs) { this(redisTemplate, lockKey); this.timeoutMsecs = timeoutMsecs; } /** * Detailed constructor. * */ public RedisLock(RedisTemplate redisTemplate, String lockKey, int timeoutMsecs, int expireMsecs) { this(redisTemplate, lockKey, timeoutMsecs); this.expireMsecs = expireMsecs; } /** * @return lock key */ public String getLockKey() { return lockKey; } private String get(final String key) { Object obj = null; try { obj = redisTemplate.execute(new RedisCallback
调用:
RedisLock lock = new RedisLock(redisTemplate, key, 10000, 20000); try { if(lock.lock()) { //需要加锁的代码 } } } catch (InterruptedException e) { e.printStackTrace(); }finally { //为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起, //操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。 ————这里没有做 lock.unlock(); }
六、一些问题
1、为什么不直接使用expire设置超时时间,而将时间的毫秒数其作为value放在redis中?
如下面的方式,把超时的交给redis处理:
lock(key, expireSec){ isSuccess = setnx key if (isSuccess) expire key expireSec }
这种方式貌似没什么问题,但是假如在setnx后,redis崩溃了,expire就没有执行,结果就是死锁了。锁永远不会超时。
2、为什么前面的锁已经超时了,还要用getSet去设置新的时间戳的时间获取旧的值,然后和外面的判断超时时间的时间戳比较呢?
因为是分布式的环境下,可以在前一个锁失效的时候,有两个进程进入到锁超时的判断。如:
C0超时了,还持有锁,C1/C2同时请求进入了方法里面
C1/C2获取到了C0的超时时间
C1使用getSet方法
C2也执行了getSet方法
假如我们不加 oldValueStr.equals(currentValueStr) 的判断,将会C1/C2都将获得锁,加了之后,能保证C1和C2只能一个能获得锁,一个只能继续等待。
注意:这里可能导致超时时间不是其原本的超时时间,C1的超时时间可能被C2覆盖了,但是他们相差的毫秒及其小,这里忽略了。
更多redis知识请关注redis入门教程栏目。
The above is the detailed content of Introduction to the implementation method of redis distributed lock. For more information, please follow other related articles on the PHP Chinese website!