Source code repository: github.com/zhshuixian/…

Yards cloud: gitee.com/ylooq/learn…

As mentioned in Spring Boot Redis integration, the data shared between processes needs to be locked to avoid the generation of dirty data. The single thread feature of Redis can be used to lock and release shared data. This chapter describes how to implement a simple distributed lock.

During Java application development, the modification of shared data resources required by multiple threads can be achieved by locking the synchronized or java.util.concurrent package. On distributed systems, these interthread locking tools are disabled for processes on different machines.

In a distributed system, services on different machines may access shared data resources at the same time. If multiple services write and read data at the same time, data obtained by different clients may be inconsistent, resulting in errors in the final data. For example, in the second kill activity, there are 100 items originally, but dozens of items may be oversold in the end.

In these cases, distributed locks are introduced to ensure that only one service access operation shares data at a time, using a mutual exclusion mechanism across the JVM to control access to shared resources. Distributed locks can be implemented in many ways, such as database – based, Zookeeper, Redis, Memcached, and Chubby.

A distributed lock shall meet the following conditions:

  • Mutual exclusion: Ensures that only one thread can access the same resource at a time
  • High availability and high performance: High availability and high performance of lock acquisition and lock release
  • Lock failure mechanism: prevents the thread holding the lock from hanging without releasing the lock, resulting in deadlock
  • Non-blocking lock: Failure to acquire a lock is returned, rather than waiting until the lock is acquired
  • Reentrancy: After a lock has expired or been released, its thread can continue to acquire the lock without data errors

1, simple Redis lock implementation

Refer to Spring Boot to integrate Redis, install and run the stand-alone Redis.

1.1. How does Redis acquire and release locks

Acquiring a lock:

Redis implements a simple distributed lock by running the SET resource-name anystring NX EX max-lock-time command to make use of the single-thread Redis feature.

# Redis command
SET key value [EX seconds] [PX milliseconds] [NX|XX]
Copy the code

SET parameter description, starting with Redis 2.6.12:

  • In the absence of EX, NX, PX, XX, override value regardless of type if key already exists. If not, create a new key — value
  • EX seconds: Set the expiration time to seconds.SET key value EX secondEffect equivalent toSETEX key second value
  • PX millisecond: Sets the expiration time of the key tomillisecondMilliseconds.SET key value PX millisecondEffect equivalent toPSETEX key millisecond value
  • NX: Sets the key only when the key does not exist.SET key value NXEffect equivalent toSETNX key value
  • XX: Sets the key only when the key already exists.
  • Value value: A random string must be added as the unique password (Token) to prevent the thread that holds an expired lock from deleting the lock of another thread

The client executes the following command:

  • If the server returnsOKThen the client acquires the lock.
  • If the server returnsNIL, the client fails to acquire the lock and can try again later.

The lock is automatically released when the expiration time is reached.

Release the lock

For lock release, do not use the DEL command, which causes the thread holding the expired lock to delete the lock held by the other thread. Lua scripts should be used to release the lock only if the key–value passed in is exactly the same as in Redis.

# Simple lua command to remove locks using EVAL... script... 1 The resource-name token-value command is used
if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end
Copy the code

1.2. Start using Redis locks

Create a new project 12-Redis-lock and introduce the following dependencies:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    // AOP dependencies, annotating the need to implement locks
    compile group: 'org.springframework.boot', name: 'spring-boot-starter-aop', version: '2.113..RELEASE'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}
Copy the code

Application. Properties Redis server address port password

# Redis host ip
spring.redis.host=wsl
# Redis server connection port
spring.redis.port=6379
# Redis database index (default 0)
spring.redis.database=0
# Redis server connection password (default null)
spring.redis.password=springboot
# maximum number of connections in the pool (use negative values to indicate no limit)
spring.redis.jedis.pool.max-active=8
Maximum connection pool blocking wait time (negative value indicates no limit)
spring.redis.jedis.pool.max-wait=-1
The maximum number of free connections in the connection pool
spring.redis.jedis.pool.max-idle=8
Minimum free connection in connection pool
spring.redis.jedis.pool.min-idle=0
Connection timeout (ms)
spring.redis.timeout=10
Copy the code

Redislockutil. Java implements Redis lock acquisition and release:

