小南枝

爸爸的工作记录和小南枝的成长笔记

在当前的web应用中,memcached因为其搭建简单、功能强大、工作稳定、使用方便而被广泛使用于数据模型、查询列表乃至页面区块的缓存中, 对于上述的这些好处,在我们使用memcached的过程中,对其缓存的批量精确清理一直是一个麻烦的问题,下面描述一下这个问题并试着去描述一些解决方 案。

第一部分:问题描述

以一个简化的论坛系统为例(图一),这个系统包含帖子、讨论区、其中讨论区是帖子的集合,帖 子从属于某个讨论区,在这里,我们假设帖子和讨论区都以memcached作为缓存,帖子的缓存key为thread_id,讨论区的缓存key是 discussion_areaId。当帖子发生改变的时候,我们就调用memecache的清理方法,清理发生变化的帖子cache,然后根据帖子id 得到讨论区的id,清理讨论区cache。

多机环境清理Memcached方案探索 - woshizhaopingfei - 冷于冰的博客

 

 

(图一)

 

随 着业务的发展,有必要给论坛加一个管理后台,便于运营人员维护论坛系统,后台访问的数据库和前台一致,这个后台也有帖子对象,为了减少数据库的访问,帖子 对象和论坛前台帖子使用的是同一个缓存key,这个系统还有一个用于审核的帖子列表,待审核列表按照讨论区分类,展示的是讨论中部分帖子,它也需要缓存, 于是给它定义了另外一个缓存key,名为auditlist_areaId 。于是系统变成了图二的样子。

多机环境清理Memcached方案探索 - woshizhaopingfei - 冷于冰的博客

 

 

(图二)

 

当 两个系统并存的时候,无论是后台还是前台修改了帖子对象,我们都需要清理所有包含帖子对象的列表缓存,确保用户能看到最新的内容。而当前memcache 支持的缓存清理方式有两种,一种是全局清理,另一种则是按照具体的key来清理。前者既无必要对性能开销也大, 对于需要频繁清理的情况,更适合的是使用后面一种清理方式,那么,一个问题就摆在我们的面前,如何让前台和后台能知道对方的缓存key,并进行缓存清理?

在系统开发的初期,对于这种共享又有差异的缓存使用情况,两个系统可以通过硬编码的方式来解决,在两个系统中,如果要清理缓存,除了清理 本系统的缓存,另外一个系统的缓存也"顺带"清理一下……设想一下,这时候又来了一个需求,要求在后台展示每个讨论区中加精的帖子,也要缓存,我们修改一 个帖子,就有可能需要修改三个不同的list缓存,而这三个list的key存在不同的系统中,更远的,如果还有第三个共用数据库的系统加入呢?

硬编码的方式让开发者疲于奔命了……

 

