首页 > 经验记录

 

 

万字长文警告 !建议在首页看的,点击标题进入文章页查看,好看点。

 

本文章又名: 兼容 Spring 体系的 java 框架实现妙计

 

 

前排先提示一波:   读我这篇文章可以没看过 MyBatis 源码,但是 Spring 源码最好是要看过的,因为很多东西我不会解释,没看过 Spring 源码的估摸着会有些懵逼。

 

看完这篇文章的话,基本上可以自己手写兼容Spring体系的框架了。

本文章大概能解答这些问题:

如何使用Spring提供的扫描器?

MyBatis 注解生成 mapper 的流程是怎样的?

通过注解选择配置的启用与否是如何实现的?

如何注入自己的动态代理对象到Spring IOC容器 (重点)

在注入自定义 BeanDefinition 到Spring IOC容器中时,FactoryBean 起到个什么作用?

 

 

 

先看效果,再谈实现

首先,我写的这个demo项目里只有一个dependency

 

然后,我有俩Dao接口,都提供了一个query()方法

 

最后,这是启动类和启动配置:

 

这是运行后的控制台打印:

——-> SELECT name FROM user WHERE id = 1
——-> SELECT name FROM user WHERE id = 2

 

这里先说明一下,JDBC的流程我是没写的,因为这个不是重点。我生成的动态代理对象只是单纯的解析了一下@Select注解 然后将其中的value值打印了出来而已。

 

 

如何实现 ?

首先,肯定是要实现这么个效果: 将自己自定义对象注入到Spring IOC 容器中。

但是Spring IOC容器内部,保存具体Bean实例的是 singletonObjects 这个ConcurrentHashMap (我这默认只说单例Bean啊)。而向这玩意里边塞值的方式 Spring 是没有提供给我们的,他只能通过Spring扫描Resouce资源然后解析为BeanDefinition再才能从getBean时解析BeanDefinition实例化对象放入此单例缓存中。

既然我们自己没有方法可以直接往单例缓存中塞值,那我们可以采取一点迂回战术。

想要实现注入这种效果,根据Spring IOC的加载Bean、使用Bean流程来说,提供了好几个不同的钩子方法给你自己实现然后在他整个Bean生命周期、IOC容器生命周期的不同阶段调用。

比如:  BeanFactoryPostProcessor

BeanFactoryPostProcessor: bean工厂的bean属性处理容器,主要是可以实现该接口从而管理我们的bean工厂内所有的BeanDefinition数据,可以随心所欲的修改属性。

BeanFactoryPostProcessor 的机制就相当于给了我们在 bean 实例化之前最后一次修改 BeanDefinition 的机会,我们可以利用这个机会对 BeanDefinition 来进行一些额外的操作,比如更改某些 bean 的一些属性,给某些 Bean 增加一些其他的信息等等操作。

BeanFactoryPostProcessor提供给我们实现的方法 postProcessBeanFactory() 中可以得到当前beanFactory的对象,然后通过该对象的 registerSingleton() 方法就可以注册一个Bean。

使用起来也很简单,比如我这样,就成功的往Spring IOC 容器中注册了一个name为”123asd”的String对象:

 

但是,用 BeanFactoryPostProcessor 不好。因为他是专门为了给你进行BeanDefinition的修改而生的,比如说你想改某个框架,就可能用到此类。

到这个钩子调用的阶段,BeanDefinition早就扫描完了,还没开始实例化。你在这强行注入已经实例化的对象到 singletonObject 里,用是可以用,但是违反了相应规范,也指不定会出什么未知bug。

 

我们最好还是能够在Spring的扫描阶段来注册BeanDefinition,保持和 Spring IOC 容器流程一致化。所幸Spring提供了专门的钩子用来给你注册BeanDefinition。只要有这个钩子,那就没问题了。

因为只要能够注入自己的BeanDefinition,在使用name获取对应的Bean时,要是获取不到,它就会试图获取此name对应的BeanDefinition,他只要一找到这玩意,就会开始实例化进程然后将其实例化,最后放入缓存后返回给调用者。

