Use Watch to implement Redis optimistic lock
Optimistic lock based on the idea of Compare And Swap (CAS) (Compare And replace), is not mutually exclusive, does not produce lock wait And consume resources, but requires repeated retry, but also because of the retry mechanism, can respond quickly. So we can use Redis to implement optimistic locking. Specific ideas are as follows:
Create a redis transaction. 4. Add the value of the redis key to the transaction. If the key monitored by watch is changed, the item is not submitted.)
Redis optimistic locks implement seckilling
public class Second { public static void main(String[] arg) { String redisKey = "lock"; ExecutorService executorService = Executors.newFixedThreadPool(20); Try {Jedis Jedis = new Jedis("127.0.0.1", 6378); Jedis.set (redisKey, "0"); jedis.close(); } catch (Exception e) { e.printStackTrace(); } for (int i = 0; i < 1000; Executorservice.execute (() -> {Jedis jedis1 = new Jedis("127.0.0.1", 6378); try { jedis1.watch(redisKey); String redisValue = jedis1.get(redisKey); int valInteger = Integer.valueOf(redisValue); String userInfo = UUID.randomUUID().toString(); If (valInteger < 20) {Transaction tx = jedis1.multi(); tx.incr(redisKey); List list = tx.exec(); // return empty list instead of empty if (list! = null && list.size() > 0) {system.out.println (" userInfo + "); Number of current successes :" + (valInteger + 1)); } // The version is changed. Else {system.out. println(" user :" + userInfo + "sec_kill failed ")}} else {system.out. println(" 20 sec_kill succeeded, sec_kill failed "); } } catch (Exception e) { e.printStackTrace(); } finally { jedis1.close(); }}); } executorService.shutdown(); }}Copy the code
Setnx implements distributed locking
Obtaining lock method 1 Is recommended
@param requestId requestId, uuid+threadID * @param expireTime expiration time, * @return */ public Boolean getLock(String lockKey,String requestId,int expireTime) {//NX: ensure mutual exclusion // hset atomic operation String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime); if("OK".equals(result)) { return true; } return false; }Copy the code
Method 2 is not recommended in the case of high concurrency
public boolean getLock(String lockKey,String requestId,int expireTime) { Long result = jedis.setnx(lockKey, requestId); If (result == 1) {// if(result == 1) {// If (result == 1) {// If (result == 1) {// If (result == 1) {jedis.expire(lockKey, expireTime); return true; } return false; }Copy the code
Lock release 1 is not recommended. Concurrency may cause problems
@param lockKey @param requestId public static void releaseLock(String lockKey,String requestId) {// If (requestid.equals (jedis.get(lockKey))) {jedis.del(lockKey); }}Copy the code
The problem is that if the jedis.del() method is called, the lock will be unlocked when it no longer belongs to the current client. So is there really such a scenario?
The answer is yes, for example, client A locks, client A unlocks after A period of time, and before jedis.del() is executed, the lock suddenly expires, client B tries to lock successfully, and then client A executes del() to unlock client B.
Method 2 (Redis + Lua script implementation) recommended using reids Lua scripts will put commands in a thing to execute
public static boolean releaseLock(String lockKey, String requestId) {String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); if (result.equals(1L)) { return true; } return false; }Copy the code
The above way to achieve Redis distributed lock problems
1. In master/slave mode, when the host is down, the lock may be repeatedly obtained by different clients
The above figure shows that client1 has obtained the lock, but before it can be synchronized to the slave server, the master server breaks down. At this time, the slave server becomes the master server, but there is no key data on the server, so Client2 can also be locked successfully.
2. After expireTime is exceeded, it cannot be used
So what’s the solution? Use Redisson
Use of Redisson distributed locks
Add jar package dependencies
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>2.7.0</version>
</dependency>
Copy the code
Configuration Redisson
public class RedissonManager { private static Config config = new Config(); Private static Redisson Redisson = null; // Redisson static{config.useclusterServers () // Interval for scanning cluster status, in milliseconds. SetScanInterval (2000) // At least 6 nodes in cluster mode (3 primary and 3 secondary nodes, 3. Master sharding, AddNodeAddress ("redis://127.0.0.1:6379"). AddNodeAddress ("redis://127.0.0.1:6380"). AddNodeAddress (" redis: / / 127.0.0.1:6381 "). The addNodeAddress (" redis: / / 127.0.0.1:6382)" AddNodeAddress (" redis: / / 127.0.0.1:6383 "). The addNodeAddress (" redis: / / 127.0.0.1:6384 "); Redisson =(redisson) redisson.create (config); Public static redisson getRedisson() {return redisson; }}Copy the code
Lock acquisition and release
Public class DistributedRedisLock {// Obtain the redisson object from the configuration class private static redisson redisson = RedissonManager.getRedisson(); private static final String LOCK_TITLE = "redisLock_"; Public static Boolean acquire(String lockName) {String key = LOCK_TITLE + lockName; RLock mylock = redisson.getLock(key); Uuid +threadId mylock.lock(2, 3, timeUtil.second); Return true; } public static void release(String lockName) {String key = LOCK_TITLE + lockName; RLock mylock = redisson.getLock(key); // mylock.unlock(); }}Copy the code
Using distributed locks
public String discount() throws IOException{ String key = "lock001"; / / lock DistributedRedisLock. Acquire (key); // Execute specific business logic //... / / release the lock DistributedRedisLock. Release (key); Return ""; }Copy the code
Redisson distributed lock implementation principle
Let’s look at some of the mechanisms shown above
Locking mechanism
If the client is facing a Redis cluster, it first selects a machine based on the Hash node. Send lua script to redis server as follows:
"if (redis.call('exists',KEYS[1])==0) then "+ -- To see if there is a lock"redis.call('hset',KEYS[1],ARGV[2],1) ; "+ -- Lock without lock"redis.call('pexpire',KEYS[1],ARGV[1]) ; "+
"return nil; end ;" +
"if (redis.call('hexists',KEYS[1],ARGV[2]) ==1 ) then "+ -- I added the lock"redis.call('hincrby',KEYS[1],ARGV[2],1) ; "+ -- reentrant lock"redis.call('pexpire',KEYS[1],ARGV[1]) ; "+
"return nil; end ;" +
"return redis.call('pttl',KEYS[1]) ;"Can't lock, return lock timeCopy the code
What Lua does: Ensure atomicity in the execution of this complex business logic. ARGV[2] : lock client ID(uid.randomuuid () + “:” + threadId)
The “exists myLock” command is used to determine if a lock does not exist. How do you lock it? Very simple, with the following command:
hset myLock 8743c9c0-0795-4907-87fd-6c719a6b4586:1 1
Use this command to set up a hash data structure. After this command is executed, a data structure similar to the following appears:
myLock :{"8743c9c0-0795-4907-87fd-6c719a6b4586:1":1 }
The above represents “8743C9C0-0795-4907-87FD-6C719a6B45866:1” the client to “myLock” the lock key completed.
The pexpire myLock 30000 command is then executed to set the lifetime of the myLock key to 30 seconds.
Lock mutually exclusive mechanism
What if client 1 already locks and client 2 tries to lock and executes the same Lua script?
The first if judgment executes “exists myLock” and finds that the lock key myLock already exists. Then the second if determines whether the hash data structure for the myLock lock key contains the ID of client 2, but it clearly does not, because it contains the ID of client 1. Therefore, client 2 will get a number returned by PTTL myLock that represents the remaining lifetime of the lock key myLock. Let’s say you have 15,000 milliseconds to live. Client 2 will then enter a while loop, constantly trying to lock.
Automatic delay mechanism
Once client 1 successfully locks, a watch dog will be started. It is a background thread that will check the lock key every 10 seconds. If client 1 still has the lock key, the lifetime of the lock key will be continuously extended.
Reentrant locking mechanism
The first if judgment is definitely not true. “exists myLock” indicates that the lock key already exists. The second if judgment holds because the ID contained in myLock’s hash data structure is the ID of client 1, i.e
“8743 c9c0 fd – 0795-4907-87-6 c719a6b4586:1”
The logic for reentrant locking is executed with:
incrby myLock 8743c9c0-0795-4907-87fd-6c71a6b4586:1 1
Using this command, the lock count of client 1 is incremented by 1. The data structure becomes:
myLock :{"8743c9c0-0795-4907-87fd-6c719a6b4586:1":2 }
Lock release mechanism
The lua script is executed when the lock is released:
Publish the redis message if the key does not exist"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;"If the + #key and field do not match, it indicates that the current client thread does not hold the lock and cannot proactively unlock the client. I didn't put the lock on it"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; "+ # subtracts value1
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); "+ # if counter>0Note The key cannot be deleted because the lock is reentrant"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; "+ # delete key and publish unlock message"else " +
"redis.call('del', KEYS[1]); "+ # delete lock"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;".Copy the code
– KEYS[1] : specifies the key to be locked. The value must be a string. — KEYS[2] : redisson_lockchannel{+ getName() + “} “– ARGV[1] :reids message body, The key of redis is unlocked, and the Subscribe of Redis wakes up other client threads that Subscribe to unlock messages to apply for the lock. — ARGV[2] : lock timeout to prevent deadlocks — ARGV[3] : lock unique identifier, which is the id(uuid.randomuuid ()) + “:” + threadId
If lock.unlock() is executed to release a distributed lock, the business logic is very simple. In other words, each time the lock in the myLock data structure is reduced by one. If the lock count is 0, the client no longer holds the lock, and the :del myLock command is used to delete the key from redis.
Then, the other client 2 can try to complete the lock.