第二部分:方案选择

  1. 消息通信

    当前台系统修改帖子对象之后,前台系统清理自己的缓存,然后调用有后台管理系统的清理缓存接口。同样,后台系统修改帖子对象,清理系统之后,就调用前台系统的清理缓存接口。

    这 个方案的好处是调用方不再需要知道被调用者具体的key,约定一个清理缓存的接口显然比约定无数个缓存key好办的多;不好的地方在于,这样两方乃至多方 的系统就因此产生了错综复杂的依赖关系。如果这类型的缓存不止一个,那清理缓存接口的代码会较为复杂,调用清理缓存的入口会到处都是。

  2. 订阅消息

    在 上一种方案的基础上,我们继续考虑,一种能够解耦的方式就呼之欲出,那就是使用一个中心节点(可以考虑基于zookeeper的配置中心),当一个系统发 生清理缓存的时候,就向中心节点发送一个消息,而需要同步列表的其它系统,因为在中心节点订阅了这个消息,就能够第一时间收到更新消息,从而执行自己的缓 存清理逻辑。

    这个方案是上一个方案的进化版,系统的接口相互依赖消除了,连key的依赖也不存在了;但是增加了一个中心节点,增大了系统的维护成本,也必须承担中心节点崩溃带来的风险。

  3. 经验式的前缀清理

    多 系统通过拷贝来共享一份缓存key已经讨论过,是一件费力不讨好的事情,但是我们还有得选择,就是对于要关联清除的缓存,我们可以在多系统中约定一种前 缀,比如,帖子的cache为thread_id,那么对应的帖子列表就可以是thread_list_areaId_1、 thread_list_areaId_2、thread_list_areaId_3,清理完帖子缓存之后,就取出帖子中的讨论区id,然后组合成列表 1、列表2、列表3的缓存key,本着宁可清到没有缓存的区域,也不能少清理的原则,列表最后的n设置为一个适中的值。

    这种方案首先也不能完全避免共享常量,只是从共享key变成共享前缀了,使用的范围也比较狭窄,不过好处是避免了系统间的调用依赖,同时也不用再维护一个配置中心。

  4. 构建底层系统的方案

    这 个方案是把所有的数据操作,都放到一个底层系统中,即当前台和后台要取列表的时候,都只能向这个新的底层系统发送请求,底层系统自己来维护所有的缓存关 系。这个方案把多系统的缓存压缩到了一个系统里面,以服务的方式提供数据服务,对其它系统隐藏了数据库的实现,清晰了系统的结构。不好的地方是增加了一层 调用底层的网络开销,另外前台要对数据查询、修改的任何操作都需要走接口实现,很多时候是要修改和重新发布底层系统才能如愿的,这样,底层系统的稳定性和 频繁修改会成为一对矛盾。

  5. 数据库为中心的清理方案

    多系统共享数据库,那数据库就是一个天然的 中心节点,能否复用这个必须的中心节点呢?答案是肯定的,我们可以把细粒度的key(帖子key)、与其关联的key(列表)都存储在数据库中,然后按照 数据库中的关联key来逐一清除缓存,就可以在变动的时候,"替"别的系统完成缓存清理。以我们上面的帖子例子来说:

    (1)建立如下的一张表

    Id

    cacheKey

    keyPattern

    20

    thread_id

    discussion_{areaId}

    21

    thread_id

    auditlist_{areaId}

    (2)修改id为50000的帖子,修改其缓存,被清理的单个缓存id是thread_50000

    (3) 从数据库中取出thread_id对应的两种模式,这两种模式中,{}大括号包裹的部分就是缓存的可变值,约定为Thread对象中存储的某个字段,可以 通过对Thread对象反射得到,一个是discussion_{areaId},auditlist_{areaId},这样,我们就可以通过反射手中 已经得到的Thread对象,得到areaId(比如说是1000),这时候,两个key最终执行的时候就变为 discussion_1000,auditlist_1000。

    这个表本身也可以成为后台管理的一部分,当开发人员需要加入新的缓存的 时候,就可以将缓存的key模式和key对应起来存入数据库,所有共用这个模型的系统,都可以共用这个缓存清理模型,更进一步,这个功能可以打成jar 包,供系统做透明的调用,系统的责任,就只要维护好key映射就可以了。

 

第三部分 总结

没有最好的方案,只有合适的方案,在这几种选项中,第2和第5是我比较中意的方案,相对而言能有效的解决系统耦合的问题,网上也有根据key的存储分布,最终来清理某一个节点的方案,亦有可观之处,上述都是从应用层面上做的考虑,如果读者能有更好的方案,欢迎留言讨论。

 

这两天趁着五一假期看一下新买的《java加密与解密的艺术》,顺手做几个小例子,下面是des加密解密的实现。
des加密是典型的对称加密,即加密的key和解密的key是同一个,下面的程序搞了一个字符串,加密后再使用同一个key做了解密,顺利得到了原文。

package com.dayfly.test;

import java.security.SecureRandom;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

/**
 *
 * @author zhaopoingfei
 *
 *         2012-4-29
 */