Spring提供的用来注册BeanDefinition的钩子有两个,一个是 BeanDefinitionRegistryPostProcessor,一个是 ImportBeanDefinitionRegistrar

BeanDefinitionRegistryPostProcessor也实现了BeanFactoryPostProcessor ,不过在实现此接口的时候可以无视BeanFactoryPostProcessor 提供的 postProcessBeanFactory()这个方法,一个类做一个事情,实现了BeanDefinitionRegistryPostProcessor就只用关心如何如何注册BeanDefinition就够了。

 

BeanDefinitionRegistryPostProcessor和ImportBeanDefinitionRegistrar的主要区别在于其提供给程序员实现的方法不同。

BeanDefinitionRegistryPostProcessor提供的方法仅带有一个BeanDefinitionRegistry参数 , 用于给你注册Bean。

而ImportBeanDefinitionRegistrar在其基础上,还额外带了一个参数 : AnnotationMetadata,此参数可以获取指定注解的属性

 

一般来说BeanDefinitionRegistryPostProcessor用来解析外部资源配置,ImportBeanDefinitionRegistrar解析注解配置。

以MyBatis来说,对应上边这俩的实现就是 :

MapperScannerConfigurer (实现BeanDefinitionRegistryPostProcessor)

MapperScannerRegistrar (实现 ImportBeanDefinitionRegistrar)

 

 

 

好,既然钩子有了,那么是不是就可以开始进行实例化了呢?

 

错!

 

首先,得用上这些钩子。咳咳,这不是在说废话啊!!

毕竟是一个写框架的流程,不是说想用就可以用的,不是说什么一个@Compent注解加上就完事了的。而是需要将配置摆在这里,用户需要的时候才进行配置加载。

如果是XML体系,那没啥好说的,用户就直接在 Spring 的 applicationContext.xml 配置里加载需要的配置Bean就行了。

那么问题来了,要是用注解呢?

 

只要用过Spring体系的,肯定用过一些Enable开头的注解,一般长这样: @EnableXXX  

只要在某个配置类或者说启动类上加入这个注解,就会自动加载某些框架的配置。比如什么 @EnableHystrix、@EnableEurekaServer、@EnableWebSecurity、@EnableCaching、@EnableAsync等等等等奇奇怪怪的注解。

这又是什么操作?

这个问题很好解决,还是以MyBatis举例,只要看一下MyBatis源码就知道这套东西是个什么意思了。

MyBatis的注解配置,有一个必要的类注解叫MapperScan,它主要是来定义扫描哪些包的。然后在该注解中导入了配置类进行加载。

 

看16行高亮行,导入的配置即为我上边说的实现了ImportBeanDefinitionRegistrar 接口的MapperScannerRegistrar。

关于Spring 的注解扫描机制,在使用注解加载配置时,Spring会对所有配置类上的注解进行扫描,也会对注解头上修饰的注解进行递归扫描。

@Import 注解 ,就属于一个Spring提供的特殊注解,扫描到该注解引用的类时,就会自动加载该类资源(也就是装载进Spring IOC容器),  一般来说就是拿来导入配置的。当然,导入普通的Bean也不是不行,不过用@Import来导入Bean就属于画蛇添足了。

导入配置的这个阶段,处于Spring IOC容器的扫描阶段。

 

其实像是这种 @EnableXXX  的注解,你点进他源码看,肯定是用了@Import 的,比如,我也这么定义了一个注解。

看我之前最开始贴出来的实现效果。是不是发现我用的 MainConfig.java 启动配置中,在类上注解了一个@EnableSkypyb 

这个 @EnableSkypyb  的底层我是这么写的

他导入了 SkypybRegistrar.class 这个配置,而这个配置,就是重中之重了。

因为,我是完全看着 MyBatis源码 写出的这个配置类,流程可以说和 MyBatis 的加载 mapper 流程一模一样。只是相对于MyBatis而言,少了很多判定和处理而已。

 

 

现在配置现在也能动态导入了

那么就可以进入扫描、修改BeanDefinition、实例化代理对象的流程了

 

核心配置代码:

 

逻辑清晰,看起来应该不困难。

