DrowYue's Blog


  • 首页

  • 归档

Redis分布式锁笔记

发表于 2019-04-09

本文内容来源于以下两篇文章:

基于Redis的分布式锁到底安全吗(上)?

基于Redis的分布式锁到底安全吗(下)?

一、 基于单Redis节点的分布式锁

获取锁:

1
SET resource_name my_random_value NX PX 30000
  • my_random_value是由客户端生成的一个随机字符串,它要保证在足够长的一段时间内在所有客户端的所有获取锁的请求中都是唯一的
  • NX表示只有当resource_name对应的key值不存在的时候才能SET成功。这保证了只有第一个请求的客户端才能获得锁,而其它客户端在锁被释放之前都无法获得锁
  • PX 30000表示这个锁有一个30秒的自动过期时间
    ​

释放锁:

执行以下Redis Lua脚本:

1
2
3
4
5
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end

重点:

  • 必须要设置一个过期时间。防止客户端获取锁后崩溃导致一直持有该锁
  • 获取锁时保证原子性
  • 获取锁时必须设置一个随机字符串my_random_value,保证了客户端释放的锁必须是自己持有的那个锁
  • 释放锁的操作必须使用Lua脚本来实现,保证原子性
    ​

问题:

【1】当客户端获取锁后,此时Redis Master节点宕机,系统触发failover自动切换到Slave节点,但由于Redis的主从复制是异步的,这可能导致在failover过程中丧失锁的安全性。考虑下面的执行序列:

  1. 客户端1从Master获取了锁
  2. Master宕机了,存储锁的key还没有来得及同步到Slave上
  3. Slave升级为Master
  4. 客户端2从新的Master获取到了对应同一个资源的锁

【2】过期时间设置多长才合适。如果设置太短的话,锁就有可能在客户端完成对于共享资源的访问之前过期,从而失去保护;如果设置太长的话,一旦某个持有锁的客户端释放锁失败,那么就会导致所有其它客户端都无法获取锁,从而长时间内无法正常工作

【3】如果持有锁的客户端长期阻塞导致锁过期(如发生Full GC),共享资源是不是已经失去了保护,如图

​

二、 分布式锁Redlock

分布式锁的算法Redlock,它基于N个完全独立的Redis节点(通常情况下N可以设置成5)

获取锁:

  1. 获取当前时间(毫秒数)

  2. 按顺序依次向N个Redis节点执行获取锁的操作。这个获取操作跟前面基于单Redis节点的获取锁的过程相同,包含随机字符串my_random_value,也包含过期时间(比如PX 30000,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败,比如该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有(注:Redlock原文中这里只提到了Redis节点不可用的情况,但也应该包含其它的失败情况)

  3. 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败

  4. 如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间

  5. 如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起释放锁的操作(即前面介绍的Redis Lua脚本)

    ​

释放锁:

客户端向所有Redis节点发起释放锁的操作,不管这些节点当时在获取锁的时候成功与否

重点:

  • 在最后释放锁的时候,antirez在算法描述中特别强调,客户端应该向所有Redis节点发起释放锁的操作。因为,客户端发给某个Redis节点的获取锁的请求成功到达了该Redis节点,这个节点也成功执行了SET操作,但是它返回给客户端的响应包却丢失了。这在客户端看来,获取锁的请求由于超时而失败了,但在Redis这边看来,加锁已经成功了。所以,释放锁的时候,客户端也应该对当时获取锁失败的那些Redis节点同样发起请求
    ​

问题:

【4】如果有节点发生崩溃重启,还是会对锁的安全性有影响的。具体的影响程度跟Redis对数据的持久化程度有关。假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:

  1. 客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)
  2. 节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了
  3. 节点C重启后,客户端2锁住了C, D, E,获取锁成功
  4. 这样,客户端1和客户端2同时获得了锁(针对同一资源)

【5】由于Redlock本质上是建立在一个同步模型之上,对系统的记时假设(timing assumption)有很强的要求,因此本身的安全性是不够的

解决方案:

【1】由于N个Redis节点中的大多数能正常工作就能保证Redlock正常工作,因此理论上它的可用性更高,单Redis节点的分布式锁在failover的时候锁失效的问题,在Redlock中不存在了

【3】释放锁时会检查my_random_value,若不一致出现释放锁失败,客户端应该回滚事务

【4】延迟重启(delayed restarts)的概念。也就是说,一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间(lock validity time)。这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响