public class DesEncryption {

    /**
     * @param args
     */
    public static void main(String[] args) throws Exception {
        String plainText = "中国人";

        // 1、创建key的过程
        KeyGenerator ge = KeyGenerator.getInstance("DES");
        // a、使用随机算法初始化key
        ge.init(SecureRandom.getInstance("SHA1PRNG"));
        // b、生成
        SecretKey key = ge.generateKey();

        // 2、开始加密,加密的明文是使用GBK来编码
        Cipher cipher = Cipher.getInstance("DES");
        cipher.init(Cipher.ENCRYPT_MODE, key);
        byte[] cipherText = cipher.doFinal(plainText.getBytes("GBK"));

        // 3、创建解密的对象,除了mode之外,和加密对象一样的用法,解密后使用GBK来解码得到原文
        Cipher revertCipher = Cipher.getInstance("DES");
        revertCipher.init(Cipher.DECRYPT_MODE, key);
        byte[] originalByte = revertCipher.doFinal(cipherText);
        String result = new String(originalByte, "GBK");
        System.out.println(result);
    }
}

最近不时和南南妈有点小摩擦,后来想了下都是些鸡毛蒜皮的小事,争论的大部分时间,我们则都在指责“你先说我的”,虽然这一点也不重要。

静心思考,我们的所作所为,很大程度上决定了别人是如何看待和对待我们的。我们愤怒的面对别人,怎么指望别人能欣然、愉快的来面对你呢。这个世界就是一面镜子,你充满愤怒的面孔会反射到你的眼里,你欣喜的面孔也会回到你的心里,如果期望让别人对自己微笑,那么就先学会对别人微笑吧。

也许这是小南南长大后会反复思考的一个问题。

2012年4月25日,奶奶旅行来到了杭州,小南南可高兴了,展示了一些连我们也没有见过的才艺。因为这次是随旅游团行动的,所以不能留在我们这里,次日,小南和爸爸妈妈一起,送走了奶奶。

因为要从google reader里取一些数据,所以研究了一下Google的Oauth接口,Google使用的是Oauth2.0规范,它提供了一个在线的测试场https://code.google.com/oauthplayground,可以用这个来做测试,下面说一下请求的顺序。

  • 第一步,进入授权页面:https://accounts.google.com/o/oauth2/auth?scope=http://www.google.com/reader/subscriptions/export&response_type=code&access_type=offline&redirect_uri=https://code.google.com/oauthplayground/&approval_prompt=force&client_id=xxxxx.apps.googleusercontent.com&hl=zh-CN&from_login=1&as=-40afd423cf3b6bab&pli=1

 

  • 第二步,授权服务回调,返回一个authCode

https://code.google.com/oauthplayground/?code=4/_pOVPjWp-5JH1_gHh4b7sSW88xHk

 

  • 第三步,根据authCode获取访问的token和refreshToken,

要使用post请求

POST /o/oauth2/token HTTP/1.1
Host: accounts.google.com
content-type: application/x-www-form-urlencoded
user-agent: google-oauth-playground

code=4%2F_pOVPjWp-5JH1_gHh4b7sSW88xHk&redirect_uri=https%3A%2F%2Fcode.google.com%2Foauthplayground%2F&client_id=407408718192.apps.googleusercontent.com&scope=&client_secret=************&grant_type=authorization_code

 

  • 第四步,以get请求

http://www.google.com/reader/subscriptions/export?access_token=xxx来调用api,或者把access_token放到请求头里面去
Authorization: OAuth xxx
 

  • tip1 如果请求返回是401,Unauthorized

则很有可能是accessToken过期了,此时需要用refreshToken去请求新的accessToken。

使用下面的请求
POST /o/oauth2/token HTTP/1.1
Host: accounts.google.com
content-type: application/x-www-form-urlencoded
user-agent: google-oauth-playground