理所当然的,为了使用@Import 导入实现了ImportBeanDefinitionRegistrar接口。同时还实现了一个ResourceLoaderAware 接口,其实实现这个ResourceLoaderAware 接口没什么鸟用,因为我当时是一直注入失败,所以MyBatis源码里那套东西我都试了一遍,他实现了ResourceLoaderAware 接口我也照葫芦画瓢实现的,实际上后来我全写完了后把这玩意删了也不影响。

 

SkypybRegistrar内部处理流程 :

1、先从 AnnotationMetadata 类中得到了我 @EnableSkypyb 注解内的的信息,主要是想知道,我应该扫描哪些包。

2、然后创建出自己自定义的扫描器,将 ImportBeanDefinitionRegistrar 提供的注册机 BeanDefinitionRegistry 传入进去

3、最后开始扫描,将需要扫描的包名数组enableSkypybValues 传入 doScan() 方法,扫描之前会先注册下扫描过滤器

 

就看这么个流程,肯定有点懵逼,因为到底是如何扫描出BeanDefinition的,又是如何将BeanDefinition改成我们自己定义的代理对象的,这里都没体现出来。

这个就必须要进入我自定义的 ClassPathSkypybScanner 这个类里了,不过我不会一股脑给他把代码全贴出来,不然可能会让人有点懵逼。

我接下来将以方法的粒度逐个方法进行讲解。探秘Spring IOC扫描器,以及我说的: FactoryBean 究极奥义

 

ClassPathSkypybScanner 类声明:

继承了ClassPathBeanDefinitionScanner ,只有一个构造方法,使用的是父类的构造器。

第一个参数是注册机,没什么好说的,第二个参数为是否使用默认过滤器,我传入false表示不使用。

 

关于ClassPathBeanDefinitionScanner :

ClassPathBeanDefinitionScanner作用就是将指定包下的类通过一定规则过滤后 将Class 信息包装成 BeanDefinition 的形式注册到IOC容器中。

可以继承他覆盖其方法,从而设置自定义的扫描器。实际上MyBatis就是这么做的,当然,我也是。

过滤器用来过滤从指定包下面查找到的 Class ,如果能通过过滤器,那么这个class 就会被转换成BeanDefinition 注册到容器。

这里我把它默认的过滤器禁掉,等会用自己写的过滤器

 

在我自己定义的自定义扫描器里边,最核心的就是 doScan() 方法了

这个直接贴代码:

 

流程也很简单:

1、直接调用父类(ClassPathBeanDefinitionScanner) 的 doScan() 方法,将我传入的包名中的类都扫描出来

2、ClassPathBeanDefinitionScanner 提供的 doScan() 在扫描后,会自动注册进 beanDefinitionMap

3、我获取了doScan()返回值后只需getBeanDefinition() 进行修改,反正指向的是一个堆内存地址

4、至于如何修改,之后再说,因为第一步调用父类 doScan() 方法有坑

 

ClassPathBeanDefinitionScanner的 doScan() 方法,你要是没做任何处理,他一个类都不会给你扫出来

需要你重写父类的 isCandidateComponent() 方法,在他扫描阶段,他会一个个走进这方法判断。

看其class是不是一个候选组件。只有是一个候选组件,才会进入到下一步判断是否通过过滤器

 

这是我重写的isCandidateComponent() 。只要是一个接口,并且是单独的,就判断其为一个候选组件。

 

至于我注册的过滤器,其实就是无脑返回true。

但是过滤器这一块是有门道的,很重要的。

我这只是为了演示,才自定义了一个过滤器让其全部通过。

addIncludeFilter() 传入一个TypeFilter,任何一个TypeFilter返回true就代表可以通过

TypeFilter接口目前有AnnotationTypeFilter实现类(类是否有注解修饰)、RegexPatternTypeFilter(类名是否满足正则表达式)等。

可以方便的实现只扫描出符合条件的类,比如 Spring 扫描 @Compone 修饰的类,他就是用的 AnnotationTypeFilter

 

只要这样写,调用父类的 doScan()  方法就可以成功扫描出指定的BeanDefinition了

