redis分散式鎖-spring boot aop+自定義註解實現分散式鎖

_否極泰來發表於2021-05-26

接這這一篇redis分散式鎖-java實現末尾,實現aop+自定義註解 實現分散式鎖

1、為什麼需要 宣告式的分散式鎖

程式設計式分散式鎖每次實現都要單獨實現,但業務量大功能複雜時,使用程式設計式分散式鎖無疑是痛苦的,而宣告式分散式鎖不同,宣告式分散式鎖屬於無侵入式,不會影響業務邏輯的實現。

我的為什麼要用:使用簡單,提升開發效率

2、怎麼實現

使用spring aop + 自定義註解來實現

下面來看下spring boot + 自定義註解 如何實現一個宣告式的分散式鎖

3、看程式碼

第一步、引入aop 需要 jar包
        <!-- SpringBoot AOP start -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>
        <!-- SpringBoot AOP end -->
第二步、@EnableAspectJAutoProxy 開啟AOP
@EnableAspectJAutoProxy
@SpringBootApplication
public class DemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}

}
第三步:自定義DistributionLock註解
import java.lang.annotation.*;

/**
 * 通常 和com.example.demo.annotation.DistributionLockParam 註解配合使用,降低鎖粒度
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DistributionLock {

    /**
     * 分散式鎖key
     */
    String key();

    /**
     * 嘗試獲得鎖的時間(單位:毫秒),預設值:5000毫秒
     *
     * @return 鎖key過期時間
     */
    long tryLockTime() default 5000;

    /**
     * 嘗試獲得鎖後,持有鎖的時間(單位:毫秒),預設值:60000毫秒
     *
     * @return
     */
    long holdLockTime() default 60000;

    /**
     * 分散式鎖key 的分隔符(預設 :)
     */
    String delimiter() default ":";

}
第四步:自定義DistributionLockParam註解
/**
 * 分散式鎖引數
 * 這個註解,是給DistributionLock用來控制鎖粒度的
 */
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DistributionLockParam {
}
第五步:定義分散式鎖AOP配置第五步:定義分散式鎖AOP配置
import com.example.demo.annotation.DistributionLock;
import com.example.demo.annotation.DistributionLockParam;
import com.example.demo.entity.Resp;
import com.example.demo.redisLock.LockParam;
import com.example.demo.redisLock.RedisLock;
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.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/**
 * 對springboot中aop切面程式設計的測試
 */
