com.alibaba.dubbo.rpc.RpcException: Failed to invoke remote method: sayHello, provider:
dubbo://127.0.0.1:20831/com.domain.package.TestService?application=dubbo-test-
rest&default.check=false&default.cluster=failfast&default.retries=0&default.timeout=1200000&default.version=1.0
.0&dubbo=2.6.1&interface=com.domain.package.TestService&methods=sayHello&pid=29268®ister.ip=192.
168.6.47&side=consumer×tamp=1524453157718, cause: com.alibaba.com.caucho.hessian.io.HessianFieldException:
org.hibernate.validator.internal.engine.ConstraintViolationImpl.constraintDescriptor:
'org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl' could not be instantiated
com.alibaba.com.caucho.hessian.io.HessianFieldException:
org.hibernate.validator.internal.engine.ConstraintViolationImpl.constraintDescriptor:
'org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl' could not be instantiated
at com.alibaba.com.caucho.hessian.io.JavaDeserializer.logDeserializeError(JavaDeserializer.java:167)
at com.alibaba.com.caucho.hessian.io.JavaDeserializer$ObjectFieldDeserializer.deserialize
(JavaDeserializer.java:408)
at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:273)
at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:200)
at com.alibaba.com.caucho.hessian.io.SerializerFactory.readObject(SerializerFactory.java:525)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2791)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2731)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2260)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2705)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2260)
at com.alibaba.com.caucho.hessian.io.CollectionDeserializer.readLengthList
(CollectionDeserializer.java:119)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2186)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2057)
at com.alibaba.com.caucho.hessian.io.JavaDeserializer$ObjectFieldDeserializer.deserialize
(JavaDeserializer.java:404)
at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:273)
at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:200)
at com.alibaba.com.caucho.hessian.io.SerializerFactory.readObject(SerializerFactory.java:525)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2791)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2731)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2260)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2705)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2260)
at com.alibaba.dubbo.common.serialize.hessian2.Hessian2ObjectInput.readObject
(Hessian2ObjectInput.java:74)
at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:90)
at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:110)
at com.alibaba.dubbo.rpc.protocol.dubbo.DubboCodec.decodeBody(DubboCodec.java:88)
at com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:121)
at com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:82)
at com.alibaba.dubbo.rpc.protocol.dubbo.DubboCountCodec.decode(DubboCountCodec.java:44)
at com.alibaba.dubbo.remoting.transport.netty.NettyCodecAdapter$InternalDecoder.messageReceived
(NettyCodecAdapter.java:133)
at org.jboss.netty.channel.SimpleChannelUpstreamHandler.handleUpstream
(SimpleChannelUpstreamHandler.java:70)
at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:564)
at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:559)
at org.jboss.netty.channel.Channels.fireMessageReceived(Channels.java:268)
at org.jboss.netty.channel.Channels.fireMessageReceived(Channels.java:255)
at org.jboss.netty.channel.socket.nio.NioWorker.read(NioWorker.java:88)
at org.jboss.netty.channel.socket.nio.AbstractNioWorker.process(AbstractNioWorker.java:109)
at org.jboss.netty.channel.socket.nio.AbstractNioSelector.run(AbstractNioSelector.java:312)
at org.jboss.netty.channel.socket.nio.AbstractNioWorker.run(AbstractNioWorker.java:90)
at org.jboss.netty.channel.socket.nio.NioWorker.run(NioWorker.java:178)
at org.jboss.netty.util.ThreadRenamingRunnable.run(ThreadRenamingRunnable.java:108)
at org.jboss.netty.util.internal.DeadLockProofWorker$1.run(DeadLockProofWorker.java:42)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:744)
Caused by: com.alibaba.com.caucho.hessian.io.HessianProtocolException:
'org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl' could not be instantiated
at com.alibaba.com.caucho.hessian.io.JavaDeserializer.instantiate(JavaDeserializer.java:313)
at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:198)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2789)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2128)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2057)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2101)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2057)
at com.alibaba.com.caucho.hessian.io.JavaDeserializer$ObjectFieldDeserializer.deserialize
(JavaDeserializer.java:404)
... 43 more
Caused by: java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:526)
at com.alibaba.com.caucho.hessian.io.JavaDeserializer.instantiate(JavaDeserializer.java:309)
... 50 more
Caused by: java.lang.NullPointerException
at org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl.<init>
(ConstraintDescriptorImpl.java:158)
at org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl.<init>
(ConstraintDescriptorImpl.java:211)
... 55 more
上面的问题从异常面来看已经很直观了,'org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl' could not be instantiated
,这个类无法实例化,那是什么原因导致它无法实例化呢?
Dubbo
的序列化协议,默认是hessian
,如果没有进行其他协议配置的话,默认使用的就是hessian
,hessian
在反序列化时有个特点需要注意一下,它会在反序列化时取参数最少的构造器来创建对象,有的时候会有很多重载的构造器,因此会有一些参数直接给null
,因此可能就会造成一些莫名其妙的问题,就像我们这个问题一样。
那这个问题如何解决呢?接着往下看
由于这个是Hessian
反序列化问题,因此与Dubbo
的版本关系不大,为了验证这个我还专门使用apache dubbo
2.6.1
版本测试了一下,问题依旧存在。
方法一:使用无参构造方法来创建对象
既然是hessian
反序列化问题,而且它在反序列化时根据构造函数参数个数优先级来取参数最少的,那我们就可以增加一个无参的构造方法来解决这个问题。
但是有的时候我们使用的是第三方的包,不太好增加无参的构造方法,那怎么办的,我们能不能使用其他方法,继续往下看。
方法二:替换jsr303实现框架
既然hibernate-validator
的org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl
这个类在使用hessian反序列化存在问题,那我们使用其他jsr303的框架来试试。
jsr303的实现框架有哪些?
org.hibernate : hibernate-validator : 5.2.4.Final
org.apache.bval : bval-jsr303 : 0.5
jersery
bval是apache的一个bean validator的实现,jersery是一个restful的框架为了满足自身的数据验证功能因此增加了jsr303的实现。
由于我们使用的springmvc构建restful因此这里就不考虑jersery,我们就从bval下手来试一试。
在进行了一番配置后(都有哪些配置?)
增加bval包,现在版本是:0.5
<dependency>
<groupId>org.apache.bval</groupId>
<artifactId>bval-jsr303</artifactId>
<version>0.5</version>
</dependency>
将bval集成到spring框架中,作为spring的验证框架
这里有两种方式,一种xml配置,一种java config
xml方式:
<mvc:annotation-driven validator="validator"/>
<!-- 数据验证 Validator bean -->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<property name="providerClass" value="org.apache.bval.jsr.ApacheValidationProvider" />
</bean>
java config方式:
重写mvcValidator方法
@Override
public Validator mvcValidator() {
Validator validator = super.mvcValidator();
if (validator instanceof LocalValidatorFactoryBean) {
LocalValidatorFactoryBean lvfb = (LocalValidatorFactoryBean) validator;
try {
String className = "org.apache.bval.jsr303.ApacheValidationProvider";
Class<?> clazz = ClassUtils.forName(className, WebMvcConfigurationSupport.class.getClassLoader());
lvfb.setProviderClass(clazz);
catch (ClassNotFoundException e) {
//没有找到bval验证框架,走spring默认整合的验证框架:hibernate-validator
//这里异常没有必要跑出去,直接吃掉
return validator;
启动后验证功能
但是不好的事情发生了,无法启动报错,错误如下:
java.lang.AbstractMethodError: org.apache.bval.jsr303.ConfigurationImpl.getDefaultParameterNameProvider()
Ljavax/validation/ParameterNameProvider;
经过对spring
的资料查找,发现spring
从4.0版本往后不在支持集成其他jsr303的框架了,只能使用hibernate-validator
,我擦这个有点暴力了。即使自己实现一个jsr303框架也无法再spring
中使用,除非不使用spring
validator
功能,直接使用自己的验证框架来进行验证,这样就无法使用@Validated param
方式。
那这种方法只能放弃了。
方法三:修改hibernate-validator的原声类,修改Dubbo ValidationFilter,这也是我最终采用的方法
其实替换jsr303框架不能成功,替换序列化协议应该也可以避免这个问题,只不过替换协议这个一般在维护的项目中不太会选择这样的方式来动刀子,现在开发很多都是分布式服务,序列化反序列化已经无处不在了,因此我建议编写代码时都增加一个无参数的构造方法,养成这样的一个好习惯可以避免很多序列化反序列化框架的坑。而且还有那些有匿名内部类的这种在序列化反序列化也需要注意,不是所有的序列化反序列化框架都支持有匿名类,gson是支持的这个为测试过,我前面也写过一篇博文里面就主要说这个问题,可以查看:《Java中内部类使用注意事项,内部类对序列化与反序列化的影响》
有兴趣的可以看一下我们常用的序列化反序列化类库的一些使用中的注意事项,可以参考这篇文章:《java常用JSON库注意事项总结》
回归话题,上面的问题我们如何解决,最终我们采用重写javax.validation.ConstraintViolation<T>
的实现类,替换掉hibernate-validation
的org.hibernate.validator.internal.engine.ConstraintViolationImpl
,因为ConstraintViolationImpl
中有部分对象无法通过hessian反序列化。
我们最终的目标是不管是validation开启在provider端还是consumer端,调用方接收到的参数校验异常数据是一致的。
修改的代码已经提交到apache dubbo
,具体查看Pull request:https://github.com/apache/incubator-dubbo/pull/1708
大概的代码如下:
增加类:DubboConstraintViolation实现javax.validation.ConstraintViolation
接口
import java.io.Serializable;
import javax.validation.ConstraintViolation;
import javax.validation.Path;
import javax.validation.ValidationException;
import javax.validation.metadata.ConstraintDescriptor;
import com.alibaba.dubbo.common.logger.Logger;
import com.alibaba.dubbo.common.logger.LoggerFactory;
public class DubboConstraintViolation<T> implements ConstraintViolation<T>, Serializable {
static final Logger logger = LoggerFactory.getLogger(DubboConstraintViolation.class.getName());
private static final long serialVersionUID = -8901791810611051795L;
private String interpolatedMessage;
private Object value;
private Path propertyPath;
private String messageTemplate;
private Object[] executableParameters;
private Object executableReturnValue;
private int hashCode;
public DubboConstraintViolation() {
public DubboConstraintViolation(ConstraintViolation<T> violation) {
this(violation.getMessageTemplate(), violation.getMessage(), violation.getInvalidValue(), violation.getPropertyPath(),
violation.getExecutableParameters(), violation.getExecutableReturnValue());
public DubboConstraintViolation(String messageTemplate,
String interpolatedMessage,
Object value,
Path propertyPath,
Object[] executableParameters,
Object executableReturnValue) {
this.messageTemplate = messageTemplate;
this.interpolatedMessage = interpolatedMessage;
this.value = value;
this.propertyPath = propertyPath;
this.executableParameters = executableParameters;
this.executableReturnValue = executableReturnValue;
// pre-calculate hash code, the class is immutable and hashCode is needed often
this.hashCode = createHashCode();
@Override
public final String getMessage() {
return interpolatedMessage;
@Override
public final String getMessageTemplate() {
return messageTemplate;
@Override
public final T getRootBean() {
return null;
@Override
public final Class<T> getRootBeanClass() {
return null;
@Override
public final Object getLeafBean() {
return null;
@Override
public final Object getInvalidValue() {
return value;
@Override
public final Path getPropertyPath() {
return propertyPath;
@Override
public final ConstraintDescriptor<?> getConstraintDescriptor() {
return null;
@Override
public <C> C unwrap(Class<C> type) {
if ( type.isAssignableFrom( ConstraintViolation.class ) ) {
return type.cast( this );
throw new ValidationException("Type " + type.toString() + " not supported for unwrapping.");
@Override
public Object[] getExecutableParameters() {
return executableParameters;
@Override
public Object getExecutableReturnValue() {
return executableReturnValue;
@Override
// IMPORTANT - some behaviour of Validator depends on the correct implementation of this equals method! (HF)
// Do not take expressionVariables into account here. If everything else matches, the two CV should be considered
// equals (and because of the scary comment above). After all, expressionVariables is just a hint about how we got
// to the actual CV. (NF)
public boolean equals(Object o) {
if ( this == o ) {
return true;
if ( o == null || getClass() != o.getClass() ) {
return false;
DubboConstraintViolation<?> that = (DubboConstraintViolation<?>) o;
if ( interpolatedMessage != null ? !interpolatedMessage.equals( that.interpolatedMessage ) : that.interpolatedMessage != null ) {
return false;
if ( propertyPath != null ? !propertyPath.equals( that.propertyPath ) : that.propertyPath != null ) {
return false;
if ( messageTemplate != null ? !messageTemplate.equals( that.messageTemplate ) : that.messageTemplate != null ) {
return false;
if ( value != null ? !value.equals( that.value ) : that.value != null ) {
return false;
return true;
@Override
public int hashCode() {
return hashCode;
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append( "DubboConstraintViolation" );
sb.append( "{interpolatedMessage='" ).append( interpolatedMessage ).append( '\'' );
sb.append( ", propertyPath=" ).append( propertyPath );
sb.append( ", messageTemplate='" ).append( messageTemplate ).append( '\'' );
sb.append( ", value='" ).append( value ).append( '\'' );
sb.append( '}' );
return sb.toString();
// Same as for equals, do not take expressionVariables into account here.
private int createHashCode() {
int result = interpolatedMessage != null ? interpolatedMessage.hashCode() : 0;
result = 31 * result + ( propertyPath != null ? propertyPath.hashCode() : 0 );
result = 31 * result + ( value != null ? value.hashCode() : 0 );
result = 31 * result + ( messageTemplate != null ? messageTemplate.hashCode() : 0 );
return result;
这里的变更为捕捉javax.validation.ConstraintViolationException
异常,对异常中的Set<ConstraintViolation<String>>
数据进行转换,去掉无法反序列化的对象,具体代码如下:
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
if (validation != null && !invocation.getMethodName().startsWith("$")
&& ConfigUtils.isNotEmpty(invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.VALIDATION_KEY))) {
try {
Validator validator = validation.getValidator(invoker.getUrl());
if (validator != null) {
validator.validate(invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments());
} catch (ConstraintViolationException e) {
Set<ConstraintViolation<?>> set = null;
//验证set中如果是hibernate-validation实现的类就处理,其他的实现类放过
Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
for (ConstraintViolation<?> v : constraintViolations) {
if (!v.getClass().getName().equals("org.hibernate.validator.internal.engine.ConstraintViolationImpl")) {
return new RpcResult(e);
} else {
if (set == null) set = new HashSet<ConstraintViolation<?>>();
set.add(new DubboConstraintViolation<>(v));
return new RpcResult(new ConstraintViolationException(e.getMessage(), set));
} catch (RpcException e) {
throw e;
} catch (Throwable t) {
return new RpcResult(t);
return invoker.invoke(invocation);
使用这个方法后,在provider
端设置validation=true
,consumer
端可以正常拿到所有校验数据的异常信息。
我觉得这个方法并不是完美的方法,虽然这个问题是hibernate-validator
框架的问题,hibernate-validator
出生的年代分布式还不是特别的完善因此没有充分的考虑序列化反序列化问题也很正常,但是作为Dubbo
框架在集成jsr303
的时候也需要考虑这些问题。具体可以查看Apache Dubbo
的Pull Request
:https://github.com/apache/incubator-dubbo/pull/1708
当我们使用持续集成Jenkins
的时候经常会结合一系列的插件使用,这里就说一下Jenkins
集成Sonar
做代码质量管理以及Junit(testng)
、JaCoCo
做单元测试和覆盖率的时候遇到的问题。
首先我们的工程使用maven
构建,单元测试使用testng
编写,在使用jenkins
之前我们应该在本地使用maven调通所有的单元测试以及test coverage
的问题。
我们使用maven-surefire-plugin
来生成单元测试报告,使用jacoco-maven-plugin
来生成test coverage
报告。下面我给出以下我使用的标准配置
maven工程调通单元测试以及测试覆盖率报告生成
pom.xml的标准配置
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.4</version>
<scope>test</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.5</version>
<configuration>
<skipTests>false</skipTests>
<argLine>${argLine} -Dfile.encoding=UTF-8</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.1</version>
<configuration>
<skip>false</skip>
</configuration>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<configuration>
<outputDirectory>${basedir}/target/coverage-reports</outputDirectory>
</configuration>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
根据上面配置执行下来的报告生成的目录结构如下:
coverage-reports
是单元测试覆盖率报告生成目录
surefire-reports
是单元测试报告生成目录
test-classes
是单元测试代码编译生成的字节码目录
jacoco.exec
是用于生成单元测试可执行文件
下面我说一下我们会遇到的常规问题
上步操作会遇到的常规问题
问题一:Tests are skipped.
[INFO] --- maven-surefire-plugin:2.5:test (default-test) @ tools ---
[INFO] Tests are skipped.
单元测试被跳过,这个可以通过maven-surefire-plugin
插件的configuration
来配置不跳过,如下配置:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.5</version>
<configuration>
<skipTests>false</skipTests>
</configuration>
</plugin>
配置skipTests
属性而不是skip
属性这里需要注意一下,有很多人配置的skip
属性
问题二:单元测试输出乱码
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running TestSuite
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
=====��һ��===============
=====��һ��===============
=====���¼���===============
单元测试输出信息乱码,这个可以通过maven-surefire-plugin
插件的configuration
来配置字符编码,如下配置:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.5</version>
<configuration>
<skipTests>false</skipTests>
<argLine>-Dfile.encoding=UTF-8</argLine>
</configuration>
</plugin>
到这里我们就可以去taget/surefire-reports
目录下查看单元测试报告。
问题三:Skipping JaCoCo execution due to missing execution data file.
[INFO] --- jacoco-maven-plugin:0.8.1:report (report) @ tools ---
[INFO] Skipping JaCoCo execution due to missing execution data file.
jacoco
执行被跳过,原因是没有找到jacoco
可执行文件jacoco.exec
。
这个时候我们去target
目录下是看不到jacoco.exec
文件的,有的版本名字叫jacoco-junit.exec
。
理论上执行的时候会自动生成exec文件,但是为什么没有生成?我们看一下执行日志
[INFO] --- jacoco-maven-plugin:0.8.1:prepare-agent (default) @ tools ---
[INFO] argLine set to -javaagent:D:\\javatools\\mvnrepository\\org\\jacoco\\org.jacoco.agent\\0.8.1\\org.jacoco.agent-0.8.1-runtime.jar=destfile=D:\\javatools\\workspace\\framework\\tools\\target\\jacoco.exec
jacoco.exec
的生成是根据-javaagent
的方式来生成的,我们有可以看到jacoco-maven-plugin
指定了argLine
参数,但是为什么没有生效?
原因是我们上面指定过单元测试编码,使用的就是argLine
参数,因此这个问题应该是上面的编码参数指定后没有带入插件添加的-javaagent
参数,那如何解决?查看下面配置:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.5</version>
<configuration>
<skipTests>false</skipTests>
<argLine>${argLine} -Dfile.encoding=UTF-8</argLine>
</configuration>
</plugin>
在argLine
中增加变量${argLine}
后面再增加自动以的参数
如果通过配置手动的指定jacoco.exec
文件的生成路径也需要注意也可能会出现这个问题,生成exec的路径指定在哪里,report执行的时候就需要通过dataFile
来指定exec的路径,让程序知道正确的exec路径,比如说:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.1</version>
<configuration>
<skip>false</skip>
<destFile>${basedir}/target/coverage-reports/jacoco.exec</destFile>
</configuration>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<configuration>
<dataFile>${basedir}/target/coverage-reports/jacoco.exec</dataFile>
<outputDirectory>${basedir}/target/coverage-reports</outputDirectory>
</configuration>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
上面通过configuration
的destFile
来自定义jacoco.exec
的生成路径,下面在report
的时候需要通过dataFile
来指定对应的jacoco.exec
的路径。
Jenkins使用JaCoCo plugin插件
首先去Jenkins
上安装JaCoCo plugin
插件,插件的安装就跳过了,插件安装好后,在job中如何配置?
Path to exec files: **/jacoco.exec 可执行文件路径
Path to class directories: 这个配置的是源代码编译后的字节码目录,也就是classes
目录不是test-classes
目录,如果有多个可以指定多个
Path to source directories: 这个配置的是源代码的目录,也就是src/main/java
目录,如果有多个可以指定多个。
配置好之后执行job会看到如下的日志:
INFO: ------------------------------------------------------------------------
Injecting SonarQube environment variables using the configuration: SonarQube
[JaCoCo plugin] Collecting JaCoCo coverage data...
[JaCoCo plugin] **/jacoco.exec;**/classes;src/main/java; locations are configured
Injecting SonarQube environment variables using the configuration: SonarQube
Injecting SonarQube environment variables using the configuration: SonarQube
[JaCoCo plugin] Number of found exec files for pattern **/jacoco.exec: 1
[JaCoCo plugin] Saving matched execfiles: /var/lib/jenkins/workspace/cc-framework-tools/target/coverage-reports/jacoco.exec
[JaCoCo plugin] Saving matched class directories for class-pattern: **/classes:
[JaCoCo plugin] - /var/lib/jenkins/workspace/cc-framework-tools/target/classes 5 files
[JaCoCo plugin] Saving matched source directories for source-pattern: src/main/java:
[JaCoCo plugin] - /var/lib/jenkins/workspace/cc-framework-tools/src/main/java 5 files
[JaCoCo plugin] Loading inclusions files..
[JaCoCo plugin] inclusions: []
[JaCoCo plugin] exclusions: []
[JaCoCo plugin] Thresholds: JacocoHealthReportThresholds [minClass=0, maxClass=0, minMethod=0, maxMethod=0, minLine=0, maxLine=0, minBranch=0, maxBranch=0, minInstruction=0, maxInstruction=0, minComplexity=0, maxComplexity=0]
[JaCoCo plugin] Publishing the results..
[JaCoCo plugin] Loading packages..
[JaCoCo plugin] Done.
[JaCoCo plugin] Overall coverage: class: 50, method: 54, line: 48, branch: 40, instruction: 55
Finished: SUCCESS
出现上面日志就证明配置成功并且可以看到报告,如果出现下面的日志就证明配置的目录没有扫到classes,需要修改Path to class directories
目录的配置
Overall coverage: class: 0, method: 0, line: 0, branch: 0, instruction: 0
最终结果如下图:
Jenkins使用Sonarqube plugin插件
首先去Jenkins上
安装SonarQube plugin
插件,插件的安装就跳过了,插件安装好后,在jenkins
的系统配置中配置sonar
服务器信息,如下
配置好后在job的配置中增加SonarQube
的支持,如下
在构建环境下添加Prepare SonarQube Scanner environment
sonar.language=java
# path to source directories (required)
# 源代码目录,如果多个使用","分割 例如:mode1/src/main,mode2/src/main
sonar.sources=src/main
# 单元测试目录,如果多个使用","分割 例如:mode1/src/test,mode2/src/test
sonar.tests=src/test
# Exclude the test source
# 忽略的目录
#sonar.exclusions=*/src/test/**/*
# 单元测试报告目录
sonar.junit.reportsPath=target/surefire-reports
# 代码覆盖率插件
sonar.java.coveragePlugin=jacoco
# jacoco.exec文件路径
sonar.jacoco.reportPath=target/coverage-reports/jacoco.exec
# 这个没搞懂,官方示例是配置成jacoco.exec文件路径
sonar.jacoco.itReportPath=target/coverage-reports/jacoco.exec
具体的参数可以查看官方文档:《Analysis Parameters》
配置好之后执行job后去Sonar
上只看到了单元测试的信息,没有看到单元测试覆盖率的信息,关于这个问题我们分析job执行的日志,如下:
问题一:No JaCoCo analysis of project coverage can be done since there is no class files.
16:01:17.455 INFO - Sensor JaCoCoOverallSensor
16:01:17.470 INFO - Analysing /var/lib/jenkins/workspace/cc-framework-tools/target/coverage-reports/jacoco.exec
16:01:17.481 INFO - No JaCoCo analysis of project coverage can be done since there is no class files.
16:01:17.481 INFO - Sensor JaCoCoOverallSensor (done) | time=26ms
16:01:17.482 INFO - Sensor JaCoCoSensor
16:01:17.482 INFO - No JaCoCo analysis of project coverage can be done since there is no class files.
16:01:17.482 INFO - Sensor JaCoCoSensor (done) | time=0ms
16:01:17.482 INFO - Sensor Code Colorizer Sensor
说的是没找到class文件所以jacoco
不能进行分析,问题很明显是没有找到class类,难道它不是去maven
标准的target/classes
下找文件么?
但是找到了这篇文章:《Jenkins, JaCoCo, and SonarQube Integration With Maven》,看到里面在pom.xml中配置了一些参数给我了启发,发现有个参数sonar.binaries
指定的是classes目录,可以插件的有些参数不兼容maven,在官方的配置中可以看到这样的字样: Not compatible with Mave
和Compatible with Maven
,能看到有写参数兼容maven默认路径有些不兼容。
随后再官方文档中也找到了与jenkins继承的properties配置说明:《Triggering Analysis on Hudson Job》
# path to project binaries (optional), for example directory of Java bytecode
# java字节码目录
sonar.binaries=binDir
最终给出Execute SonarQube Scanner
中的Analysis properties
完成配置参数如下:
# required metadata
# 项目key
sonar.projectKey=com.domian.package:projectName
# 项目名称
sonar.projectName=tools
# 项目版本,可以写死,也可以引用变量
sonar.projectVersion=${VER}
# 源文件编码
sonar.sourceEncoding=UTF-8
# 源文件语言
sonar.language=java
# path to source directories (required)
# 源代码目录,如果多个使用","分割 例如:mode1/src/main,mode2/src/main
sonar.sources=src/main/java
# 单元测试目录,如果多个使用","分割 例如:mode1/src/test,mode2/src/test
sonar.tests=src/test/java
# java字节码目录
sonar.binaries=target/classes
# 单元测试报告目录
sonar.junit.reportsPath=target/surefire-reports
# 代码覆盖率插件
sonar.java.coveragePlugin=jacoco
# jacoco插件版本
jacoco.version=0.8.1
# jacoco.exec文件路径
sonar.jacoco.reportPath=target/coverage-reports/jacoco.exec
全部配置修改完后执行job后去Sonar
上查看具体的信息如下:
故障自恢复的高可用
海量数据高并发实时写入与实时查询(HTAP 混合负载)
TiDB 的设计目标是 100% 的 OLTP 场景和 80% 的 OLAP 场景,更复杂的 OLAP 分析可以通过 TiSpark 项目来完成。
TiDB 对业务没有任何侵入性,能优雅的替换传统的数据库中间件、数据库分库分表等 Sharding 方案。同时它也让开发运维人员不用关注数据库 Scale 的细节问题,专注于业务开发,极大的提升研发的生产力。
我们来看一下TiDB的架构图
从架构图中可以看出TiDB的三大组件都支持水平扩展而且内部通信使用的是gRPC,关于TiDB和gRPC的那些事可以查看InfoQ的文章:《TiDB与gRPC的那点事》
TiDB使用的TiKV作为存储,官方建议至少TiKV使用ssd硬盘,如果条件好pd模块最好也使用ssd硬盘。
下来我们具体看一下三大组件分别都是干什么的
TiDB Server
TiDB Server 负责接收 SQL 请求,处理 SQL 相关的逻辑,并通过 PD 找到存储计算所需数据的 TiKV 地址,与 TiKV 交互获取数据,最终返回结果。 TiDB Server 是无状态的,其本身并不存储数据,只负责计算,可以无限水平扩展,可以通过负载均衡组件(如LVS、HAProxy 或 F5)对外提供统一的接入地址。
PD Server
Placement Driver (简称 PD) 是整个集群的管理模块,其主要工作有三个: 一是存储集群的元信息(某个 Key 存储在哪个 TiKV 节点);二是对 TiKV 集群进行调度和负载均衡(如数据的迁移、Raft group leader 的迁移等);三是分配全局唯一且递增的事务 ID。
PD 是一个集群,需要部署奇数个节点,一般线上推荐至少部署 3 个节点。
TiKV Server
TiKV Server 负责存储数据,从外部看 TiKV 是一个分布式的提供事务的 Key-Value 存储引擎。存储数据的基本单位是 Region,每个 Region 负责存储一个 Key Range (从 StartKey 到 EndKey 的左闭右开区间)的数据,每个 TiKV 节点会负责多个 Region 。TiKV 使用 Raft 协议做复制,保持数据的一致性和容灾。副本以 Region 为单位进行管理,不同节点上的多个 Region 构成一个 Raft Group,互为副本。数据在多个 TiKV 之间的负载均衡由 PD 调度,这里也是以 Region 为单位进行调度。
可以无限水平扩展而且三大组件都是高可用,TiDB/TiKV/PD 这三个组件都能容忍部分实例失效,不影响整个集群的可用性。关于三大组件出现问题后如何恢复可以查看:《tidb-整体架构中的高可用章节》
官方的部署建议
TiDB使用的TiKV作为存储,官方建议至少TiKV使用ssd硬盘,如果条件好pd模块最好也使用ssd硬盘。
建议 4 台及以上,TiKV 至少 3 实例,且与 TiDB、PD 模块不位于同一主机。
TiDB的部署方式还是蛮丰富的,可以使用Ansible在线以及离线的部署集群,TiDB-Ansible 是 PingCAP 基于 Ansible playbook 功能编写的集群部署工具。使用 TiDB-Ansible 可以快速部署一个完整的 TiDB 集群(包括 PD、TiDB、TiKV 和集群监控模块)。
TiDB同时也支持Docker部署方案,由于我们公司内网使用docker容器的方式管理所有服务,所以我这里使用docker方式部署。
我们使用Rancher来做企业级的容器管理平台,没有使用k8s、mesos来进行编排管理,使用的是Rancher自带的Cattle,Cattle不光有编排管理还包含了应用、服务、卷、负载均衡、健康检查、服务升级、dns服务、等功能,有兴趣的可以查看:《Rancher官方文档-Cattle》
在进行部署之前需要先去Docker官方镜像库中拉TiDB集群所需要的三大组件的镜像: Docker 官方镜像仓库
docker pull pingcap/tidb:latest
docker pull pingcap/tikv:latest
docker pull pingcap/pd:latest
这三个组件的镜像都不大,TiKV只有54MB,PD只有21MB,TiDB只有17MB
这个我需要说一下他们这块做的还是很不错的,将镜像压缩的都比较小,去除了很多无用的东西。
我们需要创建7个容器来部署一个TiDB集群:
http://192.168.18.108:2379/v2/members
http://192.168.18.108:2479/v2/members
http://192.168.18.108:2579/v2/members
返回信息以json格式,三台pd返回集群信息都是一样的
{"members":[{"id":"969b7171b723b804","name":"pd3","peerURLs":["http://192.168.18.108:2580"],"clientURLs":["http://192.168.18.108:2579"]},{"id":"d141f07798663b47","name":"pd2","peerURLs":["http://192.168.18.108:2480"],"clientURLs":["http://192.168.18.108:2479"]},{"id":"e5e987f33a60e672","name":"pd1","peerURLs":["http://192.168.18.108:2380"],"clientURLs":["http://192.168.18.108:2379"]}]}
具体的docker容器创建命令可以参考官方文档:《Docker部署方案》
TiDB支持mysql协议可以使用任意mysql客户端连接,默认安装好的集群使用mysql登录,端口:4000,用户名:root,密码为空,修改密码跟mysql修改密码方式完全一样。
SET PASSWORD FOR 'root'@'%' = 'xxx';
下面说几个我们必须要关心的东西。
事务隔离级别可以查看:《TiDB 事务隔离级别》
SQL语法没有什么变化,具体可以查看:《SQL语句语法》
SQL执行计划什么的都有跟使用mysql几乎一样,还增加了json的支持,可以设置字段列存储类型为json格式。
具体与MySQL有什么差异可以查看:《与MySQL兼容性对比》
历史数据回溯问题可以查看:《TiDB 历史数据回溯》
Binlog可以使用:《TiDB-Binlog 部署方案》
还有《备份与恢复》 和 《数据迁移》。
好了今天的大致介绍和测试环境集群搭建都到这里,后面会总结使用中遇到的问题。
这个问题我相信大家对它并不陌生,但是有很多人对它产生的原因以及处理吃的不是特别透,很多情况都是交给DBA去定位和处理问题,接下来我们就针对这个问题来展开讨论。
Mysql造成锁的情况有很多,下面我们就列举一些情况:
执行DML操作没有commit,再执行删除操作就会锁表。
在同一事务内先后对同一条数据进行插入和更新操作。
表索引设计不当,导致数据库出现死锁。
长事物,阻塞DDL,继而阻塞所有同表的后续操作。
但是要区分的是Lock wait timeout exceeded
与Dead Lock
是不一样。
Lock wait timeout exceeded
:后提交的事务等待前面处理的事务释放锁,但是在等待的时候超过了mysql的锁等待时间,就会引发这个异常。
Dead Lock
:两个事务互相等待对方释放相同资源的锁,从而造成的死循环,就会引发这个异常。
还有一个要注意的是innodb_lock_wait_timeout
与lock_wait_timeout
也是不一样的。
innodb_lock_wait_timeout
:innodb的dml操作的行级锁的等待时间
lock_wait_timeout
:数据结构ddl操作的锁的等待时间
如何查看innodb_lock_wait_timeout的具体值?
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout'
如何修改innode lock wait timeout的值?
参数修改的范围有Session和Global,并且支持动态修改,可以有两种方法修改:
通过下面语句修改
set innodb_lock_wait_timeout=100;
set global innodb_lock_wait_timeout=100;
ps. 注意global的修改对当前线程是不生效的,只有建立新的连接才生效。
修改参数文件/etc/my.cnf
innodb_lock_wait_timeout = 50
ps. innodb_lock_wait_timeout
指的是事务等待获取资源等待的最长时间,超过这个时间还未分配到资源则会返回应用失败; 当锁等待超过设置时间的时候,就会报如下的错误;ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
。其参数的时间单位是秒,最小可设置为1s(一般不会设置得这么小),最大可设置1073741824秒,默认安装时这个值是50s(默认参数设置)。
下面介绍在遇到这类问题该如何处理
数据更新或新增后数据经常自动回滚。
表操作总报 Lock wait timeout exceeded
并长时间无反应
应急方法:show full processlist;
kill
掉出现问题的进程。 ps.有的时候通过processlist是看不出哪里有锁等待的,当两个事务都在commit阶段是无法体现在processlist上
根治方法:select * from innodb_trx;
查看有是哪些事务占据了表资源。 ps.通过这个办法就需要对innodb有一些了解才好处理
说起来很简单找到它杀掉它就搞定了,但是实际上并没有想象的这么简单,当问题出现要分析问题的原因,通过原因定位业务代码可能某些地方实现的有问题,从而来避免今后遇到同样的问题。
innodb_*表的解释
Mysql
的InnoDB
存储引擎是支持事务的,事务开启后没有被主动Commit
。导致该资源被长期占用,其他事务在抢占该资源时,因上一个事务的锁而导致抢占失败!因此出现 Lock wait timeout exceeded
下面几张表是innodb的事务和锁的信息表,理解这些表就能很好的定位问题。
innodb_trx
## 当前运行的所有事务
innodb_locks
## 当前出现的锁
innodb_lock_waits
## 锁等待的对应关系
下面对 innodb_trx
表的每个字段进行解释:
trx_id:事务ID。
trx_state:事务状态,有以下几种状态:RUNNING、LOCK WAIT、ROLLING BACK 和 COMMITTING。
trx_started:事务开始时间。
trx_requested_lock_id:事务当前正在等待锁的标识,可以和 INNODB_LOCKS 表 JOIN 以得到更多详细信息。
trx_wait_started:事务开始等待的时间。
trx_weight:事务的权重。
trx_mysql_thread_id:事务线程 ID,可以和 PROCESSLIST 表 JOIN。
trx_query:事务正在执行的 SQL 语句。
trx_operation_state:事务当前操作状态。
trx_tables_in_use:当前事务执行的 SQL 中使用的表的个数。
trx_tables_locked:当前执行 SQL 的行锁数量。
trx_lock_structs:事务保留的锁数量。
trx_lock_memory_bytes:事务锁住的内存大小,单位为 BYTES。
trx_rows_locked:事务锁住的记录数。包含标记为 DELETED,并且已经保存到磁盘但对事务不可见的行。
trx_rows_modified:事务更改的行数。
trx_concurrency_tickets:事务并发票数。
trx_isolation_level:当前事务的隔离级别。
trx_unique_checks:是否打开唯一性检查的标识。
trx_foreign_key_checks:是否打开外键检查的标识。
trx_last_foreign_key_error:最后一次的外键错误信息。
trx_adaptive_hash_latched:自适应散列索引是否被当前事务锁住的标识。
trx_adaptive_hash_timeout:是否立刻放弃为自适应散列索引搜索 LATCH 的标识。
下面对 innodb_locks
表的每个字段进行解释:
lock_id:锁 ID。
lock_trx_id:拥有锁的事务 ID。可以和 INNODB_TRX 表 JOIN 得到事务的详细信息。
lock_mode:锁的模式。有如下锁类型:行级锁包括:S、X、IS、IX,分别代表:共享锁、排它锁、意向共享锁、意向排它锁。表级锁包括:S_GAP、X_GAP、IS_GAP、IX_GAP 和 AUTO_INC,分别代表共享间隙锁、排它间隙锁、意向共享间隙锁、意向排它间隙锁和自动递增锁。
lock_type:锁的类型。RECORD 代表行级锁,TABLE 代表表级锁。
lock_table:被锁定的或者包含锁定记录的表的名称。
lock_index:当 LOCK_TYPE=’RECORD’ 时,表示索引的名称;否则为 NULL。
lock_space:当 LOCK_TYPE=’RECORD’ 时,表示锁定行的表空间 ID;否则为 NULL。
lock_page:当 LOCK_TYPE=’RECORD’ 时,表示锁定行的页号;否则为 NULL。
lock_rec:当 LOCK_TYPE=’RECORD’ 时,表示一堆页面中锁定行的数量,亦即被锁定的记录号;否则为 NULL。
lock_data:当 LOCK_TYPE=’RECORD’ 时,表示锁定行的主键;否则为NULL。
下面对 innodb_lock_waits 表的每个字段进行解释:
requesting_trx_id:请求事务的 ID。
requested_lock_id:事务所等待的锁定的 ID。可以和 INNODB_LOCKS 表 JOIN。
blocking_trx_id:阻塞事务的 ID。
blocking_lock_id:某一事务的锁的 ID,该事务阻塞了另一事务的运行。可以和 INNODB_LOCKS 表 JOIN。
锁等待的处理步骤
直接查看 innodb_lock_waits 表
SELECT * FROM innodb_lock_waits;
innodb_locks 表和 innodb_lock_waits 表结合:
SELECT * FROM innodb_locks WHERE lock_trx_id IN (SELECT blocking_trx_id FROM innodb_lock_waits);
innodb_locks 表 JOIN innodb_lock_waits 表:
SELECT innodb_locks.* FROM innodb_locks JOIN innodb_lock_waits ON (innodb_locks.lock_trx_id = innodb_lock_waits.blocking_trx_id);
查询 innodb_trx 表:
SELECT trx_id, trx_requested_lock_id, trx_mysql_thread_id, trx_query FROM innodb_trx WHERE trx_state = 'LOCK WAIT';
trx_mysql_thread_id 即kill掉事务线程 ID
SHOW ENGINE INNODB STATUS ;
SHOW PROCESSLIST ;
从上述方法中得到了相关信息,我们可以得到发生锁等待的线程 ID,然后将其 KILL 掉。
KILL 掉发生锁等待的线程。
kill ID;
最近在参考CQRS DDD架构来进行公司的库存中心重构设计,在CQRS
架构中需要一个in-memory
的方式快速修改库存在通过消息驱动异步更新到DB
,也就是说内存的数据是最新的,DB
的数据是异步持久化的,在某一个时刻内存和DB
的数据是存在不一致的,但是满足最终一致性。
这样我们就需要内存当作前置DB
在使用,因此不单纯的只满足修改数据,还需要满足Query
的要求,内存结构的数据Query
是比较麻烦的,它不像DB
那样已经实现好了索引检索,需要我们自己来设计Key
的机构和搜索索引的构建。
当然行业里也有这样的做法,对数据修改的时候双写到内存(Redis
)和ElasticSearch
再异步到DB
,这样Query
全部走向ElasticSearch
,但是我觉得这样做的复杂度会增加很多,所以就在看如何基于Redis
来设计一个搜索引擎。
看到了RedisLabs
团队开发的基于Redis
的搜索引擎:RediSearch
RediSearch
Github: RediSearch
官方给出的描述
Redisearch implements a search engine on top of redis, but unlike other redis search libraries, it does not use internal data structures like sorted sets.
Inverted indexes are stored as a special compressed data type that allows for fast indexing and search speed, and low memory footprint.
This also enables more advanced features, like exact phrase matching and numeric filtering for text queries, that are not possible or efficient with traditional redis search approaches.
高性能的全文搜索引擎(Faster, in-memory, highly available full text search),可作为Redis Module运行在Redis上。但是它与其他Redis搜索库不同的是,它不使用Redis内部数据结构,例如:集合、排序集(ps.后面会写一篇基于Redis的数据结构来设计搜索引擎),Redis原声的搜索还是有很大的局限性,简单的分词搜索是可以满足,但是应用到复杂的场景就不太适合。
Full-Text indexing of multiple fields in documents.
Incremental indexing without performance loss.
Document ranking (provided manually by the user at index time).
Field weights.
Complex boolean queries with AND, OR, NOT operators between sub-queries.
Prefix matching in full-text queries.
Auto-complete suggestions (with fuzzy prefix suggestions)
Exact Phrase Search.
Stemming based query expansion in many languages (using Snowball).
Support for logographic (Chinese, etc.) tokenization and querying (using Friso)
Limiting searches to specific document fields (up to 128 fields supported).
Numeric filters and ranges.
Geographical search utilizing redis’ own GEO commands.
Supports any utf-8 encoded text.
Retrieve full document content or just ids.
Automatically index existing HASH keys as documents.
Document Deletion (Update can be done by deletion and then re-insertion).
Sortable properties (i.e. sorting users by age or name).
下面是中文版本
多个字段的文档的全文索引。
没有性能损失增量索引。
文档排名(由用户提供手动指数时间)。
字段权重。
在子查询之间使用AND,OR,NOT运算符进行复杂的布尔查询。
前缀匹配全文查询。
自动完成建议以模糊前缀(建议)
准确短语搜索。
阻止基于查询扩展多种语言(使用Snowball)。
支持语标的(中国等)标记和查询(使用Friso)
将搜索限制在特定的文档字段(128字段支持)。
数字过滤器和范围。
利用redis自己的GEO命令进行地理搜索。
支持任何utf-8编码的文本。
获取完整的文档内容或者只是id。
自动索引现有HASH keys文件。
文档删除(更新可以通过删除然后re-insertion)。
可排序属性(即按年龄或名称对用户进行排序)。
当然还支持分布式集群,只不过集群还是试验阶段还不建议正式应用到企业级应用上。
Spelling correction(拼写更正)
Aggregations(集合)
支持的Client类库
Official (Redis Labs) and community Clients:
从数据上看,使用RediSearch的吞吐量高、延迟低,但是相比于ElasticSearch和Solr支持的特性上还有些欠缺比如:中文的模糊搜索支持的不是很好,但是其性能很高在某些场景是可以作为搜索引擎的替代方案来试用。
案例资料:
利用RediSearch构建高效实时搜索案例
一步步实现 Redis 搜索引擎
我们做了一个支持全文搜索和关系查询的 Redis
上述就是关于RediSearch的资料整理,后面会尝试使用它来构建搜索引擎,会记录使用过程经历。
当我们使用cas
做单点登录的时候往往会使用集群方式部署,不管是cas server
或者是接入的app server
都会采用集群的方式部署。
在对cas server
做集群实现无状态化,需要注意一下几点,也是我上一篇cas
遇到的TGC
验证问题中总结出来的:
cas
的ticket
需要做到集中存储,可以使用redis
、jpa
、或者其他方式,这个官方文章上有详细介绍:ticket-registry
cas
的session
信息需要做到集中存储,如果使用的是tomcat
可以使用TomcatRedisSessionMananger插件来通过redis
做session
集中存储。
还有一个就是上面遇到的问题,客户端cookie
信息:TGC
,TGC
采用cookie
方式存在客户端,因此需要开启会话保持,使得相同客户端每次都会被路由到同一个cas server
上去做TGC
验证。
最后一个就是需要接入sso
的client
应用端的session
信息也需要做集中存储,因此cas server
会和client
进行通信去验证ticket
,验证完后会生成信息并存储到sesson
中,因此也需要使用TomcatRedisSessionMananger插件来通过redis
做session
集中存储。
cas server端和接入的app服务端需要保证网络通畅。
cas使用总结博文目录
最近cas
遇到的问题我都总结到了blog中,这里整理一下目录如下:
《CAS使用经验总结,纯干货》
《CAS Server强制踢人功能实现方式》
《Trouble Shooting —— CAS Server集群环境下TGC验证问题排查,需要开启会话保持》
接下来我们就说一下这次遇到的问题。
通过上面的方式可以将cas server
做到集群无状态化,但是避免不了其他的问题,下面就是最近与到的问题,现象是这样的,一部分人可以正常登陆,一部分人登陆时报错,错误如下:
2018-03-23 10:33:22.768 [http-nio-7051-exec-1] ERROR org.jasig.cas.client.util.CommonUtils - Server redirected too many times (20)
java.net.ProtocolException: Server redirected too many times (20)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1637) ~[na:1.7.0_79]
at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:254) ~[na:1.7.0_79]
at org.jasig.cas.client.util.CommonUtils.getResponseFromServer(CommonUtils.java:393) ~[cas-client-core-3.3.3.jar:3.3.3]
at org.jasig.cas.client.validation.AbstractCasProtocolUrlBasedTicketValidator.retrieveResponseFromServer(AbstractCasProtocolUrlBasedTicketValidator.java:45) [cas-client-core-3.3.3.jar:3.3.3]
at org.jasig.cas.client.validation.AbstractUrlBasedTicketValidator.validate(AbstractUrlBasedTicketValidator.java:200) [cas-client-core-3.3.3.jar:3.3.3]
at org.springframework.security.cas.authentication.CasAuthenticationProvider.authenticateNow(CasAuthenticationProvider.java:140) [spring-security-cas-3.1.7.RELEASE.jar:3.1.7.RELEASE]
at org.springframework.security.cas.authentication.CasAuthenticationProvider.authenticate(CasAuthenticationProvider.java:126) [spring-security-cas-3.1.7.RELEASE.jar:3.1.7.RELEASE]
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:156) [spring-security-core-3.1.7.RELEASE.jar:3.1.7.RELEASE]
at org.springframework.security.cas.web.CasAuthenticationFilter.attemptAuthentication(CasAuthenticationFilter.java:242) [spring-security-cas-3.1.7.RELEASE.jar:3.1.7.RELEASE]
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:195) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
at org.jasig.cas.client.session.SingleSignOutFilter.doFilter(SingleSignOutFilter.java:100) [cas-client-core-3.3.3.jar:3.3.3]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
at com.bstek.bdf2.core.security.filter.PreAuthenticatedProcessingFilter.doFilter(PreAuthenticatedProcessingFilter.java:41) [scm-bdf2-core-1.1.0-SNAPSHOT.jar:na]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:105) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
at org.springframework.security.web.session.ConcurrentSessionFilter.doFilter(ConcurrentSessionFilter.java:125) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
at com.bstek.bdf2.core.security.filter.ContextFilter.doFilter(ContextFilter.java:36) [scm-bdf2-core-1.1.0-SNAPSHOT.jar:na]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:87) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:160) [spring-security-web-3.1.7.RELEASE.jar:3.1.7.RELEASE]
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:344) [spring-web-4.0.0.RELEASE.jar:4.0.0.RELEASE]
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:261) [spring-web-4.0.0.RELEASE.jar:4.0.0.RELEASE]
从异常的描述来看是服务器端多次redirected
超过了20次导致的问题,什么原因会造成这个问题?
cas单点登录过程剖析
cas
的单点登录的过程大致是这样的。
第一步:访问app地址,例如:https://app.domain.com
,app端的cas-client-core
会判断是否已经登录,如果没有登录会重定向到如下地址:https://login.domain.com/login?service=https%3A%2F%2Fapp.domain.com%2Fcas_security_check_
第二步:当重定向到cas
登录页面后,我们输入用户名密码,cas server
端会进行如下操作
先进行AUTHENTICATION
过程,这个过程是验证我们的用户名密码是否正确,会输出如下日志:
2018-03-23 14:58:01,429 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGIN
=============================================================
WHO: admin
WHAT: Supplied credentials: [admin]
ACTION: AUTHENTICATION_SUCCESS
APPLICATION: CAS
WHEN: Fri Mar 23 14:58:01 HKT 2018
CLIENT IP ADDRESS: xx.xx.xx.xx
SERVER IP ADDRESS: xx.xx.xx.xx
=============================================================
当AUTHENTICATION
通过以后会生成TGT(TICKET_GRANTING_TICKET),这个是换取服务票据的预授票据,并且将TGT保存起来,我这里使用的是jpa方式保存到db,会输出如下日志:
=============================================================
WHO: admin
WHAT: TGT-***********************************************1VX72iaQBZ-077adac8d80f
ACTION: TICKET_GRANTING_TICKET_CREATED
APPLICATION: CAS
WHEN: Fri Mar 23 14:58:01 HKT 2018
CLIENT IP ADDRESS: 10.42.37.135
SERVER IP ADDRESS: 10.42.185.88
=============================================================
Hibernate: insert into TICKETGRANTINGTICKET (NUMBER_OF_TIMES_USED, CREATION_TIME, EXPIRATION_POLICY, LAST_TIME_USED, PREVIOUS_LAST_TIME_USED, AUTHENTICATION, EXPIRED, PROXIED_BY, SERVICES_GRANTED_ACCESS_TO, ticketGrantingTicket_ID, TYPE, ID) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'TGT', ?)
当TGT
生成完后会生成ST(SERVICE_TICKET),这个是服务票据,是授权这个服务的票据,并且会将ST保存起来和更新TGT信息,我这里使用的是jpa方式保存到db,会输出如下日志:
2018-03-23 14:58:01,504 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGIN
=============================================================
WHO: admin
WHAT: ST-153-RfpK0ACJHtPsSdnbYhVf-077adac8d80f for https://app.domain.com/cas_security_check_
ACTION: SERVICE_TICKET_CREATED
APPLICATION: CAS
WHEN: Fri Mar 23 14:58:01 HKT 2018
CLIENT IP ADDRESS: xx.xx.xx.xx
SERVER IP ADDRESS: xx.xx.xx.xx
=============================================================
Hibernate: insert into SERVICETICKET (NUMBER_OF_TIMES_USED, CREATION_TIME, EXPIRATION_POLICY, LAST_TIME_USED, PREVIOUS_LAST_TIME_USED, FROM_NEW_LOGIN, TICKET_ALREADY_GRANTED, SERVICE, ticketGrantingTicket_ID, TYPE, ID) values (?, ?, ?, ?, ?, ?, ?, ?, ?, 'ST', ?)
Hibernate: update TICKETGRANTINGTICKET set NUMBER_OF_TIMES_USED=?, CREATION_TIME=?, EXPIRATION_POLICY=?, LAST_TIME_USED=?, PREVIOUS_LAST_TIME_USED=?, AUTHENTICATION=?, EXPIRED=?, PROXIED_BY=?, SERVICES_GRANTED_ACCESS_TO=?, ticketGrantingTicket_ID=? where ID=?
这个时候服务端生成的票据就完成了,会将ST信息生成TGC(TICKET_GRANTING_COOKIE)返回给app端。
第三步:app端接收到cas server端的返回,TGC会直接写入到浏览器cookie中,app端会再发起一次ST验证,这个过程是在app的后端发起请求的,url如下:
https://login.domain.com/serviceValidate?ticket=ST-153-RfpK0ACJHtPsSdnbYhVf-077adac8d80f&service=https%3A%2F%2Fapp.domain.com%2Fcas_security_check_
第四步:cas server端收到service validate请求后会验证ST和TGC是否合法,并且验证TGC的时候cas server需要开启会话保持,让请求发送到生成TGC的机器上去,因为TGC中保存生成的服务端地址,具体问题我前面分析过查看:《Trouble Shooting —— CAS Server集群环境下TGC验证问题排查,需要开启会话保持》,cas server验证成功后会输出如下的日志:
2018-03-23 14:58:01,578 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGIN
=============================================================
WHO: admin
WHAT: ST-153-RfpK0ACJHtPsSdnbYhVf-077adac8d80f
ACTION: SERVICE_TICKET_VALIDATED
APPLICATION: CAS
WHEN: Fri Mar 23 14:58:01 HKT 2018
CLIENT IP ADDRESS: xx.xx.xx.xx
SERVER IP ADDRESS: xx.xx.xx.xx
=============================================================
ps.出现下面日志表示验证失败
2018-03-23 14:58:01,580 INFO [org.apereo.inspektr.audit.support.Slf4jLoggingAuditTrailManager] - <Audit trail record BEGIN
=============================================================
WHO: audit:unknown
WHAT: ST-154-YA6KibaqHpOMGXbluz7V-077adac8d80f
ACTION: SERVICE_TICKET_VALIDATE_FAILED
APPLICATION: CAS
WHEN: Fri Mar 23 14:58:01 HKT 2018
CLIENT IP ADDRESS: xx.xx.xx.xx
SERVER IP ADDRESS: xx.xx.xx.xx
=============================================================
第五步:app后端接收到cas server端service验证成功的返回后,会生成session并且与TG进行关系绑定,绑定信息会保存起来,这里需要注意的是如果是集群环境需要保存到redis或者其他统一存储的地方。,app后端接收验证成功后的输出日志如下:
2018-03-23 14:58:01.531 [http-apr-8080-exec-1] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Constructing validation url: https://login.domain.com/serviceValidate?ticket=ST-153-RfpK0ACJHtPsSdnbYhVf-077adac8d80f&service=https%3A%2F%2Fapp.domain.com%2Fcas_security_check_
2018-03-23 14:58:01.531 [http-apr-8080-exec-1] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Retrieving response from server.
2018-03-23 14:58:01.602 [http-apr-8080-exec-1] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Server response: <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationSuccess>
<cas:user>admin</cas:user>
</cas:authenticationSuccess>
</cas:serviceResponse>
输出以上信息就是验证成功。到这里cas server端的所有验证都完成了。
ps.出现下面日志表示app后端接收到的是验证失败返回信息
2018-03-23 14:58:02.295 [http-bio-7051-exec-6] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Constructing validation url: https://login.domain.com/serviceValidate?ticket=ST-154-YA6KibaqHpOMGXbluz7V-077adac8d80f&service=https%3A%2F%2Fapp.domain.com%2Fcas_security_check_
2018-03-23 14:58:02.295 [http-bio-7051-exec-6] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Retrieving response from server.
2018-03-23 14:58:02.830 [http-bio-7051-exec-6] DEBUG o.j.c.c.validation.Cas20ServiceTicketValidator - Server response: <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationFailure code="INVALID_TICKET">Ticket 'ST-154-YA6KibaqHpOMGXbluz7V-077adac8d80f' not recognized</cas:authenticationFailure>
</cas:serviceResponse>
第六步:app端登录成功进入主页面。
根据这个流程我们再来分析上面的异常是那个环节出现了问题。
首先上面的异常是app的后端出现的异常,app后端发起请求是在cas server生成完ticket之后才发起的,并且发起的是service validate验证请求,这个请求导致重定向超过20次。
而且还有一个重要的信息就是,一部分人可以正常登录,一部分人不能登录,我们部署的结构是2台cas server,2台app服务。
通过日志排查,2台app服务,其中一台没有出现过一场,另外一台爆出异常。这个时候问题已经有些明朗了,当负载均衡路由到出错的这台服务时,后台服务发起service validate验证时出现了问题,那接下来就让我们对比两台服务器上的配置。
我们采用的是阿里云的SLB映射到后台的nginx,app的后台服务要和cas server通信那首先网络需要是通的,理论上网络应该是没问题的,但是为了验证问题,我们就从网络这块开始排查。
因为我们使用的是阿里云而且app服务没有开通外网,app后天和cas服务通信走的是内网的SLB,接下来我们就ping一下登录地址看一下返回的slb地址是否相同。
两台机器上ping login.domain.com ,果然返回的ip不一致,其中报错的那台机器返回的是本机ip,奥这就是问题的根源,cat /etc/hosts
果然域名映射的ip不一致,应该是运维配置失误导致的问题。
通过修改host配置之后再次验证错误解决。
最终定位的到的问题感觉很白痴的问题,是因为运维配置失误导致,但是值得回味的是,通过这个问题我们对cas的单点登录机制理解的更加深刻,这就是一种收获,往往通过繁琐的分析后定位到的问题都很easy,所以当我们分析问题、定位问题的时候一定要先理解其中的原理,再结合现象去一步一步分析,这是仔细和关注度是否全面的一种考验。好了问题就说到这里,希望能够帮助到需要的人。
世界和平、Keep Real!
现在是Git
流行的年代,在Git
的套件里想要全文检索代码也有很多方案,Git
也支持命令直接检索代码,但是当使用svn
的用户代码检索应该如何处理呢?
在回答前面问题之前我们还要搞清楚另外一个问题,我们为什么要检索代码?
有的时候我们想从所有的代码库去寻找使用相同方法的代码,常规做法就是checkout
下来所有的项目,然后通过IDE
工具去关联检索使用到某个方法的代码,但是这样做比较耗费时间而且当项目过多IDE
不一定能扛得住。还有的时候我们想从规范角度去check
开发人员写的代码是否有违规的或者有问题的,就可以通过检索去寻找,当然规范的check
有更好的工具,可以使用scm
工具sonar
去check
代码它整合了很多check
模版。
鉴于上面种种的原因对代码做检索还是很有必要的,接下来我们就说一下使用svn
时如何全文检索代码。
我们可以先说一个思路,把代码灌入elasticsearch
、lucene
、solr
,然后通过ui去搜索这是一条可行的路子。
这两天发现了一个工具svnquery
很好用,它使用ASP.net
开发,采用Lucene
生成索引,提供GUI
和WEB
工具通过索引文件来检索代码。
svnquery官网
它提供三个程序,一个svnindex
用于通过svn
库生成索引目录
SvnIndex.exe %aciton% %index_path% %svn_path% -u 用户名 -p 密码
ps. action
包括create
、update
,更新和修改
执行后会生成一个索引目录,可以通过svnfind
工具可以选择索引目录来进行代码搜索,svnfind
是一个GUI
工具。
还可以通过SvnWebQuery
来进行代码搜索,SvnWebQuery
是一个.NET
的web
程序需要放入IIS
服务器来使用
引用官网的两张图
唯一的缺点就是需要一个库一个库的生成索引,没有批量生成svn
路径下所有有权限的库,如果有这个功能我个人觉得就完美了。
好了工具介绍到这里,如果有用svn
的想对代码进行检索的可以使用这个工具。
Zookeeper
在互联网行业和分布式环境下是最常用的集群协调工具,那我们今天就对Zookeeper
的常用命令和使用注意事项进一步说明,在这之前我们先看一下Zookeeper
是什么,它能做什么?
Zookeeper是什么?
ZooKeeper
是一个开源的分布式应用程序协调服务,是Google
的Chubby
一个开源的实现,是Hadoop
和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
它的这些特性可以让我们在很多场景下使用它,可以用它做注册中心、分布式锁、选举、队列等。
Zookeeper的原理
ZooKeeper
是以Fast Paxos
算法为基础的,Paxos
算法存在活锁的问题,即当有多个proposer
交错提交时,有可能互相排斥导致没有一个proposer
能提交成功,而Fast Paxos
作了一些优化,通过选举产生一个leader
(领导者),只有leader
才能提交proposer
,具体算法可见Fast Paxos
。因此,要想弄懂ZooKeeper
首先得对Fast Paxos
有所了解
ZooKeeper
的基本运转流程:
选举Leader
。
同步数据。
选举Leader
过程中算法有很多,但要达到的选举标准是一致的。
Leader
要具有最高的执行ID
,类似root
权限。
集群中大多数的机器得到响应并接受选出的Leader
。
Zookeeper数据结构
与普通的文件系统极其类似,如下:
其中每个节点称为一个znode. 每个znode由3部分组成:
stat. 此为状态信息, 描述该znode的版本, 权限等信息.
data. 与该znode关联的数据.
children. 该znode下的子节点.
Zookeeper节点类型
persistent
: persistent
节点不和特定的session
绑定, 不会随着创建该节点的session
的结束而消失, 而是一直存在, 除非该节点被显式删除.
ephemeral
: ephemeral
节点是临时性的, 如果创建该节点的session
结束了, 该节点就会被自动删除. ephemeral
节点不能拥有子节点. 虽然ephemeral
节点与创建它的session
绑定, 但只要该该节点没有被删除, 其他session
就可以读写该节点中关联的数据. 使用-e
参数指定创建ephemeral
节点.
sequence
: 严格的说, sequence
并非节点类型中的一种. sequence
节点既可以是ephemeral
的, 也可以是persistent
的. 创建sequence
节点时, ZooKeeper server
会在指定的节点名称后加上一个数字序列, 该数字序列是递增的. 因此可以多次创建相同的sequence
节点, 而得到不同的节点. 使用-s参数指定创建sequence
节点.
Zookeeper常用命令
[app@iZbp1dijzcfg8m0bcqfv9yZ zookeeper]$ ./bin/zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /usr/local/servers/zookeeper/zookeeper/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
查看当前zk节点状态
[zk@iZbp1dijzcfg8m0bcqfv9yZ bin]$ ./zkServer.sh status
JMX enabled by default
Using config: /usr/local/servers/zookeeper/zookeeper/bin/../conf/zoo.cfg
Mode: standalone
ps. standalone
代表单机模式,
[zk@iZ23np2fk60Z bin]$ ./zkServer.sh status
JMX enabled by default
Using config: /usr/local/zookeeper/bin/../conf/zoo.cfg
Mode: leader
ps. 集群模式下会显示的状态,leader
节点,集群中其他机器会从leader节点同步数据
[zk@iZ237ydkhyiZ bin]$ ./zkServer.sh status
JMX enabled by default
Using config: /usr/local/zookeeper/bin/../conf/zoo.cfg
Mode: follower
ps. 集群模式下会显示的状态,follower
节点在启动过程中会从leader节点同步所有数据
[app@iZbp1dijzcfg8m0bcqfv9yZ zookeeper]$ ./bin/zkCli.sh -server ip:port
ps. 不写ip端口默认连接本机服务.
查看节点信息
[zk: localhost:2181(CONNECTED) 0] ls /
[seq, dubbo, disconf, otter, pinpoint-cluster, zookeeper]
查看指定node的子node
[zk: localhost:2181(CONNECTED) 3] ls /zookeeper
[quota]
创建一个普通节点
[zk: localhost:2181(CONNECTED) 6] create /hello world
Created /hello
获取hello节点的数据与状态
[zk: localhost:2181(CONNECTED) 8] get /hello
world
cZxid = 0x262ea76
ctime = Wed Mar 21 14:39:12 CST 2018
mZxid = 0x262ea76
mtime = Wed Mar 21 14:39:12 CST 2018
pZxid = 0x262ea76
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0
删除hello节点
[zk: localhost:2181(CONNECTED) 9] delete /hello
[zk: localhost:2181(CONNECTED) 10] get /hello
Node does not exist: /hello
ps. 使用delete命令可以删除指定znode. 当该znode拥有子znode时, 必须先删除其所有子znode, 否则操作将失败. rmr命令可用于代替delete命令, rmr是一个递归删除命令, 如果发生指定节点拥有子节点时, rmr命令会首先删除子节点.
znode节点的状态信息
使用get命令获取指定节点的数据时, 同时也将返回该节点的状态信息, 称为Stat. 其包含如下字段:
czxid. 节点创建时的zxid.
mzxid. 节点最新一次更新发生时的zxid.
ctime. 节点创建时的时间戳.
mtime. 节点最新一次更新发生时的时间戳.
dataVersion. 节点数据的更新次数.
cversion. 其子节点的更新次数.
aclVersion. 节点ACL(授权信息)的更新次数.
ephemeralOwner. 如果该节点为ephemeral节点, ephemeralOwner值表示与该节点绑定的session id. 如果该节点不是ephemeral节点, ephemeralOwner值为0. 至于什么是ephemeral节点, 请看后面的讲述.
dataLength. 节点数据的字节数.
numChildren. 子节点个数.
znode节点的状态信息中包含czxid和mzxid, 那么什么是zxid呢?
ZooKeeper状态的每一次改变, 都对应着一个递增的Transaction id, 该id称为zxid. 由于zxid的递增性质, 如果zxid1小于zxid2, 那么zxid1肯定先于zxid2发生. 创建任意节点, 或者更新任意节点的数据, 或者删除任意节点, 都会导致Zookeeper状态发生改变, 从而导致zxid的值增加.
session
在client和server通信之前, 首先需要建立连接, 该连接称为session. 连接建立后, 如果发生连接超时, 授权失败, 或者显式关闭连接, 连接便处于CLOSED状态, 此时session结束.
创建不同类型的节点
节点的类型前面已经讲过。
创建一个临时节点
[zk: localhost:2181(CONNECTED) 12] create -e /hello world
Created /hello
[zk: localhost:2181(CONNECTED) 13] get /hello
world
cZxid = 0x262ea78
ctime = Wed Mar 21 14:45:23 CST 2018
mZxid = 0x262ea78
mtime = Wed Mar 21 14:45:23 CST 2018
pZxid = 0x262ea78
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x15c150a650f066c
dataLength = 5
numChildren = 0
创建一个序列节点
[zk: localhost:2181(CONNECTED) 14] create -s /hello1 world
Created /hello10000000007
[zk: localhost:2181(CONNECTED) 15] create -s /hello1 world
Created /hello10000000008
[zk: localhost:2181(CONNECTED) 16] ls /
[hello, dubbo, otter, zookeeper, seq, disconf, hello10000000007, hello10000000008, pinpoint-cluster]
[zk: localhost:2181(CONNECTED) 17] get /hello10000000007
world
cZxid = 0x262ea7e
ctime = Wed Mar 21 14:47:51 CST 2018
mZxid = 0x262ea7e
mtime = Wed Mar 21 14:47:51 CST 2018
pZxid = 0x262ea7e
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0
watch
watch的意思是监听感兴趣的事件. 在命令行中, 以下几个命令可以指定是否监听相应的事件.
ls命令. ls命令的第一个参数指定znode, 第二个参数如果为true, 则说明监听该znode的子节点的增减, 以及该znode本身的删除事件.
[zk: localhost:2181(CONNECTED) 27] create /hello world
Created /hello
[zk: localhost:2181(CONNECTED) 28] ls /hello true
[zk: localhost:2181(CONNECTED) 29] create /hello/test item001
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/hello
Created /hello/test
get命令
get命令. get命令的第一个参数指定znode, 第二个参数如果为true, 则说明监听该znode的更新和删除事件.
[zk: localhost:2181(CONNECTED) 30] get /hello true
world
cZxid = 0x262ef5d
ctime = Wed Mar 21 14:52:16 CST 2018
mZxid = 0x262ef5d
mtime = Wed Mar 21 14:52:16 CST 2018
pZxid = 0x262ef5e
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 1
[zk: localhost:2181(CONNECTED) 31] create /hello/test1 item001
Created /hello/test1
[zk: localhost:2181(CONNECTED) 32] rmr /hello
WATCHER::
WatchedEvent state:SyncConnected type:NodeDeleted path:/hello
stat命令
stat命令. stat命令用于获取znode的状态信息. 第一个参数指定znode, 如果第二个参数为true.
[zk: localhost:2181(CONNECTED) 35] create /hello world
WATCHER::
WatchedEvent state:SyncConnected type:NodeCreated path:/hello
Created /hello
[zk: localhost:2181(CONNECTED) 36] stat /hello true
cZxid = 0x262f0f0
ctime = Wed Mar 21 14:56:31 CST 2018
mZxid = 0x262f0f0
mtime = Wed Mar 21 14:56:31 CST 2018
pZxid = 0x262f0f0
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0