client_secret=************&grant_type=refresh_token&refresh_token=xxxxx&client_id=xxxxxx.apps.googleusercontent.com

 

  • tip2 google reader并不在公开的开放api里面

不过scope可以使用http://www.google.com/url?sa=D&q=http://www.google.com/reader/subscriptions/export&usg=AFQjCNEi1XZC9txh3s8cazlWRzTvC-onjw

实际调用的url也能使用同一个地址

 

  • tip3 关于oauth及openid的一些理论知识,可以翻墙的同学可以看下这个视频。

http://www.youtube.com/watch?v=SLsfzTNIykY

 

 

cator的官网介绍如下#

Castor is an Open Source data binding framework for Java. It's the shortest path between Java objects, XML documents and relational tables. Castor provides Java-to-XML binding, Java-to-SQL persistence, and more.

从它自己的介绍可知,它的主要作用就是用来执行java文件和xml的相互转化的。

具体介绍#

1、默认配置的一个小例子#

public class CastorTest {

    /**
     * @param args
     */
    public static void main(String[] args) throws Exception{
        Person p = new Person();
        p.setName("张三");
        p.setAddress("浙江省杭州市");
        p.setAge(10);

        StringWriter sw = new StringWriter();
        Marshaller marshaller = new Marshaller();
        marshaller.setWriter(sw);
        marshaller.marshal(p);
        System.out.println(sw.toString());
    }
}
public class Person {
        String name;

        String address;

        int age;

        public String getAddress() {
            return address;
        }

        public void setAddress(String address) {
            this.address = address;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }
}

以上是一个简单的main函数,对一个简单对象执行转换。我们可以得到这样的结果

<?xml version="1.0" encoding="UTF-8"?>
<person age="10">
        <address>浙江省杭州市</address>
        <name>张三</name>
</person>

在这个例子中,castor把类的所有字段都找了出来,基本数据类型作为元素的属性,而对象类型则成为元素的子元素,使用非常方便。这是默认情况,但是在 大部分时候,我们需要的是更精确的控制,比如,每个元素的名称可能和代码里的属性不一样,有些元素需要隐藏,有些需要显示更复杂的对象,如java的容器 类型,这时候,我们就需要castor的第二套方案上场了。

2、map方式的xml生成介绍#

上面说到,复杂要求的xml映射的时候,靠castor来猜测开发者的意图是不靠谱的。这时候,castor提供了一套好的沟通机制,这就是map文件, 你可以把你需要的格式按照castor的要求定义好,然后castor在转换这个java文件的时候,就会按照你的配置来执行转换。我们在上面的例子的基 础上小作修改,完成这个任务。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapping PUBLIC "-//EXOLAB/Castor Mapping DTD Version 1.0//EN" "http://castor.org/mapping.dtd">
<mapping>
        <class name="com.netease.backend.pris.web.outputMeta.Person" auto-complete="true">
                <map-to xml="teacher"/>
        </class>
</mapping>

增加一个xml配置文件,文件名随意,这里我们叫它mapSample.xml,文件中定义了我们要渲染的类的名字,属性auto-complete为 true的时候,就是要求castor渲染java类的时候,对于没有特殊配置的成员,做自动渲染;map-to 中的属性xml则是告诉castor,这个类渲染的时候,对应类的元素,名称为teacher。 代码的修改为

public class CastorTest {

    /**
     * @param args
     */
    public static void main(String[] args) throws Exception {
        Person p = new Person();
        p.setName("张三");
        p.setAddress("浙江省杭州市");
        p.setAge(10);

        StringWriter sw = new StringWriter();
        Mapping map = new Mapping();
        map.loadMapping("./target/test-classes/mapSample.xml");
        Marshaller marshaller = new Marshaller();
        marshaller.setMapping(map);
        marshaller.setWriter(sw);
        marshaller.marshal(p);
        System.out.println(sw.toString());
    }

}

我们来看看渲染结果。