//切面類
@Aspect
@Component
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RedisAopAspect {
    public RedisAopAspect(){
        log.info("分佈鎖 aop init");
    }
    /***
     * 定義切入點
     */
    @Pointcut("execution(@com.example.demo.annotation.DistributionLock * *(..))")
    public void pointCut(){

    }


    @Around(value = "pointCut()")
    public Object aroundMethod(ProceedingJoinPoint pjp) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();

        /////////////////////AOP 能取得的資訊 start////////////////////////
//        log.info("目標方法名為:{}",pjp.getSignature().getName());
//        log.info("目標方法所屬類的簡單類名:{}" , pjp.getSignature().getDeclaringType().getSimpleName());
//        log.info("目標方法所屬類的類名:{}", pjp.getSignature().getDeclaringTypeName());
//        log.info("目標方法宣告型別:{}" , Modifier.toString(pjp.getSignature().getModifiers()));
//        log.info("目標方法返回值型別:{}" , method.getReturnType());
//        //獲取傳入目標方法的引數
//        Object[] args = pjp.getArgs();
//        for (int i = 0; i < args.length; i++) {
//            log.info("第{}個引數為:{}" ,(i + 1) , args[i]);
//        }
//        log.info("被代理的物件:{}" , pjp.getTarget());
//        log.info("代理物件自己:{}" , pjp.getThis());
        /////////////////////AOP 能取得的資訊 end////////////////////////

        //取得註解物件資料
        DistributionLock lock = method.getAnnotation(DistributionLock.class);
        //分散式鎖實際的key
        String lockKey = getRealDistributionLockKey(pjp,lock);
        //建立分散式鎖物件 start
        LockParam lockParam = new LockParam(lockKey,lock.tryLockTime(),lock.holdLockTime());
        RedisLock redisLock = new RedisLock(lockParam);
        //建立分散式鎖物件 end

        //獲取鎖
        Boolean holdLock = redisLock.lock();
        log.info("lockKey:{}   holdLock:{} ",lockKey,holdLock);
        if(Boolean.FALSE.equals(holdLock)){
            //獲取鎖失敗後,處理返回結果
            return handleAcquireLockFailReturn(pjp);
        }

        try {
            return pjp.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }finally {
            if(redisLock!=null){
                Boolean unlock = redisLock.unlock();
                log.info("釋放鎖:unlock {}",unlock);
            }
        }
    }

    /**
     * 分散式鎖獲取失敗,處理方法
     * @param pjp
     * @return
     */
    public Object handleAcquireLockFailReturn(ProceedingJoinPoint pjp){
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        Class returnType = method.getReturnType();
        //通常每個公司都有自己的統一的返回物件,Resp.class可以根據自己現有的
        if(returnType.equals(Resp.class) ){
            log.info("返回值型別 Resp");
            return Resp.buildFail("業務處理繁忙,請稍後重試");
        }

        throw new RuntimeException("獲取鎖失敗");
    }


    /**
     * 加了DistributionLockParam註解引數值,按照順序返回list
     * @param pjp
     * @return
     */
    public List<Object> getDistributionLockParamList(ProceedingJoinPoint pjp){
        ArrayList<Object> distributionLockParamList = null;
        MethodSignature signature = ((MethodSignature) pjp.getSignature());
        //得到攔截的方法
        Method method = signature.getMethod();
        //獲取方法引數註解,返回二維陣列是因為某些引數可能存在多個註解
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
//        log.info("parameterAnnotations:{}",parameterAnnotations);
        //獲取全部引數
        Object[] objects = pjp.getArgs();
        for(int i = 0; i < parameterAnnotations.length; i++){
            for(Annotation a: parameterAnnotations[i]){
                if(a.annotationType() == DistributionLockParam.class){
                    //初始化distributionLockParamList
                    if(distributionLockParamList==null){
                        distributionLockParamList = new ArrayList();
                    }
                    //獲得引數值
                    Object o = objects[i];
                    distributionLockParamList.add(o);
                }
            }
        }

        return distributionLockParamList;
    }

    /**
     * 加了DistributionLockParam註解引數值,拼接成字串
     * @param pjp
     * @param lock
     * @return
     */
    public String getDistributionLockParamStr(ProceedingJoinPoint pjp,DistributionLock lock){
        List<Object> distributionLockParamList = getDistributionLockParamList(pjp);
        if(distributionLockParamList!=null && distributionLockParamList.size()>0){
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < distributionLockParamList.size(); i++) {
                Object param = distributionLockParamList.get(i);
                sb.append(lock.delimiter());
                sb.append(param);
            }
            return sb.toString();
        }
        return "";
    }

    /**
     * 返回分散式鎖key完整的key
     * @param pjp
     * @param lock
     * @return
     */
    public String getRealDistributionLockKey(ProceedingJoinPoint pjp,DistributionLock lock){
        String distributionLockParamStr = getDistributionLockParamStr(pjp,lock);
        return lock.key().concat(distributionLockParamStr);
    }

}

LockParam和RedisLock類已經在 【redis分散式鎖-java實現】文章裡面有,這裡就不貼出來了


Service 使用例子
import com.example.demo.entity.Resp;