然后,我在扫描完毕后,使用了自己自定义的方法 processBeanDefinitions() 对该BeanDefinition集合进行处理。

 

 

 

 

对,就到最关键的时机了!!!

总所周知,Spring IOC生命周期扫描出BeanDefinition后会放入beanDefinitionMap之中。

然后在 getBean() 的时候,才进行Bean的实例化进程,将Bean进行实例化。

 

我们现在,已经将mapper接口扫描出来了,并由Spring给我们封装完毕,将 BeanDefinition 注册进了 beanDefinitionMap。

这个时候,只要你启动Spring上下文。

就会报错:  BeanInstantiationException: Failed to instantiate [xxx.xxx.Xxx]: Specified class is an interface

 

因为你扫描出的只是一个普通的接口哇 !

就算封装成了BeanDefinition,也改变不了这个BeanDefinition描述的是个接口的事实啊!

Spring表示,很疑惑啊!他无法实例化啊!

 

而且,我们的终极目的是,偷天换日,将BeanDefinition描述的接口,改为我们自己定义的动态代理对象

 

如何才能将 BeanDefinition 描述的接口,修改为代理对象?

 

要知道,我不可能为每个接口都写个单独的代理,鬼知道用户提供了多少个接口?

我得有个统一的封装来表示用户提供的所有接口,并且可以在 运行时生成动态代理

此对象,还得有个 Class ,因为我得使用 BeanDefinition 的 setBeanClass() 方法,才能将该 BeanDefinition 的描述修改成我自定义的接口。

 

这个时候,就轮到 FactoryBean 出场了

这是 FactoryBean 接口,Spring 对实现了此接口的Bean进行了特殊处理。

主要表现为:

1、FactoryBean本身是个Bean
2、试图获取FactoryBean时,得到的是 FactoryBean 的 getObject() 返回的对象
3、可以通过 “&”+”name” 得到FactoryBean 本体对象

 

了解 FactoryBean 的应该不少。毕竟一些面试官总喜欢问 BeanFactory 和 FactoryBean 有什么区别这种只要背就能背会的问题。

但是,真的有人知道怎么用,在什么场景用这玩意吗?

 

好,现在场景有了,那么怎么用呢?

 

这里就涉及到 FactoryBean 配合 BeanDefinition 的 究极奥义 了。

 

已得知条件:

1、BeanDefinition 表示对一个Bean的描述,可以在实例化之前自由的更改他。

2、FactoryBean 就是一个Bean,但是 Spring 对其做了特殊处理,试图获取时,获取的是 getObject() 返回的对象

从以上条件进行推断:是不是可以在 FactoryBean 里边返回我自定义的代理对象?

 

可是,所有的mapper都要用某一个 FactoryBean 来返回代理对象。这个 FactoryBean 要怎么设计?

我是不是可以这么设计?

 

我可以用一个构造方法来接收指定的 Class 对象,然后动态返回此 Class对象的代理对象。

比如说我可以在此代理里边实现JDBC的逻辑、或者什么远程服务调用的逻辑。

 

可是,就算是这么用了:

那尼玛我 getBean()的时候 Spring 给我试图实例化出来,他也实例化不了啊?

更别提什么调用 getObject() 方法了。

 

而且这个class对象,必须给我从构造方法传进去。因为就算Spring给你实例化完了FactoryBean,他在 getObject() 时,由于没有class对象,也会报错。

这其中可没什么钩子给你 setClass 。

 

那怎么搞?

看上边已知条件第一条:  BeanDefinition 表示对一个Bean的描述,可以在实例化之前自由的更改他

 

 

我在setBeanClass() 之前,可以使用

来得到这个Bean原来的名字。 比如我那个 OneTestDao 接口,通过此方法就可以得到 :com.skypyb.dao.OneTestDao

 

然后通过

从而设置此 BeanDefinition 描述的 Bean 的构造参数

 

最终将刚刚获得的类名传入之后,Spring 解析BeanDefinition时 ,就会知道这个BeanDefinition 描述的 Bean 有个构造方法,要传的值是: com.skypyb.dao.OneTestDao

从而 成功实例化我们自定义的 FactoryBean。

 

