diff --git a/spring-boot-demo-zookeeper/pom.xml b/spring-boot-demo-zookeeper/pom.xml index ff6c9b0..b3fdb99 100644 --- a/spring-boot-demo-zookeeper/pom.xml +++ b/spring-boot-demo-zookeeper/pom.xml @@ -28,11 +28,41 @@ spring-boot-starter-web + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot-starter-aop + + + + + + org.apache.curator + curator-recipes + 4.1.0 + + + + cn.hutool + hutool-all + + org.springframework.boot spring-boot-starter-test test + + + org.projectlombok + lombok + true + diff --git a/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/SpringBootDemoZookeeperApplication.java b/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/SpringBootDemoZookeeperApplication.java index 29ee679..24867cf 100644 --- a/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/SpringBootDemoZookeeperApplication.java +++ b/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/SpringBootDemoZookeeperApplication.java @@ -3,6 +3,19 @@ package com.xkcoding.zookeeper; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +/** + *

+ * 启动器 + *

+ * + * @package: com.xkcoding.zookeeper + * @description: 启动器 + * @author: yangkai.shen + * @date: Created in 2018-12-27 14:51 + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ @SpringBootApplication public class SpringBootDemoZookeeperApplication { diff --git a/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/annotation/LockKeyParam.java b/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/annotation/LockKeyParam.java new file mode 100644 index 0000000..c010e6b --- /dev/null +++ b/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/annotation/LockKeyParam.java @@ -0,0 +1,30 @@ +package com.xkcoding.zookeeper.annotation; + +import java.lang.annotation.*; + +/** + *

+ * 分布式锁动态key注解,配置之后key的值会动态获取参数内容 + *

+ * + * @package: com.xkcoding.zookeeper.annotation + * @description: 分布式锁动态key注解,配置之后key的值会动态获取参数内容 + * @author: yangkai.shen + * @date: Created in 2018-12-27 14:17 + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface LockKeyParam { + /** + * 如果动态key在user对象中,那么就需要设置fields的值为user对象中的属性名可以为多个,基本类型则不需要设置该值 + *

例1:public void count(@LockKeyParam({"id"}) User user) + *

例2:public void count(@LockKeyParam({"id","userName"}) User user) + *

例3:public void count(@LockKeyParam String userId) + */ + String[] fields() default {}; +} diff --git a/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/annotation/ZooLock.java b/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/annotation/ZooLock.java new file mode 100644 index 0000000..6be1120 --- /dev/null +++ b/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/annotation/ZooLock.java @@ -0,0 +1,39 @@ +package com.xkcoding.zookeeper.annotation; + +import java.lang.annotation.*; +import java.util.concurrent.TimeUnit; + +/** + *

+ * 基于Zookeeper的分布式锁注解 + * 在需要加锁的方法上打上该注解后,AOP会帮助你统一管理这个方法的锁 + *

+ * + * @package: com.xkcoding.zookeeper.annotation + * @description: 基于Zookeeper的分布式锁注解,在需要加锁的方法上打上该注解后,AOP会帮助你统一管理这个方法的锁 + * @author: yangkai.shen + * @date: Created in 2018-12-27 14:11 + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface ZooLock { + /** + * 分布式锁的键 + */ + String key(); + + /** + * 锁释放时间,默认五秒 + */ + long timeout() default 5 * 1000; + + /** + * 时间格式,默认:毫秒 + */ + TimeUnit timeUnit() default TimeUnit.MILLISECONDS; +} diff --git a/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/aspectj/ZooLockAspect.java b/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/aspectj/ZooLockAspect.java new file mode 100644 index 0000000..de1bb3d --- /dev/null +++ b/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/aspectj/ZooLockAspect.java @@ -0,0 +1,136 @@ +package com.xkcoding.zookeeper.aspectj; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.xkcoding.zookeeper.annotation.LockKeyParam; +import com.xkcoding.zookeeper.annotation.ZooLock; +import lombok.extern.slf4j.Slf4j; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.recipes.locks.InterProcessMutex; +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.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + *

+ * 使用 aop 切面记录请求日志信息 + *