<?xml version="1.0" encoding="UTF-8"?>
<teacher age="10">
        <address>浙江省杭州市</address>
        <name>张三</name>
</teacher>

这样,对应Person类的根元素就被替换为teacher了,还是比较简单的吧,更详细的map配置,可以参考官网的map配置手册,这里就是抛砖引玉说一下了。

3、在spring中的应用#

鉴于castor的优异表现,spring的渲染层也对castor做了对接,如果需要引入castor,可以如下配置

        <bean
                class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
                <property name="defaultContentType" value="application/json" />
                <property name="mediaTypes">
                        <map>
                                <entry key="html" value="application/x-www-form-urlencoded" />
                                <entry key="json" value="application/json" />
                                <entry key="xml" value="application/xml" />
                        </map>
                </property>
                <property name="viewResolvers">
                        <list>
                                <bean
                                        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
                                        <property name="prefix" value="/WEB-INF/views/" />
                                        <property name="suffix" value=".jsp" />
                                </bean>
                        </list>
                </property>
                <property name="defaultViews">
                        <list>
                                <bean
                                        class="org.springframework.web.servlet.view.json.MappingJacksonJsonView" />
                                <bean class="com.netease.backend.pris.util.OverrideMarshallingView">
                                        <property name="marshaller">
                                                <bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller">
                                                        <property name="mappingLocation" value="classpath:map.xml" />
                                                        <property name="suppressXsiType" value="true"></property>
                                                </bean>
                                        </property>
                                        <property name="contentType" value="application/xml" />
                                </bean>
                        </list>
                </property>
        </bean>

关于cator的配置就在MarshallingView的配置中,spring提供了一个cator为基础的渲染器,可以把Model中的对象转换为xml,依照的规则,就是mappingLocation属性中指定的map文件。

4、有哪些好处#

把结果转为xml,dom4j和其它一些java技术也能做到,不过castor的好处是开发速度快(熟悉了它的特性的基础上),而且这样业务层和渲染层 就不会纠结在一起了,以model设置为届,一边是业务层,一边是展现层,如果要测试业务层的话,只要检测model就可以了;要测试渲染,则只要具备 map和对象足矣,无需运行起整个spring容器,对于前期的调试也很方便。对于阅读协议这种xml接口的东西来说,还是很实用的。

今天在review同事代码的时候,发现在一个filter里面,因为无法通过正常的注入得到spring的bean,他使用了一个threadlocal变量:如果需要的bean没有被初始化,则初始化一个spring环境,从中取得需要的bean,保存到threadLocal里面,下次就从threadLocal里面取,如果没有的话,初始化bean……

这和惯常的threadlocal用法明显有异,如果有两个线程,那么我们可能便可能会初始化两个互不相关的spring容器,除了性能问题,可能还会有功能上的问题。(如果使用到有状态的bean的话)。于是做了一个演示实验,启动好容器,在这个filter的地方打上调试断点,开始请求模拟。

1、一个请求过来,如愿进入了断点,进行比较耗时的容器初始化操作。

2、完成第一个请求之后,再发一个请求,没有进入断点。

3、同时并发50个请求,系统又有了几次进入断点,进行初始化。

4、完成50个请求之后,再次并发100个,这次没有进入断点。

最后得出的结论如下:

1、web容器的线程是重用的,使用完毕之后,会分配给后面的请求,所以一个个来的时候不会有问题。

2、web容器的线程数是有限制的,这个决定了上面的初始化次数,一个新的线程加入的时候,threadlocal中没有线程中的值,此时就会触发初始化,但是线程数量达到web容器容许的最大值,这时候,更多的请求过来后,只会被系统hold住,进入等待,而不是继续开线程服务,所以后面请求再多也不会初始化了。

最后,把代码改成了以下的方式,在init方法里面加上如下代码

ServletContext servletContext = config.getServletContext();
WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);

就可以正确得到容器并做一些操作了,这样就不用初始化很多个spring容器。