【5】有争议,antirez认为在实际系统中是可以避免大的时钟跳跃的。当然,这取决于基础设施和运维方式

三、 基于ZooKeeper的分布式锁

  • 客户端尝试创建一个znode节点,比如/lock。那么第一个客户端就创建成功了,相当于拿到了锁;而其它的客户端会创建失败(znode已存在),获取锁失败。
  • 持有锁的客户端访问共享资源完成后,将znode删掉,这样其它客户端接下来就能来获取锁了。
  • znode应该被创建成ephemeral的。这是znode的一个特性,它保证如果创建znode的那个客户端崩溃了,那么相应的znode会被自动删除。这保证了锁一定会被释放。

看起来这个锁相当完美,没有Redlock过期时间的问题,而且能在需要的时候让锁自动释放。但仔细考察的话,并不尽然。

ZooKeeper是怎么检测出某个客户端已经崩溃了呢?实际上,每个客户端都与ZooKeeper的某台服务器维护着一个Session,这个Session依赖定期的心跳(heartbeat)来维持。如果ZooKeeper长时间收不到客户端的心跳(这个时间称为Sesion的过期时间),那么它就认为Session过期了,通过这个Session所创建的所有的ephemeral类型的znode节点都会被自动删除。

设想如下的执行序列:

  1. 客户端1创建了znode节点/lock,获得了锁。
  2. 客户端1进入了长时间的GC pause。
  3. 客户端1连接到ZooKeeper的Session过期了。znode节点/lock被自动删除。
  4. 客户端2创建了znode节点/lock,从而获得了锁。
  5. 客户端1从GC pause中恢复过来,它仍然认为自己持有锁。

最后,客户端1和客户端2都认为自己持有了锁,冲突了。这与之前Martin在文章中描述的由于GC pause导致的分布式锁失效的情况类似。

看起来,用ZooKeeper实现的分布式锁也不一定就是安全的。该有的问题它还是有。但是,ZooKeeper作为一个专门为分布式应用提供方案的框架,它提供了一些非常好的特性,是Redis之类的方案所没有的。像前面提到的ephemeral类型的znode自动删除的功能就是一个例子。

还有一个很有用的特性是ZooKeeper的watch机制。这个机制可以这样来使用,比如当客户端试图创建/lock的时候,发现它已经存在了,这时候创建失败,但客户端不一定就此对外宣告获取锁失败。客户端可以进入一种等待状态,等待当/lock节点被删除的时候,ZooKeeper通过watch机制通知它,这样它就可以继续完成创建操作(获取锁)。这可以让分布式锁在客户端用起来就像一个本地的锁一样:加锁失败就阻塞住,直到获取到锁为止。这样的特性Redlock就无法实现。

小结一下,基于ZooKeeper的锁和基于Redis的锁相比在实现特性上有两个不同:

  • 在正常情况下,客户端可以持有锁任意长的时间,这可以确保它做完所有需要的资源访问操作之后再释放锁。这避免了基于Redis的锁对于有效时间(lock validity time)到底设置多长的两难问题。实际上,基于ZooKeeper的锁是依靠Session(心跳)来维持锁的持有状态的,而Redis不支持Sesion。
  • 基于ZooKeeper的锁支持在获取锁失败之后等待锁重新释放的事件。这让客户端对锁的使用更加灵活。

JDK动态代理

发表于 2019-01-14

一、代理模式

代理模式(Proxy):给某一个对象提供一个代理,并由代理对象控制对原对象的引用。

代理类Proxy与被代理类RealSubject均实现了Subject接口,并且Proxy持有RealSubject对象的引用。当Client调用Request()方法时,将调用Proxy.Request(),并由Proxy.Request()代理调用realSubject.Request()

二、JDK动态代理

JDK中基于反射实现了动态代理,可以在程序运行时生成代理类。其核心在于java.lang.reflect.InvocationHandler与 java.lang.reflect.Proxy,InvocationHandler是一个接口只包含invoke方法。

1
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

首先需要实现该接口,并且持有被代理类target的引用,当代理类的任何方法被调用时会转发到调用invoke方法。因此,可以在invoke方法中添加代理逻辑比如:打印日志、安全审计等,最后再通过反射调用被代理类的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class InvocationHandlerImpl<T> implements InvocationHandler {

private T target;

public InvocationHandlerImpl(T target) {
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before hello");
// 调用被代理的方法
Object result = method.invoke(target, args);
System.out.println("after hello");
return result;
}

}