最后的结果,就是通过我们自定义 FactoryBean 的实例对象的 getObject() 方法返回了自定义的代理,翻到文章最顶上演示,如你所见。

成功实现了 偷天换日,将BeanDefinition描述的接口,改为我们自己定义的动态代理对象

 

 

而且我的整个流程,和 MyBatis 可以说是 一模一样

不信?  MyBatis 源码 : MapperScannerRegistrar.class  请

 

 

 

整个工程的完整代码地址:  github

阅读全文

 

CAS:Compare and Swap, 翻译成比较并与交换。

 

CAS的定义是这样的,看看就好,我也是百度复制的,懒得自己描述了。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

以上就是CAS定义。

 

看完CAS的定义后,就可以说乐观锁机制了,在软件开发之中,遇到并发问题时大量的场景使用到了乐观锁机制。

与乐观锁机制相对应的,即为悲观锁, 乐观和悲观这俩词体现在哪儿呢,既然知道一点:场景为并发,那么就可以得知乐观和悲观就体现在认为并发操作是否真的有存在。

 

首先,只要是并发修改的场景,为了保证并发安全,加锁机制是肯定需要的, 就看你用个啥解决方案。

若果说我认为并发场景时时有,每秒钟都有大量的线程同时请求。 那么可以说 我比较悲观。

如果说我觉得并发并不是那么多,可能半天才有几个并发,而且操作一般还很快速,那么可以说 我比较乐观。

 

既然得知了以上的悲观乐观概念,那么解决方案也就浮现而出了

悲观锁:我觉得你并发多,那我就直接用 阻塞/拒绝等 方式来控制你的请求,让数据只可能被一个线程同时操作。

乐观锁:我认为你没啥并发 干脆就不加锁了,你要修改啥玩意都直接改,不过为了保证并发安全,要有个判定机制。 一般来说那就是CAS。

 

悲观锁这玩意就不具体说了,就是为了控制临界区只能同时被一个线程访问设计出来的,遇到并发就线程阻塞/请求失败,效率低,并且加解锁这套操作指不定要调用什么消耗资源的命令,比如java中的 synchronized关键字(1.6以前) 就是一个标准的悲观锁,加解锁时还要使用操作系统的命令导致cpu用户态与内核态的切换,加解锁的时间指不定比你执行的临界区代码时间还多。

 

下面就主要说下乐观锁在哪些地方有用到

 

如果是数据库的并发场景:

这里我说的数据库并发场景只讨论”锁” 如何控制并发问题, 不讨论脏读幻读不可重复读这些由于事务隔离级别产生的并发不安全结果。

比如MySQL,MySQL为了并发安全自然里面内置了锁的机制,不过MySQL实现的锁机制是悲观锁(我这说的是写锁啊),增删改的时候默认就给你带上了。MyISAM存储引擎就是使用表锁,你增删改的时候一次锁你一张表,InnoDB存储引擎 使用的就是行锁,只锁你的操作数据行。

行锁表锁好处和坏处也很简单,因为加锁耗时间,所以MyISAM表锁开销小、加锁快,就是锁范围大了,容易发送锁冲突,并发度低。InnoDB行锁开销大、加锁慢。锁的范围小,不易发生锁冲突,并发度高。

 

而乐观锁这玩意 MySQL 是没给你实现的,不过乐观锁机制说简单也简单,无脑CAS就完事了,数据库的操作想使用乐观锁机制可以从表设计和代码逻辑上实现

既然乐观锁机制的核心就是CAS操作,修改的值是可以确定好的,就是我执行SQL语句要改啥嘛,那么就差个所谓的内存位置和预期原值了。

内存位置和预期原值这俩词听起来挺抽象,其实就是一个更改的基准判断,在数据库场景中只要在表里加个字段就完事了,一般来说是version或者timestamp,以version版本举例的话那就是每次修改的时候都先查出来我要改的这个字段的版本号,然后在执行 SQL 语句进行修改,不过 SQL 中需要带一个 WHERE 子句,条件为 version = {查出来的版本},如果是 UPDATE 语句的话则同时将版本号+1 

