0%

redis中set系列命令(包括set,hset等等),基本上都包括两个版本,纯粹的set和setnx, setnx即set not exist, 也就是只有Key不存在时才会执行set, 而不会覆盖原有的值。

但是hmset这个命令,包括redis本身,jedis都没有提供nx版本的支持。当然,hset这个命令是有对应的hsetnx版本的,hmset意思就是multi hset,一次可以操作多个key, 从而减小网络开销。

所以,为了在使用hmset时也能降低网络的消耗,用lua写了一个脚本,实现hmsetnx的效果,即:向Hash表中set键值对时,只有键不存在时才会写入,不会覆盖原有值。

1
2
3
4
5
6
7
8
9
10
local key
for i,j in ipairs(ARGV)
do if i%2 == 0
then
redis.call('hsetnx', KEYS[1], key,j)
else
key = j
end
end
return 1

脚本的原理还是比较简单,脚本中使用的参数和hmset完全一致。依次读入参数列表,迭代器i是奇数时给key赋值,偶数时执行一次hsetnx,循环结束后也就完成了。

之后再调用jedis封装好的eval接口,

Object eval(final String script, final List keys, final List args)

或者

Object eval(final byte[] script, final List<byte[]> keys, final List<byte[]> args)

都可以,这两个接口的区别就是是否对参数进行序列化

keys中只放一个元素,就是hash表本身的key, 然后把键值对按照一个key,一个value的顺序依次放到args里。

当然,也可以用evalsha命令避免每次操作都要传输脚本本身,这里就不细说了。

为了方便,最近用vitualbox搭了一个centos7的虚拟机,整个过程比较简单,在这里记录一下。

下载vitualbox

