redisson實現基於業務的互斥鎖


雖然數據庫有鎖的實現,但是有時候對於數據的操作更需要業務層控制。

這個解決的問題有次面試被問到過,當時不知道怎么解決,亂說一通,今天也算是有個解決方案了

項目中有個需求,就是訂單需要經過一層一層的審核,審核過程中當前審核人有權限審核,上一審核人有權限撤銷上一步的審核。這樣在審核過程中就需要對訂單審核權限進行控制:

  1. 只有當前審核人和上一審核人可以進行操作
  2. 當前審核人審核后上一審核人就不能撤回
  3. 上一審核人撤回后當前審核人就無法審核

實現上述需求,我就需要對訂單的審核/撤銷接口進行控制,即同一訂單的審核/撤銷要互斥(審核/撤銷是同一個接口)

最簡單的解決方案是在該接口的方法上使用 synchronized,這種方案解決了上述的問題,但是這種方案的問題是不同訂單的審核操作也不能同時進行。

回到問題本身,我們要解決的是同一訂單的審核操作要互斥,互斥是基於訂單的,所以只要審核接口所操作的對象不是同一訂單就不需要互斥,怎么實現呢。

我想到的第一個方案是使用redis來為每個訂單加鎖

思路是

  1. 當有審核的請求線程時,先通過訂單編號(訂單的唯一索引)往redis中set一組值(使用
    RedisTemplate.opsForValue().setIfAbsent(key, value)
    如果已經存在key,返回false且不做任何改變,不存在就將 key 的值設為 value),在這里我把訂單編號作為key,set成功后在設置一個過期時間(為了避免死鎖)
  2. 當1返回true時代表加鎖成功,當前請求線程繼續執行,執行結束后需要釋放鎖,即刪除redis中的key
  3. 當1返回false時,等待,繼續執行2

這是鎖實現

 1 package pers.lan.jc.compnent;
 2 
 3 import lombok.extern.slf4j.Slf4j;
 4 import org.springframework.beans.factory.annotation.Autowired;
 5 import org.springframework.data.redis.core.RedisTemplate;
 6 import org.springframework.stereotype.Component;
 7 
 8 import java.util.concurrent.TimeUnit;
 9 
10 /**
11  * @author lan  [1728209643@qq.com]
12  * @create 2018-12-01 14:12
13  * @desc redis鎖
14  */
15 @Slf4j
16 @Component
17 public class RedisLock {
18 
19     private final static String LOCK_PREFIX = "LOCK:";
20 
21     @Autowired
22     private RedisTemplate<String, String> redisTemplate;
23 
24     public boolean lock(String key) {
25         while (true) {
26             try {
27                 if (setIfAbsent(key)) {
28                     return true;
29                 }
30                 Thread.sleep(100);
31             } catch (Exception e) {
32                 return false;
33             } finally {
34                 unlock(key);
35             }
36         }
37     }
38 
39     private synchronized boolean setIfAbsent(String key) {
40         try {
41             Boolean locked = redisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + key, key);
42             if (locked != null && locked) {
43                 redisTemplate.expire(LOCK_PREFIX + key, 120, TimeUnit.SECONDS);
44                 return true;
45             }
46         } finally {
47             unlock(key);
48         }
49         return false;
50     }
51 
52     public void unlock(String key) {
53         redisTemplate.delete(LOCK_PREFIX + key);
54     }
55 
56 }

這種方案不好的地方在於,set和expire操作不是原子的,於是setIfAbsent()方法是互斥的,並發性能並不是很好

另一種方案是使用redisson

添加依賴

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.9.1</version>
        </dependency>

  配置

package pers.lan.jc.config;

import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author lan  [1728209643@qq.com]
 * @create 2018-12-01 16:19
 * @desc redissonConfig
 */
@Slf4j
@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.setLockWatchdogTimeout(10000L);
        SingleServerConfig singleServerConfig = config.useSingleServer();
        singleServerConfig.setPassword("travis");
        singleServerConfig.setAddress("redis://118.25.43.205:6379");
        singleServerConfig.setDatabase(0);
        return Redisson.create(config);
    }
}

  使用

