redisson实现基于业务的互斥锁

2018-12-02 06:17:43来源:博客园 阅读 ()

新老客户大回馈,云服务器低至5折

虽然数据库有锁的实现,但是有时候对于数据的操作更需要业务层控制。

这个解决的问题有次面试被问到过,当时不知道怎么解决,乱说一通,今天也算是有个解决方案了

项目中有个需求,就是订单需要经过一层一层的审核,审核过程中当前审核人有权限审核,上一审核人有权限撤销上一步的审核。这样在审核过程中就需要对订单审核权限进行控制:

  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实现的

都是为了给自己看,所以没写太详细,哈哈哈

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇:java (x) 关于多线程的CPU密集型和IO密集型这件事

下一篇:转载:java中Thread.sleep()函数使用