而在程序运行时动态生成代理类则是依靠Proxy.newProxyInstance方法,该方法的参数分别为:

  1. ClassLoader loader,类加载器
  2. Class<?>[] interfaces,代理类需要实现的接口列表
  3. InvocationHandler h,上文实现的代理执行类
1
2
3
4
5
6
public static Object getProxy(Object target) {
InvocationHandlerImpl invocationHandler = new InvocationHandlerImpl(target);
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
invocationHandler);
}

生成的代理类已经实现了指定的接口,因此使用起来与正常的实现没有任何区别

1
2
3
4
public static void main(String[] args) {
HelloService proxyHelloService = (HelloService) getProxy(new HelloServiceImpl());
proxyHelloService.hello("drowyue");
}

输出如下:

1
2
3
before hello
Hello drowyue
after hello

三、原理

Proxy.newProxyInstance的核心代码如下,首先,getProxyClass0方法生成代理类,然后通过反射创建该代理类的对象并返回

1
2
3
4
5
6
7
8
9
10
11
12
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
// 生成代理类
Class<?> cl = getProxyClass0(loader, intfs);
// 反射生成代理类对象,注意构造函数参数为InvocationHandler
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
return cons.newInstance(new Object[]{h});
}

进一步跟踪getProxyClass0方法,并将实际生成代理类保存并反编译如下。代理类继承Proxy并实现了HelloService,m0~m3四个变量分别为equals、toString、hashCode、hello,四个方法。每一个方法的实现都是调用super.h.invoke方法,而父类中的h变量正是前面定义的InvocationHandler,从而实现了将接口中每个方法均代理到InvocationHandler.invoke中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public final class HelloServiceImpl extends Proxy implements HelloService {
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;

// 生成参数为InvocationHandler的构造函数
public HelloServiceImpl(InvocationHandler var1) throws {
super(var1);
}

public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}