package pers.lan.jc.controller;

import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author lan  [1728209643@qq.com]
 * @create 2018-12-01 14:32
 * @desc redis鎖控制器
 */
@RequestMapping("/lock")
@RestController
public class RedisLockController {

    @Autowired
    private RedissonClient redisson;

    private final static String PREFIX = "lan:";

    @GetMapping("/get2")
    public Object lock2(@RequestParam String key) {
        RReadWriteLock lock = redisson.getReadWriteLock(PREFIX + key);

        try {
            lock.writeLock().lock();
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + " ##########");
            System.out.println(Thread.currentThread().getName() + " @@@@@@@@@@");
            System.out.println(Thread.currentThread().getName() + " %%%%%%%%%%");
            System.out.println();
            System.out.println();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.writeLock().unlock();
        }

        return "ok";
    }
}

  redisson具體的文檔見https://github.com/redisson/redisson/wiki

項目中使用:

為了不影響原有業務和代碼冗余等,我想通過注解+AOP使用redisson加鎖,在每個接口上通過如下注解

package pers.lan.jc.annotation;

import java.lang.annotation.*;

/**
 * @author lan  [1728209643@qq.com]
 * @create 2018-12-01 18:12
 * @desc 加鎖器注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Locking {

    String key();

}

  key的值為每個接口加鎖的關鍵索引(比如訂單編號)

但是問題又來了,有的接口訂單表號是放在實體類中的,怎么引用呢?仿照spring中的Cache類注解,,通過Spring EL表達式使用,

需要加鎖的接口使用注解如下

    @Locking(key = "#book.id")
    @CachePut(key = "#book.id")
    public void update(Book book) {
        bookMapper.update(book);
    }

  其中CachePut注解可以忽略

切面如下

package pers.lan.jc.compnent;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import pers.lan.jc.annotation.Locking;

import java.lang.reflect.Method;

/**
 * @author lan  [1728209643@qq.com]
 * @create 2018-12-01 18:20
 * @desc 鎖切面
 */
@Aspect
@Component
@Slf4j
public class LockAspect {

    @Autowired
    private RedissonClient redisson;


    @Pointcut("@annotation(pers.lan.jc.annotation.Locking)")
    public void lockAspect() {
    }

    @Around("lockAspect()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Locking locking = method.getDeclaredAnnotation(Locking.class);
        String prefix = "lockKey:" + joinPoint.getTarget().getClass().getSimpleName() + "." + method.getName() + ".";
        if (locking != null) {
            try {
                ParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
                String[] parameterNames = discoverer.getParameterNames(method);
                Object[] args = joinPoint.getArgs();
                if (parameterNames != null) {
                    ExpressionParser parser = new SpelExpressionParser();
                    EvaluationContext ctx = new StandardEvaluationContext();
                    int len = Math.min(args.length, parameterNames.length);
                    for (int i = 0; i < len; i++) {
                        ctx.setVariable(parameterNames[i], args[i]);
                    }
                    Object value = parser.parseExpression(locking.key()).getValue(ctx);
                    RReadWriteLock lock = redisson.getReadWriteLock(prefix + value);
                    log.info("正在嘗試向[" + prefix + "." + method.getName() + "]加鎖, key = " + prefix + value);
                    try {
                        lock.writeLock().lock();
                        log.info("加鎖成功,正在處理業務, key = " + prefix + value);
                        return joinPoint.proceed();
                    } finally {
                        log.info("業務處理結束,釋放鎖, key = " + prefix + value);
                        lock.writeLock().unlock();
                    }
                }

            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        return joinPoint.proceed();
    }
}

  為了不影響原有業務和各種異常,所以多了很多try catch塊,因為業務中用到的都是互斥鎖,所以這里我使用的都是writeLock實現的

都是為了給自己看,所以沒寫太詳細,哈哈哈


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM