redis简报

REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统, 与memcached相比,redis支持更丰富的数据结构,特点是高性能、持久存储,适应高并发的应用场景。它起步较晚,发展迅速, 目前已被许多大型机构采用,比如Twitter、Github、新浪微博等。

Redis安装

redis的安装没有其他依赖,非常简单。操作如下:

wget http://download.redis.io/releases/redis-2.6.16.tar.gz
tar zxvf redis-2.6.16.tar.gz
cd redis-2.6.16/
make

这样就会在src目录下生成以下几个可执行文件:redis-benchmark、redis-check-aof、redis-check-dump、redis-cli、redis-server。 这几个文件加上redis.conf就是redis的最终可用包了。可以考虑把这几个文件拷贝到你希望的地方。例如:

mkdir -p /usr/local/redis/bin
mkdir -p /usr/local/redis/etc
cp redis.conf /usr/local/redis/etc
cd src
cp redis-benchmark redis-check-aof redis-check-dump redis-cli redis-server /usr/local/redis/bin

现在就可以启动redis了。

cd /usr/local/redis
bin/redis-server etc/redis.conf

注意,默认复制过去的redis.conf文件的daemonize参数为no,所以redis不会在后台运行,可以修改为yes则为后台运行redis。 另外配置文件中规定了pid文件,log文件和数据文件的地址,如果有需要先修改,默认log信息定向到stdout. 这时候就可以打开终端进行测试了,默认的监听端口是6379,所以用telnet进行连接如下:

# telnet localhost 6379
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
SET hello world
+OK
GET hello
$5
world
quit
+OK
Connection closed by foreign host.

或者使用redis-cli客户端:

# redis-cli
redis 127.0.0.1:6379> SET hello world
OK
redis 127.0.0.1:6379> GET hello
"world"

redis数据结构

redis相对memcached来说,支持更加丰富的数据结构,正如作者所说的,redis是一个数据结构服务器(data structures server), redis的所有功能就是将数据以其固有的几种结构保存,并提供给用户操作这几种结构的接口。redis目前支持以下几种数据类型:

  • String类型

字符串是最简单的类型,和memcached支持的类型是一样的,但是在功能上更加丰富。

redis 127.0.0.1:6379> SET name "Redis 2.6.16"
OK
redis 127.0.0.1:6379> GET name
"Redis 2.6.16"

另外,它还支持批量读写:

redis 127.0.0.1:6379> MSET age 30 sex Male
OK
redis 127.0.0.1:6379> MGET age sex
1) "30"
2) "Male"

还可以当成数字来使用,并支持对数字的加减操作:

redis 127.0.0.1:6379> INCR age
(integer) 31
redis 127.0.0.1:6379> INCRBY age 2
(integer) 33
redis 127.0.0.1:6379> GET age
"33"
redis 127.0.0.1:6379> DECR age
(integer) 32
redis 127.0.0.1:6379> DECRBY age 2
(integer) 30
redis 127.0.0.1:6379> GET age
"30"

还支持对字符串进行部分修改或获取操作

redis 127.0.0.1:6379> STRLEN name
(integer) 12
redis 127.0.0.1:6379> GETRANGE name 0 4
"Redis"
redis 127.0.0.1:6379> APPEND name ", NoSQL"
(integer) 19
redis 127.0.0.1:6379> GET name
"Redis 2.6.16, NoSQL"
  • List类型

Redis能够把数据存储成一个链表,并能对这个链表进行操作:

redis 127.0.0.1:6379> LPUSH language Java
(integer) 1
redis 127.0.0.1:6379> LPUSH language C++
(integer) 2
redis 127.0.0.1:6379> RPUSH language C
(integer) 3
redis 127.0.0.1:6379> LLEN language
(integer) 3
redis 127.0.0.1:6379> LRANGE language 0 2
1) "C++"
2) "Java"
3) "C"
redis 127.0.0.1:6379> LPOP language
"C++"
redis 127.0.0.1:6379> LLEN language
(integer) 2
redis 127.0.0.1:6379> LREM language 1 Java
(integer) 1
redis 127.0.0.1:6379> LLEN language
(integer) 1

Redis也支持很多修改操作

redis 127.0.0.1:6379> LRANGE language 0 2
1) "C"
redis 127.0.0.1:6379> LINSERT language BEFORE C C++
(integer) 2
redis 127.0.0.1:6379> LINSERT language BEFORE C Java
(integer) 3
redis 127.0.0.1:6379> LLEN language
(integer) 3
redis 127.0.0.1:6379> LRANGE language 0 2
1) "C++"
2) "Java"
3) "C"
redis 127.0.0.1:6379> LTRIM language 2 -1
OK
redis 127.0.0.1:6379> LLEN language
(integer) 1
redis 127.0.0.1:6379> LRANGE language 0 2
1) "C"
  • Sets类型

Redis能够将一系列不重复的值存储成一个集合,并支持修改和集合关系操作。

redis 127.0.0.1:6379> SADD system Win
(integer) 1
redis 127.0.0.1:6379> SADD system Linux
(integer) 1
redis 127.0.0.1:6379> SADD system Mac
(integer) 1
redis 127.0.0.1:6379> SADD system Linux
(integer) 0
redis 127.0.0.1:6379> SMEMBERS system
1) "Win"
2) "Mac"
3) "Linux"

Sets结构也支持相应的修改操作

redis 127.0.0.1:6379> SREM system Win
(integer) 1
redis 127.0.0.1:6379> SMEMBERS system
1) "Mac"
2) "Linux"
redis 127.0.0.1:6379> SADD system Win
(integer) 1
redis 127.0.0.1:6379> SMEMBERS system
1) "Mac"
2) "Win"
3) "Linux"

Redis还支持对集合的子交并补等操作

redis 127.0.0.1:6379> SADD phone Android
(integer) 1
redis 127.0.0.1:6379> SADD phone Iphone
(integer) 1
redis 127.0.0.1:6379> SADD phone Win
(integer) 1
redis 127.0.0.1:6379> SMEMBERS phone
1) "Win"
2) "Iphone"
3) "Android"
redis 127.0.0.1:6379> SINTER system phone
1) "Win"
redis 127.0.0.1:6379> SUNION system phone
1) "Win"
2) "Iphone"
3) "Mac"
4) "Linux"
5) "Android"
redis 127.0.0.1:6379> SDIFF system phone
1) "Mac"
2) "Linux"
  • Sorted Sets类型

Sorted Sets和Sets结构非常相似,不同的是Sorted Sets中的数据会有一个score属性,并会在写入时就按这个score排好序。

redis 127.0.0.1:6379> ZADD days 0 mon
(integer) 1
redis 127.0.0.1:6379> ZADD days 1 tue
(integer) 1
redis 127.0.0.1:6379> ZADD days 2 wed
(integer) 1
redis 127.0.0.1:6379> ZADD days 3 thu
(integer) 1
redis 127.0.0.1:6379> ZADD days 4 fri
(integer) 1
redis 127.0.0.1:6379> ZADD days 5 sat
(integer) 1
redis 127.0.0.1:6379> ZADD days 6 sun
(integer) 1
redis 127.0.0.1:6379> ZCARD days
(integer) 7
redis 127.0.0.1:6379> ZRANGE days 0 6
1) "mon"
2) "tue"
3) "wed"
4) "thu"
5) "fri"
6) "sat"
7) "sun"
redis 127.0.0.1:6379> ZSCORE days sat
"5"
redis 127.0.0.1:6379> ZCOUNT days 3 6
(integer) 4
redis 127.0.0.1:6379> ZRANGEBYSCORE days 3 6
1) "thu"
2) "fri"
3) "sat"
4) "sun"
  • Hash类型

Redis能够存储多个键值对的数据

redis 127.0.0.1:6379> HMSET student name Tom age 12 sex Male
OK
redis 127.0.0.1:6379> HKEYS student
1) "name"
2) "age"
3) "sex"
redis 127.0.0.1:6379> HVALS student
1) "Tom"
2) "12"
3) "Male"
redis 127.0.0.1:6379> HGETALL student
1) "name"
2) "Tom"
3) "age"
4) "12"
5) "sex"
6) "Male"
redis 127.0.0.1:6379> HDEL student sex
(integer) 1
redis 127.0.0.1:6379> HGETALL student
1) "name"
2) "Tom"
3) "age"
4) "12"