public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final String hello(String var1) throws {
try {
return (String)super.h.invoke(this, m3, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}

public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("com.drowyue.java.reflect.proxy.HelloService").getMethod("hello", Class.forName("java.lang.String"));
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}

四、总结

JDK动态代理特点

  • JDK动态代理只能对接口进行代理
  • 生成的代理类都会关联一个InvocationHandler对象,在代理类上调用接口中的方法均会最终调用InvocationHandler.invoke方法执行
  • 除了接口中声明的方法,代理类还会代理 java.lang.Object 中的hashCode、equals、toString三个方法

动态代理不足

  • JDK动态代理只能对接口进行代理,不能对类进行代理,所以实现类中非接口声明的其他方法是无法被代理的
  • JDK动态代理是基于反射调用的,执行效率较低

dubbo特性介绍笔记

发表于 2018-05-28

多协议

  • 不同服务可以使用不同协议

    1
    2
    3
    4
    5
    6
    7
    <!-- 多协议配置 -->
    <dubbo:protocol name="dubbo" port="20880" />
    <dubbo:protocol name="rmi" port="1099" />
    <!-- 使用dubbo协议暴露服务 -->
    <dubbo:service interface="com.alibaba.hello.api.HelloService" version="1.0.0" ref="helloService" protocol="dubbo" />
    <!-- 使用rmi协议暴露服务 -->
    <dubbo:service interface="com.alibaba.hello.api.DemoService" version="1.0.0" ref="demoService" protocol="rmi" />
  • 多协议暴露服务

    1
    2
    3
    4
    5
    <!-- 多协议配置 -->
    <dubbo:protocol name="dubbo" port="20880" />
    <dubbo:protocol name="hessian" port="8080" />
    <!-- 使用多个协议暴露服务 -->
    <dubbo:service id="helloService" interface="com.alibaba.hello.api.HelloService" version="1.0.0" protocol="dubbo,hessian" />

    ​

多注册中心

  • 多注册中心注册

    1
    2
    3
    4
    5
    <!-- 多注册中心配置 -->
    <dubbo:registry id="hangzhouRegistry" address="10.20.141.150:9090" />
    <dubbo:registry id="qingdaoRegistry" address="10.20.141.151:9010" default="false" />
    <!-- 向多个注册中心注册 -->
    <dubbo:service interface="com.alibaba.hello.api.HelloService" version="1.0.0" ref="helloService" registry="hangzhouRegistry,qingdaoRegistry" />
  • 不同服务使用不同注册中心

    1
    2
    3
    4
    5
    6
    7
    <!-- 多注册中心配置 -->
    <dubbo:registry id="chinaRegistry" address="10.20.141.150:9090" />
    <dubbo:registry id="intlRegistry" address="10.20.154.177:9010" default="false" />
    <!-- 向中文站注册中心注册 -->
    <dubbo:service interface="com.alibaba.hello.api.HelloService" version="1.0.0" ref="helloService" registry="chinaRegistry" />
    <!-- 向国际站注册中心注册 -->
    <dubbo:service interface="com.alibaba.hello.api.DemoService" version="1.0.0" ref="demoService" registry="intlRegistry" />

分组聚合

  • 配置

    1
    2
    3
    4
    5
    6
    provider.xml:
    <bean id="mergeService" class="com.alibaba.dubbo.examples.merge.impl.MergeServiceImpl"/>
    <dubbo:service group="merge" interface="com.alibaba.dubbo.examples.merge.api.MergeService" ref="mergeService"/>

    <bean id="mergeService2" class="com.alibaba.dubbo.examples.merge.impl.MergeServiceImpl2"/>
    <dubbo:service group="merge2" interface="com.alibaba.dubbo.examples.merge.api.MergeService" ref="mergeService2"/>
    1
    2
    consumer.xml:
    <dubbo:reference id="mergeService" interface="com.alibaba.dubbo.examples.merge.api.MergeService" group="*"/>
    1
    List<String> result = mergeService.mergeResult();
  • 合并结果扩展

    1
    2
    3
    4
    com.alibaba.dubbo.rpc.cluster.merger.ArrayMerger
    com.alibaba.dubbo.rpc.cluster.merger.ListMerger
    com.alibaba.dubbo.rpc.cluster.merger.SetMerger
    com.alibaba.dubbo.rpc.cluster.merger.MapMerger
  • 自定义扩展

    指定合并策略, 缺省根据返回值类型自动匹配, 如果同类型有两个合并器时,需指定合并器的名称

    1
    2
    3
    4
    @SPI
    public interface Merger<T> {
    T merge(T... items);
    }
    1
    <dubbo:reference id="mergeService" interface="com.alibaba.dubbo.examples.merge.api.MergeService" group="*" merger="xxxMerger"/>

    https://dubbo.gitbooks.io/dubbo-dev-book/content/impls/merger.html

缓存

  • 在消费者端进行结果缓存,用于加速热数据的访问速度, Dubbo提供声明式缓存,以减少用户加缓存的工作量

    1
    2
    3
    threadlocal=com.alibaba.dubbo.cache.support.threadlocal.ThreadLocalCacheFactory
    lru=com.alibaba.dubbo.cache.support.lru.LruCacheFactory
    jcache=com.alibaba.dubbo.cache.support.jcache.JCacheFactory
    1
    <dubbo:reference id="cacheService" interface="com.alibaba.dubbo.examples.cache.api.CacheService" cache="true"/>
  • lru 基于最近最少使用原则删除多余缓存,保持最热的数据被缓存,默认保存1000。

  • threadlocal 当前线程缓存,比如一个页面渲染,用到很多 portal,每个 portal 都要去查用户信息,通过线程缓存,可以减少这种多余访问。

  • jcache 与 JSR107 集成,可以桥接各种缓存实现。

    ​

异步调用

  • 基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务, 相对多线程开销较小

    1
    <dubbo:reference id="asyncService" interface="com.alibaba.dubbo.examples.async.api.AsyncService" async="true"/>

本地存根

  • 远程服务后,客户端通常只剩下接口,而实现全在服务器端,但提供方有些时候想在客户端也执行部分逻辑,那么就在服务消费者这一端提供了一个Stub类,然后当消费者调用provider方提供的dubbo服务时,客户端生成 Proxy 实例,这个Proxy实例就是我们正常调用dubbo远程服务要生成的代理实例,然后消费者这方会把 Proxy 通过构造函数传给消费者方的Stub ,然后把 Stub 暴露给用户,Stub 可以决定要不要去调 Proxy。会通过代理类去完成这个调用,这样在Stub类中,就可以做一些额外的事,来对服务的调用过程进行优化或者容错的处理。

本地伪装

  • 通常用于服务降级,比如某验权服务,当服务提供方全部挂掉后,客户端不抛出异常,而是通过 Mock 数据返回授权失败。

  • Mock是Stub 的一个子集,便于服务提供方在客户端执行容错逻辑

    1
    2
    <dubbo:reference interface="com.foo.BarService" mock="true" />
    <dubbo:reference interface="com.foo.BarService" mock="com.foo.BarServiceMock" />
    1
    2
    3
    4
    5
    6
    7
    package com.foo;
    public class BarServiceMock implements BarService {
    public String sayHello(String name) {
    // 你可以伪造容错数据,此方法只在出现RpcException时被执行
    return "容错数据";
    }
    }
    1
    2
    3
    4
    5
    6
    Offer offer = null;
    try {
    offer = offerService.findOffer(offerId);
    } catch (RpcException e) {
    logger.error(e);
    }
    1
    <dubbo:reference interface="com.foo.BarService" mock="return null" />

并发控制

  • 客户端并发控制

    1
    2
    <!-- 设置com.test.UserServiceBo接口中所有方法,每个方法最多同时并发请求10个请求 -->
    <dubbo:reference id="userService" interface="com.test.UserServiceBo" group="dubbo" version="1.0.0" timeout="3000" actives="10"/>
    1
    2
    3
    4
    <!-- 设置接口中的单个方法的并发请求个数 -->
    <dubbo:reference id="userService" interface="com.test.UserServiceBo" group="dubbo" version="1.0.0" timeout="3000">
    <dubbo:method name="sayHello" actives="10" />
    </dubbo:reference>

    如果客户端请求该方法并发超过了10则客户端会被阻塞,等客户端并发请求数量少于10的时候,该请求才会被发送 到服务提供方服务器,客户端并发控制是使用ActiveLimitFilter过滤器来控制的。

  • 服务端并发控制

    1
    2
    <!-- 设置接口中所有方法,每个方法最多同时并发处理10个请求 -->
    <dubbo:service interface="com.test.UserServiceBo" ref="userService" group="dubbo" version="1.0.0" timeout="3000" executes="10"/>
    1
    2
    3
    4
    <!-- 设置接口中的单个方法的并发请求个数 -->
    <dubbo:service interface="com.test.UserServiceBo" ref="userService" group="dubbo" version="1.0.0" timeout="3000" >
    <dubbo:method name="sayHello" executes="10" />
    </dubbo:service>

    服务提供方设置并发数量后,如果同时请求数量大于了设置的executes的值,则会抛出异常,而不是像消费端设置actives时候,会等待。服务提供方并发控制是使用ExecuteLimitFilter过滤器实现的。

  • Load Balance 均衡

    1
    2
    <dubbo:reference interface="com.foo.BarService" loadbalance="leastactive" />
    <dubbo:service interface="com.foo.BarService" loadbalance="leastactive" />

    可选值:random,roundrobin,leastactive(随机、轮循、最少活跃连接数)

连接控制

  • 服务端连接控制

    1
    2
    <dubbo:provider protocol="dubbo" accepts="10" />
    <dubbo:protocol name="dubbo" accepts="10" />
  • 客户端连接控制

    如果是长连接,例如 Dubbo 协议,connections 表示该服务对每个提供者建立的长连接数

    1
    2
    <dubbo:reference interface="com.foo.BarService" connections="10" />
    <dubbo:service interface="com.foo.BarService" connections="10" />

    com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol#getClients

    com.alibaba.dubbo.rpc.protocol.dubbo.DubboInvoker#doInvoke

    Dubbo源代码实现五:RPC中的服务消费方实现

线程栈自动dump

  • 当业务线程池满时,我们需要知道线程都在等待哪些资源、条件,以找到系统的瓶颈点或异常点。dubbo通过Jstack自动导出线程堆栈来保留现场,方便排查问题默认策略:

    • 导出路径,user.home标识的用户主目录
    • 导出间隔,最短间隔允许每隔10分钟导出一次

    指定导出路径:

    1
    2
    3
    4
    5
    6
    # dubbo.properties
    dubbo.application.dump.directory=/tmp

    <dubbo:application>
    <dubbo:parameter key="dump.directory" value="/tmp" />
    </dubbo:application>

Hello World

发表于 2017-11-30

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

DrowYue

4 日志
3 标签
© 2020 DrowYue
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.3