当当当,一个标准的CAS操作出现了,标准的比较交换流程,比较版本号,若和我之前查询出来的一致则交换,代表此时并没有人修改此字段。

若是我在修改前 此版本号已经被别人修改,那么由于此SQL带了判定的WHERE子句,直接将导致此SQL不成立。

一个乐观锁就这么实现了,而且数据库的这种场景还不用考虑ABA的问题,反正版本在每次修改时都不会一样。 然后在代码逻辑里根据SQL的返回值判定结果,成功就继续,失败要不触发失败操作,要不自旋抢锁。

 

如果是java程序当中的并发场景:

java当中 synchronized 关键字的锁膨胀阶段中 偏向锁、轻量级锁  都可以说是一个乐观锁(这里说的是经过1.6优化之后的)。

偏向锁 意思是偏向于第一个获取此锁的线程,获取锁的操作也是CAS操作,这里对java对象头不详细说,总之只要将java对象头中偏向线程ID以CAS操作改成自己的成功了,就代表获取到锁了。

如果存在并发情况,第二个线程试图获取锁的时候就会等所有线程到达全局安定点后将其升级为轻量级锁,这个轻量级锁就是一个典型的CAS+自旋操作 乐观锁。

轻量级锁情况下线程想要获取锁会直接在自己的线程栈中创建一个锁记录,然后将锁对象的对象头中markwork复制到自己的锁记录里面来,再试图将此对象对象头的markwork指针直接指向自己的锁记录。这套CAS流程要是失败,就自旋再次重复。经典的乐观锁机制。

 

 

CAS和乐观锁这个东西应该这篇文章算是解释清楚了,还举了几个例子比如数据库如何实现乐观锁、java中有哪些地方在使用乐观锁,synchronized在1.6之后的优化没仔细说,锁膨胀的过程只是简单地过了下,因为这个文章主要还是讲讲CAS和使用CAS实现乐观锁机制的,synchronized底层不在文章讨论范围之内,而且这玩意搜索引擎里边资料到处是,也没啥必要细说。

阅读全文

Zookeeper 是一种分布式协调服务,在分布式环境中协调和管理服务是一个复杂的过程。ZooKeeper 通过其简单的架构和 API 解决了这个问题。ZooKeeper 允许开发人员专注于核心应用程序逻辑,而不必担心应用程序的分布式特性。

 

分布式协调服务主要用来解决分布式环境当中多个进程之间的同步控制,让他们有序的去访问某种临界资源,防止造成”脏数据”的后果。

为了防止分布式系统中的多个进程之间相互干扰,需要一种分布式协调技术来对这些进程进行调度。

而这个分布式协调技术的核心就是来实现这个分布式锁。

 

分布式锁应该具备哪些条件?

1、在分布式系统环境下,一段代码在同一时间只能被一个机器的一个线程执行

2、高可用的获取锁与释放锁

3、高性能的获取锁与释放锁

4、具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)

5、具备锁失效机制,防止死锁

6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败

 

关于业界比较流行的分布式锁实现方案:

一般来说Redis会经常被提及到,但是Redis并不是天生为了实现分布式锁而设计出来的, 他是个NoSql内存数据库;不过我们可以利用一些Redis的特性来实现 分布式锁 这个需求

这个链接文章比较清晰的说了redis如何实现分布式锁和redis分布式锁面临的问题,这个说的很清楚了我就不详细说了。

http://zhangtielei.com/posts/blog-redlock-reasoning.html

 

那Zookeeper呢 ,雅虎工程师设计Zookeeper的初衷,就是为了实现分布式锁服务的,利用 Zookeeper 的顺序临时节点,来实现分布式锁和等待队列。当然Zookeeper不止这个功能,比如它可以用来实现服务发现与注册的功能,单以这个需求来使用Zookeeper的也是很多的。

 

在说Zookeeper实现分布式锁之前,首先来讲下他为啥可以实现分布式锁,以及Zookeeper的数据模型:

Zookeeper 的数据模型很像数据结构当中的树,也很像文件系统的目录。

树是由节点所组成,Zookeeper 的数据存储也同样是基于节点,这种节点叫做 Znode,但是,不同于树的节点,Znode 的引用方式是路径引用,类似于文件路径:/car/bmw   、/animal/cat

这样的层级结构,让每一个 Znode 节点拥有唯一的路径。

 

那么Znode 包含哪些元素呢?有这么些:

  • data:Znode 存储的数据信息。
  • ACL:记录 Znode 的访问权限,即哪些人或哪些 IP 可以访问本节点。
  • stat:包含 Znode 的各种元数据,比如事务 ID、版本号、时间戳、大小等等。
  • child:当前节点的子节点引用

Zookeeper 是为读多写少的场景所设计。Znode 并不是用来存储大规模业务数据,而是用于存储少量的状态和配置信息,每个节点数据最大只有1M。

关于Zookeeper的基本增删改查的操作就不详细说明了,主要有这么几种:create、delete、exists、getData、setData、getChildren。

而跟增删改查息息相关的,Zookeeper 客户端在请求读操作的时候,可以选择是否设置 Watch,也就是Zookeeper的事件通知机制

 

可以把 Watch 理解成是注册在特定 Znode 上的触发器。调用了 create、delete、setData 方法的时候 (做出修改操作时),将会触发 Znode 上注册的事件,请求 Watch 的客户端会接收到异步通知。

大概的事件通知机制如下:

  • 客户端调用 getData 方法,watch 参数是 true。服务端接到请求,返回节点数据,并且在对应的哈希表里插入被 Watch 的 Znode 路径,以及 Watcher 列表。
  • 当被 Watch 的 Znode 已删除,服务端会查找哈希表,找到该 Znode 对应的所有 Watcher,异步通知客户端,并且删除哈希表中对应的 Key-Value。

 

好,说了这么多,差不过该进入主题如何实现分布式锁了,上边说了利用 Zookeeper 的顺序临时节点可以实现分布式锁,那么这顺序临时节点又是个什么玩意?

默认创建的Znode  是”持久节点” 创建节点的客户端与 Zookeeper 断开连接后,该节点依旧存在。

除此之外,还有其他几种,其实看名字也能猜出是啥用了:

持久顺序节点:  所谓持久顺序节点,就是在创建节点时,Zookeeper 根据创建的时间顺序给该节点名称进行编号。其他和持久节点一样。

临时节点:  和持久节点相反,当创建节点的客户端与 Zookeeper 断开连接后,临时节点会被删除。

临时顺序节点: 顾名思义,临时节点和顺序节点的合成体;在创建节点时,Zookeeper 根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与 Zookeeper 断开连接后,节点会被删除。

 

Zookeeper 分布式锁的原理机制:

说完节点,也就差不多可以实现分布式锁了,其实知道这几个节点是个什么玩意后,在结合一下Watch机制,基本可以在脑内猜想个实现大概出来。

加锁释放锁流程:

1、首先,在 Zookeeper 当中创建一个持久节点,给它取个名字名字叫 ParentLock。当第一个客户端想要获得锁时,在 ParentLock 这个节点下面创建一个临时顺序节点 Lock1。之后,Client1 查找 ParentLock 下面所有的临时顺序节点并排序,判断自己所创建的节点 Lock1 是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。

2、这时候,如果再有一个客户端 Client2 前来获取锁,则在 ParentLock 下再创建一个临时顺序节点 Lock2。

3、Client2 查找 ParentLock 下面所有的临时顺序节点并排序,判断自己所创建的节点 Lock2 是不是顺序最靠前的一个,结果发现节点 Lock2 并不是最小的。于是,Client2 向排序仅比它靠前的节点 Lock1 注册 Watcher,用于监听 Lock1 节点是否存在。这意味着 Client2 抢锁失败,进入了等待状态,这就形成了一个等待队列。

4、  当任务完成时,Client1 会显示调用删除节点 Lock1 的指令。此时由于 Client2 一直监听着 Lock1 的存在状态,当 Lock1 节点被删除,Client2 会立刻收到通知。这时候 Client2 会再次查询 ParentLock 下面的所有节点,确认自己创建的节点 Lock2 是不是目前最小的节点。如果是最小,则 Client2 顺理成章获得了锁。