@Component
public class RedisLockUtil {
    Logger logger = LoggerFactory.getLogger(this.getClass());
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    
    public boolean lock(String key, String value, long timeout, TimeUnit timeUnit) {
    / / lock, can also use stringRedisTemplate opsForValue () setIfAbsent (key, value, 15, TimeUnit. SECONDS);
    // Expiration. From (timeout, timeUnit) Expiration time and unit
    / / RedisStringCommands. SetOption. SET_IF_ABSENT, equivalent to NX when key does not exist
        Boolean lockStat = stringRedisTemplate.execute((RedisCallback<Boolean>) connection ->
                connection.set(key.getBytes(StandardCharsets.UTF_8), value.getBytes(StandardCharsets.UTF_8),
                        Expiration.from(timeout, timeUnit), RedisStringCommands.SetOption.SET_IF_ABSENT));
        returnlockStat ! =null && lockStat;
    }

    public boolean unlock(String key, String value) {
        try {
            Use the Lua script to verify that the passed key--value is the same as in Redis
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Boolean unLockStat = stringRedisTemplate.execute((RedisCallback<Boolean>) connection ->
                    connection.eval(script.getBytes(), ReturnType.BOOLEAN, 1,
                            key.getBytes(StandardCharsets.UTF_8), value.getBytes(StandardCharsets.UTF_8)));
            return unLockStat == null| |! unLockStat; }catch (Exception e) {
            logger.error("Unlock failed key = {}", key);
            return false; }}}Copy the code

Test new lockController.java:

@RestController
public class LockController {
    @Resource RedisLockUtil redisLockUtil;
    Logger logger = LoggerFactory.getLogger(this.getClass());

    @RequestMapping("/buy")
    public String buy(@RequestParam String goodId) {
        long timeout = 15;
        TimeUnit timeUnit = TimeUnit.SECONDS;
        // UUID 作为 value
        String lockValue = UUID.randomUUID().toString();
        if (redisLockUtil.lock(goodId, lockValue, timeout, timeUnit)) {
            // Business processing
            logger.info("Acquire lock, conduct business processing");
            try {
                // Sleep for 10 seconds
                Thread.sleep(10 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            / / releases the lock
            if (redisLockUtil.unlock(goodId, lockValue)) {
                logger.error("Redis distributed lock unlock exception key is" + goodId);
            }
            return "Purchase successful";
        }
        return "Please try again later"; }}Copy the code

Run the project, at the same time, multiple clients visit http://localhost:8080/buy? GoodId = Springboot and observe the different returns.

2. Use annotations to acquire and release locks

In the above example, Redis get and release has been encapsulated as a RedisLockUtil class, but it is still cumbersome to use. There is a lot of repetitive code that can be extracted as common pre-processing and post-processing. The business code just needs to focus on the specific business. Here is a brief demonstration of how to obtain and release Redis locks using Spring AOP custom annotations.

The process of acquiring and releasing locks above can be extracted as common pre – and post-processing. The business code only needs to focus on the specific business. Here is a simple demonstration of how to achieve the acquisition and releasing of Redis locks using Spring AOP custom annotations.

2.1 Introduction to AOP

AOP is Aspect Oriented Program, is an important complement to object-oriented programming, is one of the most important functions in Spring.

With AOP, you can encapsulate logic and functionality that systems call together, such as Redis’s ability to acquire locks and release locks. Reduce duplicate code and reduce direct coupling between different modules, facilitating future expansion and maintainability. For example, redislockutil.java has added functionality that changes the type and number of parameter values passed in, and without AOP, all related code needs to be changed.

In the thought of section-oriented programming, the functions are divided into core business and peripheral functions. AOP is specially used to deal with the problem of cross concerns distributed in each module (different methods) in the system, that is, peripheral functions.

  • Core services: specific services, such as user login and database operations
  • Peripheral functions: such as log, thing management, security check, cache, etc

AspectJ is an AOP framework based on the Java language that provides powerful AOP capabilities, some of which have been borrowed or adopted by many other AOP frameworks.

Create a new redislock. Java with custom annotations:

// indicates that methods can be annotated
@Target({ElementType.METHOD})
// The annotation retention period is reserved at runtime
@Retention(RetentionPolicy.RUNTIME)
// Automatic inheritance
@Inherited
@Documented
public @interface RedisLock {
    /** The key value of the lock must be a non-empty string */
    @NotNull
    @NotEmpty
    String key(a);
    /** 锁的 value 值 */
    String value(a) default "";
    /** Default lock validity period Default 15 */
    long expire(a) default 15;
    /** Unit of the lock validity period. The default value is second */
    TimeUnit timeUnit(a) default TimeUnit.SECONDS;
}
Copy the code

Next we implement RedisLockAspect. Java to add pre – and post-processing to methods using the @redislock annotation:

@Component
@Aspect
public class RedisLockAspect {
    Logger logger = LoggerFactory.getLogger(this.getClass());
    @Resource
    private RedisLockUtil redisLockUtil;
    private String lockKey;
    private String lockValue;

    /** the pointcut redislock. Java is used@RedisLockThe annotation starts with */
    @Pointcut("@annotation(org.xian.lock.RedisLock)")
    public void pointcut(a) {}@Around(value = "pointcut()")
    public Object around(ProceedingJoinPoint joinPoint)  {
        // Get its @redislock annotation
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        RedisLock redisLock = method.getAnnotation(RedisLock.class);
        // Get the value of the key value on the annotation
        lockKey = redisLock.key();
        lockValue = redisLock.value();
        if (lockValue.isEmpty()) {
            lockValue = UUID.randomUUID().toString();
        }
        try {
            Boolean isLock = redisLockUtil.lock(lockKey, lockValue, redisLock.expire(), redisLock.timeUnit());
            logger.info("{} acquires the lock as {}", redisLock.key(), isLock);
            if(! isLock) {// Failed to obtain the lock
                logger.debug("Failed to acquire lock {}", redisLock.key());
                // You can customize an exception class and its interceptor, as shown in Spring Boot 2.X
                // https://ylooq.gitee.io/learn-spring-boot-2/#/09-ErrorController? id=spring-boot-2x-%e5%ae%9e%e6%88%98-restful-api-%e5%85%a8%e5%b1%80%e5%bc%82%e5%b8%b8%e5%a4%84%e7%90%86
                // or @afterthrowing: exception throw enhancement, equivalent to ThrowsAdvice
                throw new RuntimeException("Lock acquisition failed");
            } else {
                try {
                    // The lock is successfully obtained
                    return joinPoint.proceed();
                } catch (Throwable throwable) {
                    throw new RuntimeException("System exception"); }}}catch (Exception e) {
            throw new RuntimeException("System exception"); }}@After(value = "pointcut()")
    public void after(a) {
        / / releases the lock
        if (redisLockUtil.unlock(lockKey, lockValue)) {
            logger.error("Redis distributed lock unlock exception key {}", lockKey); }}}Copy the code

LockController. Java is added

@RequestMapping("/buybuybuy")
@RedisLock(key = "lock_key", value = "lock_value")
public String buybuybuy(@RequestParam(value = "goodId") String goodId) {
    try {
        Thread.sleep(10 * 1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "Purchase successful";
}
Copy the code

Run to http://localhost:8080/buybuybuy? GoodId = springboot.

Disadvantages: It is not possible to pass in key values as shown in the Redis cache below. This feature allows custom annotations to support SpEL Expression parsing through the Spring Expression Language (SpEL), which is not covered here.

Delete (@requestParam (value = "username") String username)
@CacheEvict(key = "#username")
public User delete(@RequestParam(value = "username") String username) {
    User user = select(username);
    userRepository.delete(user);
    return user;
}
Copy the code

Summary:

Simple introduction to the stand-alone version of Redis to achieve distributed lock acquisition and release, for the cluster type of Redis is not suitable for the actual business development, you can use Redisson, RedLock framework according to the situation.

A brief introduction to the implementation of custom annotations, the deficiency is that there is no implementation of SpEL expression parsing function, and the introduction of important concepts of Spring AOP is relatively simple, the link below is I saw, written well introduction blog, can be compared with the code and extension reading information to deepen understanding.

Resources and extended reading

Doc.redisfans.com/string/set….

www.ibm.com/developerwo…

Cloud.tencent.com/developer/a…

Segmentfault.com/a/119000000…