A preface.

Hello, everyone. In my previous blog post: Old Times cache Pits, I covered common cache pits. Redis is currently the most important middleware for Java backend cache, so it is very important to be familiar with the common usage scenarios of Redis. This article will be the first redis combat: distributed locking. Hope to help you, if there is any wrong in the article, welcome to point out, common progress.

2. Distributed lock introduction

This paper introduces the single application under the programming lock, unlock the realization principle and use. So how do we control the security of access to shared resources in microservices where access to shared resources is not within a SINGLE JVM? The answer is distributed locking. There are many common ways to implement distributed locks: mysql, ZK, redis. The most frequently used is based on Redis, with good performance, but relatively high maintenance complexity.

The core content of this article is the actual practice, providing a way based on how to easily access and use single Redis service. This article assumes that you already know the concept of distributed locks. If you are still not familiar with the common implementation of distributed locks, you are advised to read: Next time someone asks you about distributed locks, throw this article to them

Universal distributed lock

Before we talk about universal distributed locks, let’s recall the code for our add-unlock operation paradigm for shared resources in singleton applications

ReentrantLock lock = new ReentrantLock(); try { lock.lock(); }catch (Exception e){// Exception handling}finally {lock.unlock(); // Roll back business logic}Copy the code

Similarly, the locking paradigm based on redis implementation is the same as above, but the lock declaration method has been changed. Set resourceName value ex 5 nx with the set command of Redis. Here we take a look at the ReentrantLock paradigm above and see if it feels like the lock unlocking of a critical resource can be pulled out into a slice. Lock before normal logic is processed, and unlock after processing.

After reading this article, you are the Architect, and I beg you to stop writing ancestral code, I suggest some of the conventions that you should be aware of when writing code on a daily basis. According to the content of the above two articles and based on the idea of writing section, can we define a general annotation, as long as the annotation is marked on the method, before the execution of the method, we uniformly set distributed lock, the business logic execution, release the lock. If the execution logic of locking and unlocking is in the middle of a method, you can write a function to separate the business logic of locking and unlocking to achieve unified unlocking.

3.1. Business distributed lock annotations

Ok, let’s start by defining an annotation that can be added to the business method

package com.baiyan.lock.annotation; import com.baiyan.lock.config.RedisConstant; import java.lang.annotation.*; /** * @author baiyan * @time Documented @target ({elementType.annotation_type, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface DistributedLock { /** * False: Throws an exception. * * @return */ Boolean blocked() default true; /** * Lock expiration time, default 15 seconds ** @return */ int lockTime() default redisconstant.default_expire_time; /** * Enable lock renewal ** @return */ Boolean enableExtendTime() default false; /** * default lock renewal time ** @return */ int extendTime() default redisconstant.default_extend_expire_time; /** * Default lock renewal count ** @return */ int extendCounts() default redisconstant.default_extend_expire_counts; }Copy the code

Description:

This annotation defines a general concept of distributed locking. For demo purposes, reentrant locking is not used as redission. Locks added are not reentrant. In fact, those of you who know the implementation of Redission know that reentrant lock is also implemented based on AQS, which will not be explained in this paper.

Parameter analysis:

Blocked () : A Boolean type. The default value is true. When a method accesses a shared resource, the thread must wait or return an error if it fails to capture the lock.

LockTime () : lock expiration time. The default value is 15 seconds

EnableExtendTime () : indicates whether to enable lock extension. Distributed lock in the process of invocation, it is possible that the business logic has not been completed, but the lock expires, causing other threads to acquire the lock and modify the data, resulting in thread insecurity. In Redission, there is a special watchdog mechanism to implement lock renewal. When the lock expires, the server will check whether the client still holds the lock, and if so, the lock will be extended.

ExtendTime () : lock renewal time

ExtendCounts () : number of lock renewals

3.2. The lock interface

package com.baiyan.lock.model; /** * @author baiyan * @time 2021/5/29 15:14 */ public interface BaseLock {/** * Make sure that the business type is unique * * @return */ String getLockBusinessType(); /** * distributed lock unique identifier ** @return */ String getLockUniqueFlag(); }Copy the code

Description:

Since it is based on the section form, the locking method is the input parameter parsing for the parsing method. If the lock position is not at the beginning of a method, you can separate the logic holding the lock into a method and implement the interface as an input parameter.

Parameter analysis:

GetLockBusinessType () : indicates the business type. When adding a distributed lock, you need to declare the business to ensure that the prefix of a type of lock is consistent

GetLockUniqueFlag () : unique identifier of a distributed lock. Ensure that the key values of different data in the same service type are unique. You are advised to use the primary key ID of the data.

3.3. Section