public interface IOrderService {
    Resp updateOrder(String orderCode, Integer userId, Integer status);
}
import com.example.demo.annotation.DistributionLock;
import com.example.demo.annotation.DistributionLockParam;
import com.example.demo.entity.Resp;
import com.example.demo.service.IOrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class OrderServiceImpl implements IOrderService {


    @Override
    @DistributionLock(key = "updateOrderSatus",tryLockTime = 1000)
    public Resp updateOrder(@DistributionLockParam String orderCode, Integer userId, Integer status){
        try {
            log.info("updateOrder 處理業務 start");
            Thread.sleep(1000*10);
            log.info("updateOrder 處理業務 end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return Resp.buildSuccess("修改訂單狀態成功");
    }

}

collection呼叫service
import com.example.demo.entity.Resp;
import com.example.demo.service.IOrderService;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping(value = "/v1/test")
public class TestController {
    @Autowired
    IOrderService orderService;

    @ApiOperation(value = "修改訂單狀態")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "orderCode", value = "訂單編號", paramType = "query"),
            @ApiImplicitParam(name = "userId", value = "使用者ID", paramType = "query"),
            @ApiImplicitParam(name = "status", value = "訂單狀態 1:未發貨 2:已發貨 3:完成", paramType = "query"),
    })
    @RequestMapping(value = "/updateOrderStatus", method = RequestMethod.PUT)
    public Resp updateOrderStatus(@RequestParam(value = "orderCode")String orderCode,
                                @RequestParam(value = "userId")Integer userId,
                                @RequestParam(value = "status")Integer status){
        log.info("updateOrderStatus reqParam:orderCode:{},userId:{},status:{}",orderCode,userId,status);
        return orderService.updateOrder(orderCode,userId,status);
    }

}

4、瀏覽器swagger呼叫

image

5、下面呼叫一次這個介面,控制檯列印的日誌

Connected to the target VM, address: '127.0.0.1:63200', transport: 'socket'

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.5.0)

2021-05-25 23:13:10.967  INFO 57853 --- [           main] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default
2021-05-25 23:13:11.993  INFO 57853 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2021-05-25 23:13:12.000  INFO 57853 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2021-05-25 23:13:12.000  INFO 57853 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.46]
2021-05-25 23:13:12.066  INFO 57853 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2021-05-25 23:13:12.066  INFO 57853 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1059 ms
2021-05-25 23:13:12.119  INFO 57853 --- [           main] com.example.demo.config.RedisAopAspect   : 分佈鎖 aop init
2021-05-25 23:13:12.694  INFO 57853 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2021-05-25 23:13:12.695  INFO 57853 --- [           main] d.s.w.p.DocumentationPluginsBootstrapper : Context refreshed
2021-05-25 23:13:12.705  INFO 57853 --- [           main] d.s.w.p.DocumentationPluginsBootstrapper : Found 1 custom documentation plugin(s)
2021-05-25 23:13:12.715  INFO 57853 --- [           main] s.d.s.w.s.ApiListingReferenceScanner     : Scanning for api listing references
2021-05-25 23:13:12.880  INFO 57853 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 2.249 seconds (JVM running for 2.777)
2021-05-25 23:13:12.882  INFO 57853 --- [           main] o.s.b.a.ApplicationAvailabilityBean      : Application availability state LivenessState changed to CORRECT
2021-05-25 23:13:12.883  INFO 57853 --- [           main] o.s.b.a.ApplicationAvailabilityBean      : Application availability state ReadinessState changed to ACCEPTING_TRAFFIC
2021-05-25 23:19:58.582  INFO 57853 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2021-05-25 23:19:58.582  INFO 57853 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2021-05-25 23:19:58.583  INFO 57853 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
2021-05-25 23:20:11.970  INFO 57853 --- [nio-8080-exec-2] c.e.demo.controller.TestController       : updateOrderStatus reqParam:orderCode:1,userId:2,status:3
2021-05-25 23:20:11.996  INFO 57853 --- [nio-8080-exec-2] com.example.demo.config.RedisAopAspect   : lockKey:updateOrderSatus:1   holdLock:true 
2021-05-25 23:20:12.000  INFO 57853 --- [nio-8080-exec-2] c.e.demo.service.impl.OrderServiceImpl   : updateOrder 處理業務 start
2021-05-25 23:20:22.005  INFO 57853 --- [nio-8080-exec-2] c.e.demo.service.impl.OrderServiceImpl   : updateOrder 處理業務 end
2021-05-25 23:20:22.007  INFO 57853 --- [nio-8080-exec-2] com.example.demo.config.RedisAopAspect   : 釋放鎖:unlock true

spring boot啟動類:com.example.demo.DemoApplication
swagger 地址:http://127.0.0.1:8080/swagger-ui.html

其他:

如果宣告式的分散式鎖想實現可重入機制,可以把new RedisLock(lockParam),替換成這篇文章的 redis分散式鎖-可重入鎖 就能實現了

程式碼我已經上傳到gitee上了,大家可以下載玩一下,記得star一下

gitee 程式碼傳送門

相關文章