Redis能够支持Hash的批量修改和获取

redis 127.0.0.1:6379> HMSET kid name Akshi age 2 sex Female
OK
redis 127.0.0.1:6379> HMGET kid name age sex
1) "Akshi"
2) "2"
3) "Female"

在这些数据结构的基础上,跟memcached一样,Redis也支持设置数据过期时间,并支持一些简单的组合型的命令。

设置数据过期时间

redis 127.0.0.1:6379> SET name "John Doe"
OK
redis 127.0.0.1:6379> EXISTS name
(integer) 1
redis 127.0.0.1:6379> EXPIRE name 5
(integer) 1

5秒后再查看

redis 127.0.0.1:6379> EXISTS name
(integer) 0
redis 127.0.0.1:6379> GET name
(nil)

简单的组合型的命令。通过MULTI和EXEC,将几个命令组合起来执行

redis 127.0.0.1:6379> SET counter 0
OK
redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379> INCR counter
QUEUED
redis 127.0.0.1:6379> INCR counter
QUEUED
redis 127.0.0.1:6379> INCR counter
QUEUED
redis 127.0.0.1:6379> EXEC
1) (integer) 1
2) (integer) 2
3) (integer) 3
redis 127.0.0.1:6379> GET counter
"3"

你还可以用DICARD命令来中断执行中的命令序列

redis 127.0.0.1:6379> SET newcounter 0
OK
redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379> INCR newcounter
QUEUED
redis 127.0.0.1:6379> INCR newcounter
QUEUED
redis 127.0.0.1:6379> INCR newcounter
QUEUED
redis 127.0.0.1:6379> DISCARD
OK
redis 127.0.0.1:6379> GET newcounter
"0"

JUnit参数化介绍与实践

junit在4.11版本实现了参数化功能,基于这个功能,相当于可以动态生成测试用例的。先看看官方例子:

@RunWith(Parameterized.class)
public class FibonacciTest {

    @Parameters(name = "{index}: fib({0})={1}")
    public static Iterable<Object[]> data() {
        return Arrays.asList(new Object[][] { { 0, 0 }, { 1, 1 }, { 2, 1 },
                { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 } });
    }

    private int input;
    private int expected;

    public FibonacciTest(int input, int expected) {
        this.input = input;
        this.expected = expected;
    }

    @Test
    public void test() {
        assertEquals(expected, Fibonacci.compute(input));
    }
}

使用要点如下:

  1. 类名用@RunWith(Parameterized.class)进行注解
  2. 需要一个用于准备参数的方法,可以返回列表,但是注意列表中的元素需要是数组,对应于构造方法中的参数顺序。相当于junit会自动根据参数调用构造方法,然后再执行测试用例。这个准备参数的方法需要用@Parameters进行注解。
  3. 可以通过设置@Parameters中的name属性自定义展示的测试名称。其中{index}表示当前参数在准备好的参数列表中的索引位置,{0},{1}…表示当前用例中的第x个参数值。以上面为例,第一个用例名称就是0::fib(0)=0

我在最近的工程中,就使用了这一特性。我准备了两个目录,一个用来配置请求报文,另外一个用来配置响应报文。 就是想通过测试用来来比较实际的响应报文和期望的响应报文是否规则匹配。

一开始我使用了在一个测试用例中做这个事情,虽然用例可以跑,但几十个报文是作为一个用例来运行的, 不能直观的展现出具体报文的处理情况。

后来我采用参数化的办法,在准备参数的时候,读取这两个目录生成对应的报文列表,再由测试用例根据请求报文进行业务处理, 得到实际响应报文后,与期望的响应报文进行规则匹配。整个架子的代码写完之后,只需要配置报文就可以增加测试用例了, 更重要的是,可以直观的展现出具体报文的处理情况。示例代码如下:

@RunWith(Parameterized.class)
public class IntegrationTest {
    public IntegrationTest(String name) {
        this.name = name;
    }

    @Parameterized.Parameters(name = "{index}: {0}")
    public static Collection<String[]> data() {
        List<String[]> testData = new ArrayList<String[]>();

        ClassLoader loader = IntegrationTest.class.getClassLoader();
        String reqpath = loader.getResource("req").getPath();
        String[] files = new File(reqpath).list(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.endsWith("_req.xml");
            }
        });

        for (String file : files) {
            int ldx = file.lastIndexOf("_req.xml");
            testData.add(new String[]{file.substring(0, ldx)});
        }
        return testData;
    }
    
    @Test
    public void clientTest() {
        //TODO with this.name
    }
}

数独随机生成探讨

思路

数独游戏应该很多人都玩过,规则也很简单。以前写过数独的程序解法,最近考虑了一下数独的随机生成。毕竟,每个数独一开始都是残缺的,它是怎么生成的?

如果通过正向生成的话,还需要判断剩余的空格是否能够满足数独的问题。所以,我认为应该通过反向来创建,意思是通过一个完整的数独,然后随机从上面抠取,抠得越多,表明完成的难度越大。

具体方案

问题就变成,如何生成一个完整的随机的数独?根据数独的特点,我认为可以通过一个已有的数独来生成其他数独。方案如下:

row1

  1. 首先看看上面的图案,对于一个完整的数独,1,2,3行调换次序,仍然可以保证变换后的数独是成立。 同理,456,789也是同样的道理。行是如此,列也是可以成立的,abc,def,ghi的内部调换都是可以成立的。 row2
  2. 还有上图所示的情况,123和456整三行一起调换也是可以的。同样,在列上面也是一样的道理。
  3. 还有其他一些思路,例如进行旋转操作,或者反过来的做法。

关于重复性

对于上述1,2两种方案,会不会出现重复的情况呢?我们进行下面的分析:

row3

  1. 假设A1是x,B1是y,C1是z,我们想找到某个变化可以达到同样的效果。
  2. 如果通过行变换的话,因为数独的限制,不可能有某一行在同样的列上有xyz存在,所以不能做到。
  3. 同理,如果是列变化的话,在第一行的位置也不可能再有xyz存在,所以也是不能做到的。

意思就是说,如果从某个变化开始,每做一次变换,都能得到一个新的数独,这样我们可以统计一下,一共可以有多少种变换方式。每三行中的行内的变化有6种排列,三行三行的也是有6中排列,计算上列的变换,就有 6X6X6X6X6X6X6X6=1679616种。

从流关闭说起

基本原则: 谁生产谁销毁

这个是用来解决责任权的问题,例如你的方法接收一个InputStream作为参数,通常就不应该在方法内去关闭它,而由客户端代码去处理。如果要关闭,通常应该在方法签名上明确说明,具体样例参考commons-io的IOUtils类。

还有另外一个例子,就是经常使用的Servlet的输入输出流,根据这个原则,也是不应该在代码中进行关闭的,这个工作是由Web容器负责的。

关闭的是什么?

java本身是带GC的,所以对象在消除引用之后,按正常是能够被回收的,那么为什么会有关闭操作?

这是为了回收系统资源,主要是端口(网络IO),文件句柄(输入输出)等,通常涉及的场景也是操作文件,网络操作、数据库应用等。对于类unix系统,所有东西都是抽象成文件的,所以可以通过lsof来观察。

为了更详细的说明这点,我们可以测试一下下面的代码:

public class Hello {
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 100; i++) {
            FileInputStream is = new FileInputStream("/tmp/data/data"+i);
            byte[] bs = new byte[512];
            int len;
            while ((len = is.read(bs)) != -1) ;
            Thread.sleep(1000L);
        }
        Thread.sleep(10000L);
    }
}

运行之后,通过losf或进程目录查看相关的文件句柄数量是不断增长的:

lsof -p 25945 |grep /tmp/data | wc -l
88

ls  /proc/25945/fd | wc -l
93

如果有关闭操作的话,就会发现打开文件数一直都处于很低的位置。如果持续出现未关闭的情况,积累到一定程度就可能超过系统限制,出现too many open files的错误。

如何确保关闭

关闭通常是调用close()方法来实现的,并且需要在finally来进行处理。另外,我们经常会遇到多个资源的关闭情况,因为IO库是采用修饰器模式的,所以基本原则是先关闭外层对象,再关闭内层对象,每个close调用都需要处理异常情况,例如:

InputStream is = null;
OutputStream os = null;
try{
   // ...
}
finally{
  if(is != null)
     try{
       is.close();
     }
     catch(IOException e){}

  if(os != null)
     try{
       os.close()
     }
     catch(IOException e){}
}

实践

  • 上面的关闭处理的确是比较繁琐的,可以考虑进行封装或者直接使用IOUtils.closeQuietly方法,节约不少代码行。
  • 自从JDK5之后,需要进行关闭处理的对象可以考虑实现java.io.Closeable接口。这个可以让资源关闭使用同一套代码。

JDK7改进及其他思路

在JDK7里,你只需要将资源定义在try()里,Java7就会在readLine抛异常时,自动关闭资源。 但是资源类必须实现java.lang.AutoCloseable接口,同时支持管理多个资源,释放次序与声明次序相反。

try (BufferedReader br = new BufferedReader(new FileReader(path)) {
    return br.readLine();
}

虽然感觉总是很繁琐,语法糖味道重,但比以前倒是进步不少了。 不过我们还是来看看Go中的做法,它提供了defer操作,用于在脱离方法作用域的时候自动调用,有点析构的味道。 看下面的示例:

func main() {
    files, err := os.Open("testqq.txt")        
    if err != nil {
        fmt.Printf("Error is:%s", "Game Over!")
        return
    }
    defer files.Close()
}

说说字符串拼接

String对象是无状态的

String的内部属性在初始化的时候就固定好了,也没有提供方法进行修改(反射等极端方法除外),并且类被定义成final,所以String对象都是是实实在在的无状态对象,是不可变的。

在通常的字符串拼接中,如果采用+运算符的话,通常会产生一个字符串对象,并把两个字符串的内部字符数组拷贝过去。 因此,在一个常见的频繁修改字符串的场景中,字符数组的拷贝开销是很大的,随之字符串的加长会越到后面越慢,例如下面的代码。

String sb = "";
for(String str : strs){
  sb += str;
}
return sb;

StringBuffer与StringBuilder

jdk早就考虑了这种场景,于是提供了StringBuffer,简单来说,就是一个可变的字符数组,开辟了一个字符数组缓冲区,增加(Append)时只是往缓冲区的空余地方写字符,当然也有可能缓冲区不够用,它的开销就集中在不够用的缓冲区扩展中(每次在现有基础上扩展一倍空间)。所以,最好能提前估计字符串的最终长度,减少扩展造成的消耗。不过,即便如此,通常也要把直接用String拼接的效率高许多,例如下面的代码。

StringBuffer sb = new StringBuffer();
for(String str : strs){
  sb.append(str);
}
return sb.toString();

到了jdk5,增加了StringBuilder,相对于StringBuffer来说,虽然它不是线程安全的,但在绝大多数场景下都是适用的,并且理论效率更佳(从oracle jdk的实现看,两个类除了是否同步这点,实现是一致的)。因此,习惯使用StringBuffer的童鞋,应该多关注一下StringBuilder。

字符串拼接的编译优化

再回到+操作符,本身java是没有运算符重载的,+只会对基本数学运算有效,而字符串,这么写只是语法糖而已,会变成StringBuilder操作(jdk5之前是StringBuffer)。例如下面的代码:

public String test(String a){
   return a + "b";
}

通过javap查看,可以看到是这样的(大意就是new一个StringBuilder对象然后用append进行连接);

public java.lang.String test(java.lang.String);
  Code:
   Stack=2, Locals=2, Args_size=2
   0:   new     #2; //class java/lang/StringBuilder
   3:   dup
   4:   invokespecial   #3; //Method java/lang/StringBuilder."<init>":()V
   7:   aload_1
   8:   invokevirtual   #4; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   11:  ldc     #5; //String b
   13:  invokevirtual   #4; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   16:  invokevirtual   #6; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   19:  areturn
  LineNumberTable:
   line 3: 0

因此,如果是像上面的情况,直接用+是合理的,对于其他情况,得考虑StringBuilder,同时要避免无意生成多余字符串的情况,例如append(“s” + a)的写法,编译器是不会自动优化的,写代码的时候应该换成append(“s”).append(a)。

更多关于字符串不变量的讨论,请见初探Java字符串