You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

README.md 12 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. # spring-boot-demo-zookeeper
  2. > 此 demo 主要演示了如何使用 Spring Boot 集成 Zookeeper 结合AOP实现分布式锁。
  3. ## pom.xml
  4. ```xml
  5. <?xml version="1.0" encoding="UTF-8"?>
  6. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  7. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  8. <modelVersion>4.0.0</modelVersion>
  9. <artifactId>spring-boot-demo-zookeeper</artifactId>
  10. <version>1.0.0-SNAPSHOT</version>
  11. <packaging>jar</packaging>
  12. <name>spring-boot-demo-zookeeper</name>
  13. <description>Demo project for Spring Boot</description>
  14. <parent>
  15. <groupId>com.xkcoding</groupId>
  16. <artifactId>spring-boot-demo</artifactId>
  17. <version>1.0.0-SNAPSHOT</version>
  18. </parent>
  19. <properties>
  20. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  21. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  22. <java.version>1.8</java.version>
  23. </properties>
  24. <dependencies>
  25. <dependency>
  26. <groupId>org.springframework.boot</groupId>
  27. <artifactId>spring-boot-starter-web</artifactId>
  28. </dependency>
  29. <dependency>
  30. <groupId>org.springframework.boot</groupId>
  31. <artifactId>spring-boot-configuration-processor</artifactId>
  32. <optional>true</optional>
  33. </dependency>
  34. <dependency>
  35. <groupId>org.springframework.boot</groupId>
  36. <artifactId>spring-boot-starter-aop</artifactId>
  37. </dependency>
  38. <!-- curator 版本4.1.0 对应 zookeeper 版本 3.5.x -->
  39. <!-- curator 与 zookeeper 版本对应关系:https://curator.apache.org/zk-compatibility.html -->
  40. <dependency>
  41. <groupId>org.apache.curator</groupId>
  42. <artifactId>curator-recipes</artifactId>
  43. <version>4.1.0</version>
  44. </dependency>
  45. <dependency>
  46. <groupId>cn.hutool</groupId>
  47. <artifactId>hutool-all</artifactId>
  48. </dependency>
  49. <dependency>
  50. <groupId>org.springframework.boot</groupId>
  51. <artifactId>spring-boot-starter-test</artifactId>
  52. <scope>test</scope>
  53. </dependency>
  54. <dependency>
  55. <groupId>org.projectlombok</groupId>
  56. <artifactId>lombok</artifactId>
  57. <optional>true</optional>
  58. </dependency>
  59. </dependencies>
  60. <build>
  61. <finalName>spring-boot-demo-zookeeper</finalName>
  62. <plugins>
  63. <plugin>
  64. <groupId>org.springframework.boot</groupId>
  65. <artifactId>spring-boot-maven-plugin</artifactId>
  66. </plugin>
  67. </plugins>
  68. </build>
  69. </project>
  70. ```
  71. ## ZkProps.java
  72. ```java
  73. /**
  74. * <p>
  75. * Zookeeper 配置项
  76. * </p>
  77. *
  78. * @author yangkai.shen
  79. * @date Created in 2018-12-27 14:47
  80. */
  81. @Data
  82. @ConfigurationProperties(prefix = "zk")
  83. public class ZkProps {
  84. /**
  85. * 连接地址
  86. */
  87. private String url;
  88. /**
  89. * 超时时间(毫秒),默认1000
  90. */
  91. private int timeout = 1000;
  92. /**
  93. * 重试次数,默认3
  94. */
  95. private int retry = 3;
  96. }
  97. ```
  98. ## application.yml
  99. ```yaml
  100. server:
  101. port: 8080
  102. servlet:
  103. context-path: /demo
  104. zk:
  105. url: 127.0.0.1:2181
  106. timeout: 1000
  107. retry: 3
  108. ```
  109. ## ZkConfig.java
  110. ```java
  111. /**
  112. * <p>
  113. * Zookeeper配置类
  114. * </p>
  115. *
  116. * @author yangkai.shen
  117. * @date Created in 2018-12-27 14:45
  118. */
  119. @Configuration
  120. @EnableConfigurationProperties(ZkProps.class)
  121. public class ZkConfig {
  122. private final ZkProps zkProps;
  123. @Autowired
  124. public ZkConfig(ZkProps zkProps) {
  125. this.zkProps = zkProps;
  126. }
  127. @Bean
  128. public CuratorFramework curatorFramework() {
  129. RetryPolicy retryPolicy = new ExponentialBackoffRetry(zkProps.getTimeout(), zkProps.getRetry());
  130. CuratorFramework client = CuratorFrameworkFactory.newClient(zkProps.getUrl(), retryPolicy);
  131. client.start();
  132. return client;
  133. }
  134. }
  135. ```
  136. ## ZooLock.java
  137. > 分布式锁的关键注解
  138. ```java
  139. /**
  140. * <p>
  141. * 基于Zookeeper的分布式锁注解
  142. * 在需要加锁的方法上打上该注解后,AOP会帮助你统一管理这个方法的锁
  143. * </p>
  144. *
  145. * @author yangkai.shen
  146. * @date Created in 2018-12-27 14:11
  147. */
  148. @Target({ElementType.METHOD})
  149. @Retention(RetentionPolicy.RUNTIME)
  150. @Documented
  151. @Inherited
  152. public @interface ZooLock {
  153. /**
  154. * 分布式锁的键
  155. */
  156. String key();
  157. /**
  158. * 锁释放时间,默认五秒
  159. */
  160. long timeout() default 5 * 1000;
  161. /**
  162. * 时间格式,默认:毫秒
  163. */
  164. TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
  165. }
  166. ```
  167. ## LockKeyParam.java
  168. > 分布式锁动态key的关键注解
  169. ```java
  170. /**
  171. * <p>
  172. * 分布式锁动态key注解,配置之后key的值会动态获取参数内容
  173. * </p>
  174. *
  175. * @author yangkai.shen
  176. * @date Created in 2018-12-27 14:17
  177. */
  178. @Target({ElementType.PARAMETER})
  179. @Retention(RetentionPolicy.RUNTIME)
  180. @Documented
  181. @Inherited
  182. public @interface LockKeyParam {
  183. /**
  184. * 如果动态key在user对象中,那么就需要设置fields的值为user对象中的属性名可以为多个,基本类型则不需要设置该值
  185. * <p>例1:public void count(@LockKeyParam({"id"}) User user)
  186. * <p>例2:public void count(@LockKeyParam({"id","userName"}) User user)
  187. * <p>例3:public void count(@LockKeyParam String userId)
  188. */
  189. String[] fields() default {};
  190. }
  191. ```
  192. ## ZooLockAspect.java
  193. > 分布式锁的关键部分
  194. ```java
  195. /**
  196. * <p>
  197. * 使用 aop 切面记录请求日志信息
  198. * </p>
  199. *
  200. * @author yangkai.shen
  201. * @date Created in 2018-10-01 22:05
  202. */
  203. @Aspect
  204. @Component
  205. @Slf4j
  206. public class ZooLockAspect {
  207. private final CuratorFramework zkClient;
  208. private static final String KEY_PREFIX = "DISTRIBUTED_LOCK_";
  209. private static final String KEY_SEPARATOR = "/";
  210. @Autowired
  211. public ZooLockAspect(CuratorFramework zkClient) {
  212. this.zkClient = zkClient;
  213. }
  214. /**
  215. * 切入点
  216. */
  217. @Pointcut("@annotation(com.xkcoding.zookeeper.annotation.ZooLock)")
  218. public void doLock() {
  219. }
  220. /**
  221. * 环绕操作
  222. *
  223. * @param point 切入点
  224. * @return 原方法返回值
  225. * @throws Throwable 异常信息
  226. */
  227. @Around("doLock()")
  228. public Object around(ProceedingJoinPoint point) throws Throwable {
  229. MethodSignature signature = (MethodSignature) point.getSignature();
  230. Method method = signature.getMethod();
  231. Object[] args = point.getArgs();
  232. ZooLock zooLock = method.getAnnotation(ZooLock.class);
  233. if (StrUtil.isBlank(zooLock.key())) {
  234. throw new RuntimeException("分布式锁键不能为空");
  235. }
  236. String lockKey = buildLockKey(zooLock, method, args);
  237. InterProcessMutex lock = new InterProcessMutex(zkClient, lockKey);
  238. try {
  239. // 假设上锁成功,以后拿到的都是 false
  240. if (lock.acquire(zooLock.timeout(), zooLock.timeUnit())) {
  241. return point.proceed();
  242. } else {
  243. throw new RuntimeException("请勿重复提交");
  244. }
  245. } finally {
  246. lock.release();
  247. }
  248. }
  249. /**
  250. * 构造分布式锁的键
  251. *
  252. * @param lock 注解
  253. * @param method 注解标记的方法
  254. * @param args 方法上的参数
  255. * @return
  256. * @throws NoSuchFieldException
  257. * @throws IllegalAccessException
  258. */
  259. private String buildLockKey(ZooLock lock, Method method, Object[] args) throws NoSuchFieldException, IllegalAccessException {
  260. StringBuilder key = new StringBuilder(KEY_SEPARATOR + KEY_PREFIX + lock.key());
  261. // 迭代全部参数的注解,根据使用LockKeyParam的注解的参数所在的下标,来获取args中对应下标的参数值拼接到前半部分key上
  262. Annotation[][] parameterAnnotations = method.getParameterAnnotations();
  263. for (int i = 0; i < parameterAnnotations.length; i++) {
  264. // 循环该参数全部注解
  265. for (Annotation annotation : parameterAnnotations[i]) {
  266. // 注解不是 @LockKeyParam
  267. if (!annotation.annotationType().isInstance(LockKeyParam.class)) {
  268. continue;
  269. }
  270. // 获取所有fields
  271. String[] fields = ((LockKeyParam) annotation).fields();
  272. if (ArrayUtil.isEmpty(fields)) {
  273. // 普通数据类型直接拼接
  274. if (ObjectUtil.isNull(args[i])) {
  275. throw new RuntimeException("动态参数不能为null");
  276. }
  277. key.append(KEY_SEPARATOR).append(args[i]);
  278. } else {
  279. // @LockKeyParam的fields值不为null,所以当前参数应该是对象类型
  280. for (String field : fields) {
  281. Class<?> clazz = args[i].getClass();
  282. Field declaredField = clazz.getDeclaredField(field);
  283. declaredField.setAccessible(true);
  284. Object value = declaredField.get(clazz);
  285. key.append(KEY_SEPARATOR).append(value);
  286. }
  287. }
  288. }
  289. }
  290. return key.toString();
  291. }
  292. }
  293. ```
  294. ## SpringBootDemoZookeeperApplicationTests.java
  295. > 测试分布式锁
  296. ```java
  297. @RunWith(SpringRunner.class)
  298. @SpringBootTest
  299. @Slf4j
  300. public class SpringBootDemoZookeeperApplicationTests {
  301. public Integer getCount() {
  302. return count;
  303. }
  304. private Integer count = 10000;
  305. private ExecutorService executorService = Executors.newFixedThreadPool(1000);
  306. @Autowired
  307. private CuratorFramework zkClient;
  308. /**
  309. * 不使用分布式锁,程序结束查看count的值是否为0
  310. */
  311. @Test
  312. public void test() throws InterruptedException {
  313. IntStream.range(0, 10000).forEach(i -> executorService.execute(this::doBuy));
  314. TimeUnit.MINUTES.sleep(1);
  315. log.error("count值为{}", count);
  316. }
  317. /**
  318. * 测试AOP分布式锁
  319. */
  320. @Test
  321. public void testAopLock() throws InterruptedException {
  322. // 测试类中使用AOP需要手动代理
  323. SpringBootDemoZookeeperApplicationTests target = new SpringBootDemoZookeeperApplicationTests();
  324. AspectJProxyFactory factory = new AspectJProxyFactory(target);
  325. ZooLockAspect aspect = new ZooLockAspect(zkClient);
  326. factory.addAspect(aspect);
  327. SpringBootDemoZookeeperApplicationTests proxy = factory.getProxy();
  328. IntStream.range(0, 10000).forEach(i -> executorService.execute(() -> proxy.aopBuy(i)));
  329. TimeUnit.MINUTES.sleep(1);
  330. log.error("count值为{}", proxy.getCount());
  331. }
  332. /**
  333. * 测试手动加锁
  334. */
  335. @Test
  336. public void testManualLock() throws InterruptedException {
  337. IntStream.range(0, 10000).forEach(i -> executorService.execute(this::manualBuy));
  338. TimeUnit.MINUTES.sleep(1);
  339. log.error("count值为{}", count);
  340. }
  341. @ZooLock(key = "buy", timeout = 1, timeUnit = TimeUnit.MINUTES)
  342. public void aopBuy(int userId) {
  343. log.info("{} 正在出库。。。", userId);
  344. doBuy();
  345. log.info("{} 扣库存成功。。。", userId);
  346. }
  347. public void manualBuy() {
  348. String lockPath = "/buy";
  349. log.info("try to buy sth.");
  350. try {
  351. InterProcessMutex lock = new InterProcessMutex(zkClient, lockPath);
  352. try {
  353. if (lock.acquire(1, TimeUnit.MINUTES)) {
  354. doBuy();
  355. log.info("buy successfully!");
  356. }
  357. } finally {
  358. lock.release();
  359. }
  360. } catch (Exception e) {
  361. log.error("zk error");
  362. }
  363. }
  364. public void doBuy() {
  365. count--;
  366. log.info("count值为{}", count);
  367. }
  368. }
  369. ```
  370. ## 参考
  371. 1. [如何在测试类中使用 AOP](https://stackoverflow.com/questions/11436600/unit-testing-spring-around-aop-methods)
  372. 2. zookeeper 实现分布式锁:《Spring Boot 2精髓 从构建小系统到架构分布式大系统》李家智 - 第16章 - Spring Boot 和 Zoo Keeper - 16.3 实现分布式锁