直接去官网(https://www.virtualbox.org/wiki/Downloads)下载即可

下载centos安装包

同样官网下载(https://www.centos.org/download/),我下载的是minimal iso

安装

安装过程很简单,一路默认点下去就可以,中间内存、分区什么的可以根据需要调一下

配置本机ssh访问

vitualbox默认的分辨率非常低,可以通过安装增强工具进行优化。不过由于我们不需要图形化界面,其实可以通过其他方式解决这一问题,就是用xshell或者putty通过ssh远程登陆到虚拟机上。

打开ssh服务

1
2
service sshd start
chkconfig sshd on

分别启动ssh服务,并将ssh设定为自启动

关闭防火墙

由于只是弄着玩的,直接把防火墙关掉,方便。

centos7的防火墙操作和之前版本区别很大:

1
2
sudo systemctl stop firewalld.service
sudo systemctl disable firewalld.service

关闭防火墙和自动启动

配置端口转发

在VitualBox下配置端口转发:设置-网络-高级-端口转发,将22端口转发到主机的端口上,可以同样是22,也可以配置成其他端口.

如果需要在主机上访问虚拟机的其他端口,例如tomcat的8080,activemq的61616,8161,也可以在这儿一并配了。

查看虚拟机ip

在主机上执行ipconfig,找sudo systemctl disable firewalld.service对应的ip, 然后就可以在xshell中配置对应的ip和端口,访问虚拟机了。

配置自启动服务

centos下配置自启动的方式很多,我们在这里提供一种最简单的方式。

写一个脚本

例如vim /opt/app/service.sh

1
2
3
#!/bin/bash
export JAVA_HOME=/opt/app/jdk1.8.0_121
sh /opt/app/apache-activemq-5.14.4/bin/activemq start >/opt/app/start.log

把需要自启动的脚本全都放这儿,以后想增加自启动服务的时候,也只需要操作这个脚本。

配置

chmod +x /opt/app/service.sh

centos7下/etc/rc.d/rc.local也需要自己去加执行权限:

chmod +x /etc/rc.d/rc.local

然后打开/etc/rc.d/rc.local,在最后把自己写的脚本加上:

/opt/app/service.sh

保存,就完成自启动服务的配置了。

之后,我们可以通过vitualbox的无界面方式启动,然后在xshell中自由操作。

hawtio(hawt.io)是一个开源的监控系统,它提供了多种启动方式,可以运行单独的jar包、war包,然后远程连接其他应用进行监控,也可以将它直接嵌到我们自己的应用中。

本文会介绍在一个单独的java进程(java standalone application)中嵌入hawtio,对应官方文档(http://hawt.io/getstarted/index.html)的 “Using hawtio inside a stand alone Java application”,不过这一节文档问题是比较多的,如果你只看这段,会遇到各种问题。

下面介绍具体步骤

引入jar包

除了官方文档里说的hawtio-embedded外,hawtio-insight-log,hawtio-core,hawtio-web这几个包都是必需的,我们都引入当前的最新版本.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
<groupId>io.hawt</groupId>
<artifactId>hawtio-embedded</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>io.hawt</groupId>
<artifactId>hawtio-insight-log</artifactId>
<version>1.5.0</version>
</dependency>
<dependency>
<groupId>io.hawt</groupId>
<artifactId>hawtio-core</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>io.hawt</groupId>
<artifactId>hawtio-web</artifactId>
<version>2.0.0</version>
</dependency>

下载war包

同样在官方start文档(http://hawt.io/getstarted/index.html)中下载hawtio-default.war包,放到任意位置,war包的名字也可以随便改

代码

基本的代码,如果不需要其他配置,非常简单

1
2
3
Main main = new Main();
main.setWar("hawtio.war");
main.run();

使用main.setWar配置的是war包具体路径,可以在工程内,也可以在工程外,但是并非官方文档所说的“包含war包的目录路径(somePathOrDirectoryContainingHawtioWar)”
之后运行main.run就可以启动hawtio了.

权限

新版本的hawtio默认是要密码的,如果想简单,可以配置一条jvm参数: -Dhawtio.authenticationEnabled=false, 关掉权限验证。

示例代码可以在github (https://github.com/lcy362/CamelDemo/blob/master/src/main/java/com/mallow/demo/camel/MainWithHawtio.java) 上看,可以直接下载运行,不过需要在本地启动一个activemq. 这里的例子是一个使用hawtio监控apache-camel的简单例子,启动后,在hawtio页面上栏可以直接看到camel的标签,使用非常方便。

目前,java下应用最广泛的日志系统主要就是两个系列: log4j和slf4j+logback 。

其中,slf4j只包含日志的接口,logback只包括日志的具体实现,两者加起来才是一个完整的日志系统。Log4j则同时包含了日志接口和实现。

这两套日志系统之间有可以相互兼容的组件,分别是slf4j-log4j12和 log4j-over-slf4j,引入之后就可以用log4j打出slf4j接口的日志,或者用logback打出log4j接口的日志。

背景知识介绍到这里,再简单说一下标题里提到的问题。问题的现象就是我们在war包里配置了log4j的日志级别为info, 但在catalina里却一直在打大量的debug日志。初看现象肯定很诡异,前期各种研究tomcat配置也没什么头绪。直到磁盘压力太大,去看jstack发现大量进程是等待在Logback代码中,才发现之前关注错了重点。再去具体了解了java下的日志系统后,问题也就很明了了。

先把几个事实摆出来:
1. 打出的debug日志都是用slf4j写的,根据堆栈得知logback具体执行了日志打印
2. logback在无配置文件时默认debug级别
3. 我们的war包中同时包含logback,log4j和slf4j-log4j12
4. Slf4j无法主动选择具体的日志实现
想必看到这里,大家也明白了问题所在。根据我们引入的包,log4j和logback都可以实现打印slf4j日志,而具体谁来打,不是一个用正常办法可以控制的事情,在这个具体案例下,logback就成了具体的日志打印者。而因为我们其实是想用lo4j打日志,所以没有配logback配置,所以logback就按默认的debug级别打了大量日志。

解决办法也很简单,就是把logback的包全去掉。看似有些暴力,但确实是最合理的一个解决办法。

最后提供一个排查日志问题的通用套路,免得找不到方向乱看。
1. 找几行不符合自己日志配置的具体日志,翻阅对应代码,看看是哪个日志接口打的
2. 查jar包,看看这套日志框架有哪些具体实现
3. 把多的jar包去掉

对于一个java object的序列化,想测一下使用json和使用一般序列化工具,在时间性能、空间性能上的区别。

json选择用fastjson.

序列化工具使用了protostuff和kyro. 为什么不用protobuf呢?因为感觉对于一个已有的上百个属性的java class来说,再去新建一个匹配的proto文件有点反人类。protostuff是protobuf的改良版本,可以直接将一个java object进行序列化,使用方法与kyro有点类似,没有protobuf那么多中间过程。其他的,hession, java自带序列化之类的,据说性能比kryo和protobuf差很多,就不测了。

简单测了一下,发现差距还挺明显的,所以感觉也不需要做具体的评测了。把日志截一段发出来,大家感受下。

fastjson serilise cost <span class="hljs-number">555805</span>  <span class="hljs-built_in">length</span>: <span class="hljs-number">1740</span>
kyro serilise cost <span class="hljs-number">227375</span>   length502
protostuff serilise cost <span class="hljs-number">78950</span>   length633
fastjson deserilise cost <span class="hljs-number">130662</span>
kyro deserilise cost <span class="hljs-number">201716</span>
protostuff deserilise cost <span class="hljs-number">230533</span>
fastjson serilise cost <span class="hljs-number">727915</span>  <span class="hljs-built_in">length</span>: <span class="hljs-number">1740</span>
kyro serilise cost <span class="hljs-number">378958</span>   length502
protostuff serilise cost <span class="hljs-number">94739</span>   length633
fastjson deserilise cost <span class="hljs-number">154346</span>
kyro deserilise cost <span class="hljs-number">373432</span>
protostuff deserilise cost <span class="hljs-number">219085</span>
fastjson serilise cost <span class="hljs-number">804892</span>  <span class="hljs-built_in">length</span>: <span class="hljs-number">1740</span>
kyro serilise cost <span class="hljs-number">392380</span>   length502
protostuff serilise cost <span class="hljs-number">220664</span>   length633
fastjson deserilise cost <span class="hljs-number">243560</span>
kyro deserilise cost <span class="hljs-number">360010</span>
protostuff deserilise cost <span class="hljs-number">132241</span>
fastjson serilise cost <span class="hljs-number">601991</span>  <span class="hljs-built_in">length</span>: <span class="hljs-number">1740</span>
kyro serilise cost <span class="hljs-number">244349</span>   length502
protostuff serilise cost <span class="hljs-number">80924</span>   length633
fastjson deserilise cost <span class="hljs-number">241191</span>
kyro deserilise cost <span class="hljs-number">230928</span>
protostuff deserilise cost <span class="hljs-number">127109</span>

cost的时间用的是System.nanoTime(); 三种用的都是不加任何配置的默认配置。

序列化之后的占用空间,kryo略低于protostuff, 两者都远高于json. 这是很好理解的,毕竟json串是可读的,不要强求太多。

而序列化和反序列化的耗时,都是protostuff优于kyro优于fastjson, 而且差别挺明显。

所以结论呢,如果对空间没有极其苛刻的要求,protostuff也许是最佳选择。protostuff相比于kyro还有一个额外的好处,就是如果序列化之后,反序列化之前这段时间内,java class增加了字段(这在实际业务中是无法避免的事情),kyro就废了。但是protostuff只要保证新字段添加在类的最后,而且用的是sun系列的JDK, 是可以正常使用的。因此,如果序列化是用在缓存等场景下,序列化对象需要存储很久,也就只能选择protostuff了。

当然,如果有可读性之类的需求,就只能用json了。

 

这篇文章只讲使用,不讲原理,简单粗暴。

分布式锁,顾名思义,就是分布式的锁,应用于一些分布式系统中。例如,有一个服务部在数太机器上,然后有可能操作数据库中的同一条记录。这时,就需要分布式锁。

分布式锁实现的方式很多,一般来说需要一个实体来代表一个锁,占用锁时就新建这个实体,锁释放时也对应将相应实体删除。同时,一般还需要一个锁超时过期的策略,避免一些异常情况造成锁无法被释放。

zookeeper和redis都是常用的实现分布式锁的方式。接下来就简单介绍一下这两种方式的使用。

基于zookeeper的分布式锁

使用zookeeper的话,建议直接使用curator客户端.

        <span class="hljs-tag">&lt;<span class="hljs-title">dependency</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-title">groupId</span>&gt;</span>org.apache.curator<span class="hljs-tag">&lt;/<span class="hljs-title">groupId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-title">artifactId</span>&gt;</span>curator-recipes<span class="hljs-tag">&lt;/<span class="hljs-title">artifactId</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-title">version</span>&gt;</span>2.9.1<span class="hljs-tag">&lt;/<span class="hljs-title">version</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-title">dependency</span>&gt;</span>`</pre>

curator中实现了一个InterProcessSemaphoreMutex类,用作分布式锁。

实现原理其实也很直白:建立锁的时候约定一个路径新建一个节点,作为锁的实体;锁释放时就将这个节点删除。

代码例子片段:

<pre class="prettyprint">`InterProcessSemaphoreMutex lock;
lock.acquire(&hellip;);  <span class="hljs-comment">//acquire获取锁</span>
&hellip;.
lock.release();释放锁

详细的使用例子代码可以参考 https://github.com/lcy362/Scenes/blob/master/src/main/java/com/mallow/concurrent/zklock/InterProcessMutexExample.java

基于redis的分布式锁

同样推荐一个第三方的redis客户端redisson, https://github.com/redisson/redisson. redisson的知名度不如curator高,但也是一个非常优秀的开源工具,支持各种集群、数据结构。

redis锁的原理就是占用锁时新建一个key, 锁释放时key删除。

代码示例可以参考https://github.com/lcy362/Scenes/blob/master/src/main/java/com/mallow/concurrent/redislock/ValuelockExample.java

题目

Given a string, find the length of the longest substring without repeating characters.

Examples:

Given “abcabcbb”, the answer is “abc”, which the length is 3.

Given “bbbbb”, the answer is “b”, with the length of 1.

Given “pwwkew”, the answer is “wke”, with the length of 3. Note that the answer must be a substring, “pwke” is a subsequence and not a substring.

也就是说给定一个字符串,输出不包含重复字母的最长子串长度。

思路

遍历一次字符串,O(n)复杂度下可以解决。主要思路就是在遍历的过程中

1. 记录每个字母上一次出现的位置

2. 维持一个从当前位置往前数不包含重复字母的子串,记录这个字串的起止位置start, end

遍历的过程中就是根据相应位置字母是否出现过,以及上次出现的位置,不断更新start, end的过程。

代码

可以到github上查看: https://github.com/lcy362/Algorithms/tree/master/src/main/java/com/mallow/algorithm

<span class="hljs-keyword">import</span> java.util.HashMap;

<span class="hljs-javadoc">/**
 * leetcode 3
 * https://leetcode.com/problems/longest-substring-without-repeating-characters/
 * Created by lcy on 2017/2/15.
 */</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LongestSubstringNotRepeat</span> {</span>
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> <span class="hljs-title">lengthOfLongestSubstring</span>(String s) {
        <span class="hljs-keyword">if</span> (s.length() &lt;= <span class="hljs-number">1</span>) {
            <span class="hljs-keyword">return</span> s.length();
        }
        HashMap&lt;Character, Integer&gt; charPos = <span class="hljs-keyword">new</span> HashMap&lt;&gt;();
        <span class="hljs-keyword">char</span>[] chars = s.toCharArray();
        <span class="hljs-keyword">int</span> len = <span class="hljs-number">0</span>;
        <span class="hljs-keyword">int</span> max = <span class="hljs-number">0</span>;
        <span class="hljs-keyword">int</span> start = <span class="hljs-number">0</span>;
        <span class="hljs-keyword">int</span> end = <span class="hljs-number">0</span>;
        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; chars.length; i++) {
            <span class="hljs-keyword">if</span> (charPos.containsKey(chars[i])) {
                <span class="hljs-keyword">int</span> tempstart = charPos.get(chars[i]) + <span class="hljs-number">1</span>;
                <span class="hljs-keyword">if</span> (tempstart &gt; start) {
                    start = tempstart;
                }
                end++;
                len = end - start;
            } <span class="hljs-keyword">else</span> {
                len++;
                end++;
            }
            charPos.put(chars[i], i);
            <span class="hljs-keyword">if</span> (len &gt; max) {
                max = len;
            }
        }
        <span class="hljs-keyword">return</span> max;
    }

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span>(String args[]) {
        LongestSubstringNotRepeat l = <span class="hljs-keyword">new</span> LongestSubstringNotRepeat();
        System.out.println(l.lengthOfLongestSubstring(<span class="hljs-string">"abcabcbb"</span>));
        System.out.println(l.lengthOfLongestSubstring(<span class="hljs-string">"bbbbb"</span>));
        System.out.println(l.lengthOfLongestSubstring(<span class="hljs-string">"pwwkew"</span>));
        System.out.println(l.lengthOfLongestSubstring(<span class="hljs-string">"abba"</span>));
    }

主要还是通过一个例子加深一下对java多线程里wait,notify的理解,因此写了一个例子,三个线程分别输出A,B,C三个字母,控制这三个线程的执行顺序,从而实现ABCABCABC..这样的输出。

这个问题主要还是需要设计一下锁的策略,这里只是提供了一种方式:

每个线程占用两把锁,分别代表自己(self)和前一个线程(prev), 三个线程的持有锁情况如下表所示:

线程号prev锁self锁
A c a
B a b
C b c

A 首先启动,持有ac, 运行后先释放a, b可以执行。

线程run方法代码如下:

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">run</span>() {
        <span class="hljs-keyword">while</span> (<span class="hljs-keyword">true</span>) {
            <span class="hljs-keyword">synchronized</span> (prev) {
                <span class="hljs-keyword">synchronized</span> (self) {
                    System.out.print(name);
                    self.notify();
                }
                <span class="hljs-keyword">try</span> {
                    <span class="hljs-comment">//如果想控制输出速度, 可以将sleep加在此处</span>
                    <span class="hljs-comment">//如果加在sout之后,会导致c线程启动并占有b锁之后,a线程才会释放a锁,输出顺序会变成acbacb</span>
                    <span class="hljs-comment">//也可以加大三个线程启动的间隔时间解决这一问题</span>
                    <span class="hljs-keyword">try</span> {
                        Thread.sleep(<span class="hljs-number">1000</span>);
                    } <span class="hljs-keyword">catch</span> (InterruptedException e) {
                        e.printStackTrace();
                    }
                    prev.wait();
                } <span class="hljs-keyword">catch</span> (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }

需要注意的是,如果想控制输出速度,需要考虑一下sleep的位置和时间,避免在A线程执行完并释放a锁之前,C线程已经启动并持有了B锁,导致B线程无法正常启动。

问题过程

我司需要接收很多外部数据,数据源的形式很多,ibmmq, activemq, redis pubsub, 等等都有。为了将这些数据接到内部amq/kafka,之前运行了一大批进程,管理起来十分复杂,因此最近用apache-camel对这些进程作了整合。

上线几个小时之后,kafka磁盘空间开始报警。初步断定是这次上线导致的。

排查流程

主要还是对kafka不熟悉,只是能用而已,因此排查过程走了不少弯路。

由于camel本身文档很不完善,一开始配置参数时也主要靠看源码+猜,所以首先怀疑配置的压缩参数compressionCodec=gzip是否无效。

因此进行了一次简单的测试(别问我为什么一开始不测)。建了一个单分区的测试topic, 然后分别使用gzip和none模式发送相同的数据,观察kafka的log file文件大小变化趋势。发现压缩确实是有效的。

因此又跟直接使用kafka api作了对比,发现差别非常大,即使是压缩之后,使用camel-kafka占用的空间也比使用api大数倍。然后去查了两边源码,发现最底层的代码是完全一致的,所以还是怀疑某些参数配的不对。

之后又注意到一个细节,使用camel时,log的大小跟消息数呈线性关系,比如一条占1字节,10条就占10字节。但使用api的话,1条也占1字节,连续发10条可能才占两字节。

这时候就怀疑kafka是不是有批量发送之类的机制,然后咨询了负责kafka的同事,果然是这个原因,而造成占用空间差别大的原因就是是否同步发的区别,同步发的话就不存在批量发送了。批量发送的话,这一批消息会被压缩在一起,而单条发时,就是每一条分别压缩。我们知道,在文件非常小的时候,使用gzip压缩的效果是很差的,甚至可能压完比源文件还大。然后又做了些测试,确定了是这个问题。这个参数被同事封装在了kafka的client接口里,因此导致我照着之前代码改参数时漏掉了这一个。

一些感悟

其实回想起来,这个问题挺low的,如果对kafka多些了解,是不会有这种问题的。

首先,对kafka没做过全面的了解,只是学会了怎么用,大概了解了一下它是什么。而很多基本的机制都没有概念。使用一个开源工具时,对它做一次全面的了解还是很重要的,虽然每个工具都深入研究底层代码不现实,但是系统性的了解一遍这些工具有什么机制,确实花不了多少时间。

再一个,公司内一般会封装一些访问各种组件的工具包,以提升效率,这些工具包最好也了解一下怎么实现的,否则可能不经意间就掉坑里了。

任重而道远啊..

Integer类实质上也是一个普通的java类,即使值相同,也是不同的对象。

例如

        Integer a = <span class="hljs-number">148</span>;
        Integer b = <span class="hljs-number">148</span>;
        System.out.println(a==b);`</pre>

这时输出为false. 很容易理解。

但是如果把值换成128以下的数,比如48.

<pre class="prettyprint">`        Integer a = <span class="hljs-number">48</span>;
        Integer b = <span class="hljs-number">48</span>;
        System.out.println(a==b);`</pre>

这时就会发现输出变成了true。原因是jdk对128以下的整数作了缓存,当声明两个值为48的Integer对象时,其实是指向同一位置。

当然也可以强制声明一个新的Integer对象。

<pre class="prettyprint">`        Integer a = <span class="hljs-number">48</span>;
        Integer b = <span class="hljs-keyword">new</span> Integer(<span class="hljs-number">48</span>);
        System.out.println(a==b); 

这时输出就变成false了