5、如果说获得锁的 Client1 在任务执行过程中如果崩溃了,则会断开与 Zookeeper 服务端的链接。根据临时节点的特性,相关联的节点 Lock1 会随之自动删除;所以Client2 又收到通知获得了锁。

 

说到这,是不是觉得Zookeeper的机制来实现分布式锁较为不错,人家都给你封装好了,不用向Redis那样实现分布式锁还得考虑超时、原子性、误删除之类的。

还有等待队列可以提升抢锁效率,比起Redis实现的效果确实是舒服了许多。

 

阅读全文

说是这么说哈,实际上这个泳道活动图只是一个大概流程,上边写的组件也基本上都是接口 而不是具体实现 (具体实现这鬼画得出来啊)

对于Spring这么强的东西来说,内部实现比我画的复杂多了,我这也就是随便画画 在线丢人而已

 

不过,就这么个简单的图还是凝结了我好久的知识总结的心血来着,从很久以前自学Spring开始,到后来因为职业在工作中长期的使用;

再到后期翻人家博客、看看部分源码,长久日子下来可算是对Spring熟悉点了而不是单纯的做个调参侠,最近因为离职了,就想着画个图巩固一下,在增强记忆的同时还可以练练好久没动用的UML技术。

结果为了画这个图,为了避免画错,又找了挺多资料学习 (其实到现在我也不确定我画的这个流程有没有问题,不过就算有应该也不会偏差多少)

越了解的深就越觉得Spring精妙,不愧是你.jpg

 

避免看不清图片,在这奉上链接,点开就能看:

http://assets.processon.com/chart_image/5d463499e4b07c4cf3012bd4.png

 

使用的画图工具为 processon ,是个网址

什么都好,就是不充钱只能画⑨个图,到现在为止我已经删掉好几个以前画的图了。

 

阅读全文

本来遇到了很奇怪的坑,crontab定时任务执行的shell脚本和自己手动执行的shell脚本不一致。

在java进程已启动的情况下, crontab定时任务执行的脚本却总是找不到对应的pid。

试了好几种解决方法,结果还是不行。

全部命令都用绝对路径执行啊,环境导入啊之类的,都试了下,没有作用,后来自己用crontab里写的定时任务试了试,出现了很多很奇怪的东西

后来还是在 grep “进程名”  之前加了个grep java  才解决的

 

crontab的定时任务如下: 

 

 

shell脚本如下:

 

都写了注释,应该可以说是很清晰了。

核心逻辑就是查询指定的进程id,若是有则无事发生,若是没有 则执行另一个启动脚本将服务启动。期间伴随日志打印到特定文件中。

 

定时任务则是每十分钟启动一次,这个cron表达式在SpringBoot的定时任务里也有实现,就不说了。

这样子定时任务运行起来其实是没有问题的,但是还不能满足需求。

 

试想这么一个场景:

刚刚关闭掉这个服务器,还在上传文件/部署/设置参数 的过程中,此时时间刚好到那个定时任务启动的时间了,他这定时任务又给你把服务给启动起来了,那岂不是很尴尬。

而且定时任务又不是想停就停,linux的定时任务要停只能执行 crontab -e 然后手动修改文件才可以。

 

这个时候我灵光一闪,既然定时任务停不了,那我让你执行命令无法成功不就完事了

于是我在启动服务的脚本和关闭服务的脚本中加入了如下代码:

 

在我手动关闭服务器时,关闭脚本会将定时任务试图执行的moniter.sh 文件名字给改了,这时候定时任务无法找到对应脚本则无法执行相应命令。

然后在我手动启动时,再给他改回去,让定时任务可以找到对应脚本。

 

最终实现效果果然如我所料一般完美,要是服务宕机,自然不是我手动关闭的,那么由于文件名没有被修改掉所以定时任务可以定时执行检测脚本,之后服务就会被重启。

那在维护时我手动进行的服务停止则会带动定时任务的停止,不用担心他自个偷偷摸摸的启动了。

整体实现效果非常满意。

 

阅读全文
EA PLAYER &

历史记录 [ 注意:部分数据仅限于当前浏览器 ]清空

      00:00/00:00