+ * + * @package: com.xkcoding.log.aop.aspectj + * @description: 使用 aop 切面记录请求日志信息 + * @author: yangkai.shen + * @date: Created in 2018/10/1 10:05 PM + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ +@Aspect +@Component +@Slf4j +public class ZooLockAspect { + private final CuratorFramework zkClient; + + private static final String KEY_PREFIX = "DISTRIBUTED_LOCK_"; + + private static final String KEY_SEPARATOR = "/"; + + @Autowired + public ZooLockAspect(CuratorFramework zkClient) { + this.zkClient = zkClient; + } + + /** + * 切入点 + */ + @Pointcut("@annotation(com.xkcoding.zookeeper.annotation.ZooLock)") + public void doLock() { + + } + + /** + * 环绕操作 + * + * @param point 切入点 + * @return 原方法返回值 + * @throws Throwable 异常信息 + */ + @Around("doLock()") + public Object around(ProceedingJoinPoint point) throws Throwable { + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + Object[] args = point.getArgs(); + ZooLock zooLock = method.getAnnotation(ZooLock.class); + if (StrUtil.isBlank(zooLock.key())) { + throw new RuntimeException("分布式锁键不能为空"); + } + String lockKey = buildLockKey(zooLock, method, args); + InterProcessMutex lock = new InterProcessMutex(zkClient, lockKey); + try { + // 假设上锁成功,以后拿到的都是 false + if (lock.acquire(zooLock.timeout(), zooLock.timeUnit())) { + return point.proceed(); + } else { + throw new RuntimeException("请勿重复提交"); + } + } finally { + lock.release(); + } + } + + /** + * 构造分布式锁的键 + * + * @param lock 注解 + * @param method 注解标记的方法 + * @param args 方法上的参数 + * @return + * @throws NoSuchFieldException + * @throws IllegalAccessException + */ + private String buildLockKey(ZooLock lock, Method method, Object[] args) throws NoSuchFieldException, IllegalAccessException { + StringBuilder key = new StringBuilder(KEY_SEPARATOR + KEY_PREFIX + lock.key()); + + // 迭代全部参数的注解,根据使用LockKeyParam的注解的参数所在的下标,来获取args中对应下标的参数值拼接到前半部分key上 + Annotation[][] parameterAnnotations = method.getParameterAnnotations(); + + for (int i = 0; i < parameterAnnotations.length; i++) { + // 循环该参数全部注解 + for (Annotation annotation : parameterAnnotations[i]) { + // 注解不是 @LockKeyParam + if (!annotation.annotationType().isInstance(LockKeyParam.class)) { + continue; + } + + // 获取所有fields + String[] fields = ((LockKeyParam) annotation).fields(); + if (ArrayUtil.isEmpty(fields)) { + // 普通数据类型直接拼接 + if (ObjectUtil.isNull(args[i])) { + throw new RuntimeException("动态参数不能为null"); + } + key.append(KEY_SEPARATOR).append(args[i]); + } else { + // @LockKeyParam的fields值不为null,所以当前参数应该是对象类型 + for (String field : fields) { + Class clazz = args[i].getClass(); + Field declaredField = clazz.getDeclaredField(field); + declaredField.setAccessible(true); + Object value = declaredField.get(clazz); + key.append(KEY_SEPARATOR).append(value); + } + } + } + } + return key.toString(); + } + +} diff --git a/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/config/ZkConfig.java b/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/config/ZkConfig.java new file mode 100644 index 0000000..5f5a656 --- /dev/null +++ b/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/config/ZkConfig.java @@ -0,0 +1,43 @@ +package com.xkcoding.zookeeper.config; + +import com.xkcoding.zookeeper.config.props.ZkProps; +import org.apache.curator.RetryPolicy; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.retry.ExponentialBackoffRetry; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + *

+ * Zookeeper配置类 + *

+ * + * @package: com.xkcoding.zookeeper.config + * @description: Zookeeper配置类 + * @author: yangkai.shen + * @date: Created in 2018-12-27 14:45 + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ +@Configuration +@EnableConfigurationProperties(ZkProps.class) +public class ZkConfig { + private final ZkProps zkProps; + + @Autowired + public ZkConfig(ZkProps zkProps) { + this.zkProps = zkProps; + } + + @Bean + public CuratorFramework curatorFramework() { + RetryPolicy retryPolicy = new ExponentialBackoffRetry(zkProps.getTimeout(), zkProps.getRetry()); + CuratorFramework client = CuratorFrameworkFactory.newClient(zkProps.getUrl(), retryPolicy); + client.start(); + return client; + } +} diff --git a/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/config/props/ZkProps.java b/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/config/props/ZkProps.java new file mode 100644 index 0000000..a944fb7 --- /dev/null +++ b/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/config/props/ZkProps.java @@ -0,0 +1,36 @@ +package com.xkcoding.zookeeper.config.props; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + *

+ * Zookeeper 配置项 + *

+ * + * @package: com.xkcoding.zookeeper.config.props + * @description: Zookeeper 配置项 + * @author: yangkai.shen + * @date: Created in 2018-12-27 14:47 + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ +@Data +@ConfigurationProperties(prefix = "zk") +public class ZkProps { + /** + * 连接地址 + */ + private String url; + + /** + * 超时时间(毫秒),默认1000 + */ + private int timeout = 1000; + + /** + * 重试次数,默认3 + */ + private int retry = 3; +} diff --git a/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/controller/TestController.java b/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/controller/TestController.java new file mode 100644 index 0000000..3db3ac6 --- /dev/null +++ b/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/controller/TestController.java @@ -0,0 +1,42 @@ +package com.xkcoding.zookeeper.controller; + +import cn.hutool.core.lang.Dict; +import com.xkcoding.zookeeper.annotation.LockKeyParam; +import com.xkcoding.zookeeper.annotation.ZooLock; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.concurrent.TimeUnit; + +/** + *

+ * 测试Controller + *

+ * + * @package: com.xkcoding.zookeeper.controller + * @description: 测试Controller + * @author: yangkai.shen + * @date: Created in 2018-12-27 15:51 + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ +@RestController +@Slf4j +public class TestController { + + @GetMapping("/buy") + public Dict buy(int userId) throws InterruptedException { + log.info("{} 购买中。。。", userId); + doBuy(userId); + return Dict.create().set("flag", true).set("msg", "秒杀成功").set("data", userId); + } + + @ZooLock(key = "buy", timeout = 1, timeUnit = TimeUnit.MINUTES) + private void doBuy(@LockKeyParam int userId) throws InterruptedException { + log.info("{} 正在出库。。。", userId); + TimeUnit.SECONDS.sleep(5); + log.info("{} 扣库存成功。。。", userId); + } +} diff --git a/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/util/ZkLockHelper.java b/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/util/ZkLockHelper.java new file mode 100644 index 0000000..c7cacda --- /dev/null +++ b/spring-boot-demo-zookeeper/src/main/java/com/xkcoding/zookeeper/util/ZkLockHelper.java @@ -0,0 +1,64 @@ +package com.xkcoding.zookeeper.util; + +import lombok.extern.slf4j.Slf4j; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.recipes.locks.InterProcessMutex; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + *

+ * Zookeeper分布式锁工具类 + *

+ * + * @package: com.xkcoding.zookeeper.util + * @description: Zookeeper分布式锁工具类 + * @author: yangkai.shen + * @date: Created in 2018-12-27 14:55 + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ +@Slf4j +@Component +public class ZkLockHelper { + private final CuratorFramework zkClient; + + @Autowired + public ZkLockHelper(CuratorFramework zkClient) { + this.zkClient = zkClient; + } + + /** + * 尝试获取锁 + * + * @param key 锁的键 + * @param timeout 超时时间 + * @param unit 单位 + * @return 是否获取锁 + */ + public boolean tryLock(String key, long timeout, TimeUnit unit) { + InterProcessMutex lock = new InterProcessMutex(zkClient, key); + try { + return lock.acquire(timeout, unit); + } catch (Exception e) { + throw new RuntimeException("系统异常"); + } + } + + /** + * 释放锁 + * + * @param key 锁的键 + */ + public void unLock(String key) { + InterProcessMutex lock = new InterProcessMutex(zkClient, key); + try { + lock.release(); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } +} diff --git a/spring-boot-demo-zookeeper/src/main/resources/application.properties b/spring-boot-demo-zookeeper/src/main/resources/application.properties deleted file mode 100644 index e69de29..0000000 diff --git a/spring-boot-demo-zookeeper/src/main/resources/application.yml b/spring-boot-demo-zookeeper/src/main/resources/application.yml new file mode 100644 index 0000000..2a79b21 --- /dev/null +++ b/spring-boot-demo-zookeeper/src/main/resources/application.yml @@ -0,0 +1,9 @@ +server: + port: 8080 + servlet: + context-path: /demo + +zk: + url: 127.0.0.1:2181 + timeout: 1000 + retry: 3