package com.baiyan.lock.aspect; import com.baiyan.lock.annotation.DistributedLock; import com.baiyan.lock.config.RedisConstant; import com.baiyan.lock.model.BaseLock; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.time.Duration; import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; /** @author baiyan * @time 2021/5/29 15:07 */ @component @aspect public class DistributedLockAspect { @Autowired StringRedisTemplate stringRedisTemplate; @Around("@annotation(com.baiyan.lock.annotation.DistributedLock)") public Object logExecutionTime(ProceedingJoinPoint pjp) throws Throwable { MethodSignature signature = (MethodSignature) pjp.getSignature(); DistributedLock annotation = signature.getMethod().getAnnotation(DistributedLock.class); BaseLock lock = (BaseLock) Optional.ofNullable(pjp.getArgs()) .flatMap(args -> Stream.of(args).filter(e -> e instanceof BaseLock).findFirst()).orElse(null); If (objects.isnull (lock)){throw new RuntimeException(" method argument isNull, distributed lock failed to add "); } String redisLockKey = RedisConstant.LOCK_PREFIX + lock.getLockBusinessType() +lock.getLockUniqueFlag(); Object proceed; try{ lock(annotation,redisLockKey); proceed = pjp.proceed(); return proceed; }finally { unlock(redisLockKey); @param redisLockKey / / private void lock(DistributedLock annotation, DistributedLock annotation, DistributedLock annotation, DistributedLock annotation, DistributedLock annotation, DistributedLock annotation, DistributedLock annotation, DistributedLock annotation, DistributedLock annotation, DistributedLock annotation String redisLockKey){ int expireTime = annotation.enableExtendTime() ? annotation.lockTime() + annotation.extendTime() * annotation.extendCounts() : annotation.lockTime(); boolean getLock = false; // The process of spin can be set to a certain interval time and the maximum spin time, place the lock is not released for a long time, preempting the lock thread more and more, service // avalanche, here is a convenient example, only provide thought while (! getLock){ getLock = stringRedisTemplate.opsForValue() .setIfAbsent(redisLockKey,String.valueOf(Thread.currentThread().getId()), Duration.ofSeconds(expireTime)); if(! annotation.blocked() && ! GetLock){throw new RuntimeException(" lock failed "); }} /** * unlock ** @param redisLockKey Unlock key */ private void unlock(String redisLockKey){String value = stringRedisTemplate.opsForValue().get(redisLockKey); // Avoid exceptions thrown when parsing exceptions, go into the unlock logic, If (objects.equals (value, string.valueof (thread.currentThread ().getid ()))){ stringRedisTemplate.delete(redisLockKey); }}}Copy the code

As mentioned at the beginning, distributed locks are a generic type of component, and we can make this aspect a component starter, using Spring’s SPI mechanism, to introduce business applications out of the box

For those not familiar with the SPI mechanism of Start, check out this article I wrote about @SpringBootApplication’s past and present life

3.4. Constant configuration classes

package com.baiyan.lock.config; /** * @author baiyan * @time 2021/5/29 14:38 */ public class RedisConstant {/** * public final static String LOCK_PREFIX = "distributed_lock_prefix::"; /** * Default distributed lock expiration time */ public final static int DEFAULT_EXPIRE_TIME = 15; Public final static int DEFAULT_EXTEND_EXPIRE_TIME = 5; public final static int DEFAULT_EXTEND_EXPIRE_TIME = 5; /** * Public final static int DEFAULT_EXTEND_EXPIRE_COUNTS = 3; }Copy the code

4. Usage Examples

4.1. Lock entity classes

package com.baiyan.lock.model; import lombok.Data; /** * @author baiyan * @time 2021/5/29 15:20 */ @data public class implements BaseLock {/** * Order */ private Long orderId; /** * private String mobile; @Override public String getLockBusinessType(){ return "order"; } @Override public String getLockUniqueFlag(){ return this.mobile; }}Copy the code

4.2. The request

package com.baiyan.lock.controller; import com.baiyan.lock.annotation.DistributedLock; import com.baiyan.lock.model.Order; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; /** * @author baiyan * @time 2021/5/29 14:59 */ @RestController public class TestController { @PostMapping @DistributedLock(blocked = false) public void add(@RequestBody Order order) throws Exception{ Thread.sleep(10000); System.out.println(order); }}Copy the code

The @distributedlock (blocked = false) annotation means that when the request arrives, an error will be returned to the front end if the current mobile phone number’s DistributedLock is already held. Of course, you can define specific business exceptions on the cut side. Then use Spring’s elegant handling of global exceptions to return specific business prompts to the front end. The direct annotation ** @distributedLock ** indicates that when the request arrives, it will spin to wait for the lock to be released before entering the method.

Five. Contact me

If you think the article is well written, you can like it and comment + follow it

Nailing: louyanfeng25

WeChat: baiyan_lou