还有一周才能回家,咋现在就有点近乡情怯的感觉了。每次回去都有无数的惆怅:旧日景观的消失,儿时玩伴的失散,或是就是找到几个从前的好友,却基本上没什么可聊的话题了,亲人们则不知不觉都在老去,错过了子侄们无数个可爱的成长的日子……

人生不如意事十之八九,大概说的就是这个吧。年轻的时候,生怕别人不知道自己有一腔假假的愁绪,于是做孤傲状、做冷漠状、做颠倒的文字,慢慢长大起来,领略了一些艰难,反而倒是想把愁绪藏起来了,老家亲友如相问,也要强作坚强,只说甘甜而已,生怕hold不住的时候,便也就真的把难过传染给别人了,这生活的矛与盾,想起来真是让人啼笑皆非啊,果然和辛弃疾的词一样,前半阙是“为赋新词强说愁”,后半阙却是“却道天凉好个秋”。
这几天一直为节后的票发愁,贵得离谱,忽然听到一首挺土的歌:有钱没钱,回家过年。一时间心里的什么软软的东西仿佛被触动了,去他娘的衣锦还乡,去他娘的啥,就算我啥也没做到,但是我也要挣扎着回家去,家里的人不会计较我实际上贫穷还是富有,是成功了还是失败了,他们只会安静的接纳我,过年了,没别的,就是收拾好行囊,把自己和老婆孩子,完完整整的带回去,好好的和家人热闹一下,给爸爸和外婆好好的磕个头,如此足矣。

在班车上又看完了一本书,不记得是这样看完的第几本书了,这本书的书名叫《孤独六讲》,作者是台湾的蒋勋。

书中描述的是社会中存在的六种孤独,情欲孤独、语言孤独、革命孤独、暴力孤独、思维孤独、伦理孤独。因为车上无法做笔记,有些概念也没有搞清楚是属于哪个范畴的,不过暴力孤独和伦理孤独的阐述让我很有共鸣。

暴力,在原始社会,或者更原始的时候表达是一种强大的象征,但是在如今则是被视为危险和不好的,但是这本能无法完全消除,于是,在社会道德的包裹下,暴力以各种遮掩的形式出现了,社会在杂技团,在立法委,在滑冰场,无处不展示着这种被扭曲的暴力,颇有点弗洛伊德关于性的论述的调调,也许不久的将来,人们能更理性的看待暴力美学,挖掘这里面属于生命力的美好东西。

伦理孤独,则是阐述在传统伦理的束缚下,个人自由所遭遇的困扰,尤其是自诩孔教后人的中国人,更是首当其中,多少伤害,恰恰都是以爱的名义,在伦理的框架下,子女都是父母的私有之物一样,缺乏独立,无法得到合适的尊重,这些我深有体会,前不久有个朋友,美好的姻缘便在她的父亲粗暴无礼的干涉下告吹了,这种堂而皇之做下的恶事,我们却还以“天下无不是的父母”来做注脚,恰恰是纵容了这种可怕的伦理孤独。作者也并非就反对伦理,事实上作者对此有一段最精妙的阐述,大意是说,并非完全赞同儿子告发父亲犯罪的这种行为,也并非完全同意孔子说的“父为子隐,子为父隐”,作者提倡,应该反对的是结论式的遵循某个规定,任何论断之前,都必须有思辨的空间,结论需要自己来得出,一个没有伦理亲情的社会,是何等的惨厉;而一个忽略法制的社会,不幸的人就永无出头之日了,所以重要的不是结论,这也不是能轻率给出结论的命题,关键是一个有积极思辨的社会,才有希望做出正确的判断。作者说出了多少人提都不敢提的概念,这可怕的伦理孤独,一旦有了名字,这种恐怖便不是不可战胜的了,期望能读懂这些的读者,都能在以爱干涉自己亲人的时候,反观一下自身,自己是不是一个变相的施暴者呢?如此,则真正的文明有望矣!