Monthly Archives: May 2014

使用时注意proxy-target-class选项

如果使用的是默认的<aop:aspectj-autoproxy/>,当target有interface时,生成proxy会实现这个interface,但这个proxy并不是target的子类。

有时候,你的target有interface, 但仍然有自己的public方法需要暴露,这时你应该保证你的aop proxy会继承target.

办法就是:

引用

<aop:aspectj-autoproxy
proxy-target-class="true"/>

分库设计的几点准则

分库可以将数据库访问的压力分担到多个子库中,提升数据库读写的性能;但数据库一旦分库,上层的读写操作就必须考虑分库这一事实。比如说,要查询某条记录,查询条件除了业务上的条件,还应该把带上一个分库的键值对,好让你的分库框架找到这条记录所在的库。

我根据自己的项目经验稍微归纳一下。

核心准则:尽量让所有的访问都只落在一个分库里。如果要访问多个分库才能完成操作,那么访问性能会由于多次网络I/O而下降,在事务方面也很不方便。

推论

1.
所有的SQL都应该带上分库的键值对,以帮助系统路由到记录所在的分库。比如,你的论坛系统中的帖子是按版面ID分库的,那么在查询某个帖子的详情时也应该把这个贴子所在版面的ID放到SQL里。

2.
被分库表的子表如果需要分库的话,应该按同一维度分库,以保证某主记录的相关子记录跟它总是落在同一个分库中,以方便表连接和事务。 如果帖子是按版面ID分库的,那么一个版面下的所有帖子“赞”记录也都应该跟这个版面下的所有帖子放在同一个分库中; 否则你很难实现“找出某版面下前十个帖子及每个帖子前十个赞贴的人”和“在同一个事务中新增一条评论记录并且将帖子的评论数加1”

3.
存在表连接和事务的其他场合,也都应该尽量保证相关记录出现在同一个分库

4.
分库数最好不要特别多,以节省万一要全库扫描时产生的成本。在某些情况下,你的查询可能就是要扫描所有分库,一次分库查询就是一次网络I/O,但如果查询次数不多,这样的性能往往也可以接受。所以如无必要,不要设置过高的分库数;可以通过“清理三个月前数据”等机制以控制全库的总记录数,这样一来所需的分库数也不太多。

代码片断:获取一个类集中所有元素的某个属性

怎么从 List<Person> personList中取出 List<Long> idList ?  没有闭包语法的情况下,你只能写一个for循环,一个一个往里塞。。。

老写这种东西,会不胜期烦。 可以写个微型的框架,让你写更少更简洁的代码完成这种事情。

你会这样用:

List<Long> idList = MyCollectionUtils.extractProperty(
				personList, GetPersonIdPropCommand());
		public static final class GetPersonIdPropCommand implements
				GetPropertyCommand<Person, Long> {
				......
		}

框架在这里:


	/**
	 * 从一个类集中抽取中每个元素的某个属性. 举例说明:{person1, person2} => {person1Id, person2Id}
	 * 
	 * 
	 * @param objects
	 * @param getPropCommand
	 * @return
	 */
	public static <O, P> ArrayList<P> extractProperty(Collection<O> objects,
			GetPropertyCommand<O, P> getPropCommand) {
		ArrayList<P> resultList = new ArrayList<P>();
		for (O object : objects) {
			if(object == null){
				continue;
			}
			P property = getPropCommand.getProperty(object);
			resultList.add(property);
		}
		return resultList;
	}

	public interface GetPropertyCommand<O, P> {
		P getProperty(O object);
	}

你可能还需要这样两个操作:

	/**
	 * 把一个类集变成map, key是元素的某个属性 <br/>
	 * 举例说明:{person1, person2} => {<person1Id, person1>, <person2Id, person2>} <br/>
	 * 如果某个对象为空或相应的属性为空,则该对象不会出现在返回结果中 <br/>
	 
	 * @param objects
	 * @param getPropCommand
	 * @return linked hash map, 以保证顺序与原来相同. 
	 */
	public static <O, P> LinkedHashMap<P, O> collectionToPropMap(
			Collection<O> objects, GetPropertyCommand<O, P> getPropCommand) {
		LinkedHashMap<P, O> resultMap = new LinkedHashMap<P, O>();
		for (O object : objects) {
			if(object == null){
				continue;
			}
			P property = getPropCommand.getProperty(object);
			if(property == null){
				continue;
			}
			resultMap.put(property, object);
		}
		return resultMap;
	}
	
	
	
	/**
	 * 把一个类集变成map, key是元素的某个属性<br/>
	 * 举例说明:{person1, person2, person3} => {<male, [person1]>, <female, [person2, person3]>} <br/>
	 * 如果某个对象为空或相应的属性为空,则该对象不会出现在返回结果中 <br/>
	 * 注意:这个方法不是线程安全的 <br/>
	 
	 * @param objects
	 * @param getPropCommand
	 * @return linked hash map, 以保证顺序与原来相同
	 */
	public static <O, P> LinkedHashMap<P, List<O>> collectionToPropListMap(
			Collection<O> objects, GetPropertyCommand<O, P> getPropCommand) {
		LinkedHashMap<P, List<O>> resultMap = new LinkedHashMap<P, List<O>>();
		for (O object : objects) {
			if(object == null){
				continue;
			}
			P property = getPropCommand.getProperty(object);
			if(property == null){
				continue;
			}
			
			List<O> thisPropList = resultMap.get(property);
			if(thisPropList == null){
				thisPropList = new ArrayList<O>();
				resultMap.put(property, thisPropList);
			}
			thisPropList.add(object);
		}
		return resultMap;
	}

统计值在持久化前做一下校验:不能在值域之外

由于并发、程序bug或其他原因,你的统计值或多或少可能不那么准确。

如果说不准确可以勉强接受,超住值域之外就属于丢脸了。

如果你的页面上显示一个人发过的帖子数为-1 , 那就贻笑大方了。如果是0。即使不精确,也还说的过去。

所以,在持久化任何统计值之前,先把这个值正规化为值域的边界值(比如,小于0则置成0)。

影响业务逻辑的标签、徽章应该由开发人员创建

社区系统里的各种标签、徽章应该由谁创建,由谁来贴标签、颁发徽章?

贴标签、颁发徽章当然要由运营操作人员来做。

谁来创建呢?如果这个标签仅仅用来显示,而不会扭曲任何if/else逻辑,那可以由操作人员在后台界面上完成。

否则,就应该由开发人员创建,因为代码里要用它。如果还没创建好,怎么用它?

一般情况下,可以仅仅作为枚举类写死在代码里,连数据库表都不用建。

数据库常见冗余设计

冗余本应计算出来的数值

统计数值可以通过select count(*)临时计算出来,但这种计算可能比较耗时,所以可以直接在表中增加这样一个字段并在相应事件发生时更新。 比如一个贴子的赞数、评论数都可以直接作为帖子表的字段,新增赞、评论时再累进这些字段。在查询一批贴子时,可以不用任何附加SQL就可以拿到这批帖子的赞数和评论数。

还有个例子:“前十个赞本贴的人”,也可以拼成单条字符串放在帖子表中。

字段冗余
A表除了外键关联B表的主键,还冗余了B表的其他一些字段。

1. 这种策略一般用于避免表连接: 在很多场景下,只需要查询A表即可。

2. 约束:业务上规定被冗余的字段不会发生改变,否则,修改B表后还要把A表也改一下,代码中很容易遗漏这一点,这样做还会带来性能损失。

3. 不冗余,不代表一定要表连接。要冗余还是不冗余,可以参考这里

整条记录冗余
A表和B表没有外键关系,而是“复制”关系。它们字段基本上一样,或者说一个是另一个的子集。

分库分表环境下常常需要这种策略,以便对单张表按多个维度进行拆分。 SNS中有个典型的例子: 关注关系。  我们既要查看一个人的偶像列表,又要查看一个人的粉丝列表。所以,当数据量大时,你既要按粉丝ID分库,又要按偶像ID分库。一个关系有两个分库维度,那就只好做两张表了。

最好用一些专门的数据复制中间件完成这种事情。这类复制逻辑非常简单,中间件一般能轻松支持; 使用中间件,也可以使这种冗余关系对上层透明,业务逻辑编写者不需要自己实现数据复制。

通过异步机制解耦主干流程和附加流程

在web2.0系统中,用户的一次写操作,可能不仅仅意味着系统里要新增一条相应的记录,系统里可能还有很多统计性的事情要做。

比如在社区系统中,一个用户发了一个帖子,系统除了生成这个帖子的记录之外,还要给用户的发贴数加1,用户使用过的tag数加1,每个tag对应的帖子数加1 。。。

可以把主流程(生成帖子记录)和附加流程(各种加1)整合在一个事务里执行,程序健壮且易理解。但也可以在完成主流程后,发一条消息出去,让消息的消费者完成这些统计操作。

这种
异步机制最明显的好处,就像很多人说的那样,可以
尽快响应用户请求,提升性能

但更重要的好处是,
它实现了解耦,有利于各模块独立发展,并促进分工

1.
把附加流程实现在主流程的模块里,不符合主模块的职责定位; 主模块可能要因此依赖很多附加模块的库,不利于系统的简洁,对发布也有影响。

2.
把附加流程实现在主流程的模块里,不利于主模块和附加模块的开发人员各司其责,代码也容易遗漏或出错

由于异步消息的不可靠性,这样做也有它的副作用

1. 消息可能会丢失,导致附加流程未能执行。 要解决这个问题,只有使用不丢消息的消息中间件:这个中间件只有在收到消费者的回执后才会删除消息。

2. 消息可能重复,导致附加流程执行两次。如果你的附加流程不是统计性质,则让消费者做好幂等性就好了; 如果是统计类操作的话,那就要另建去重措施了。如果对统计数值的准确性要求不高,可以用bloom filter简单搞定; 否则的话,需要在缓存里保存一段时间内的消息ID,收到消息后做下比对再决定要不要消费它。

“手动干预排序”的设计与实现

你的记录一般会按某个业务字段排序,比如ID,生成时间等等。

在论坛或SNS系统中,你可能要允许手工调整记录间的排序,比如“置顶一个帖子”,“下沉某个帖子让人感觉它是一个月前发表的”等等。

在设计社区系统的数据库时,你首先要有意识地考虑下某个记录列表是否需要手工排序。如果需要,你应该使用一个专门的字段来解决这个问题。

=============================

下面就说下这个字段(假设它叫 sort_order)相关的设计与实现:

1. 考虑到比较式分页的需求,这个字段的值要像ID一样不能重复。你可以用数据库的序列生成器生成这个字段,也可以像下面将要说的那样,根据一定的编码规则生成这个字段。

2. 如果你设计的是社区系统,那么你的记录的原始排序规则很有可能是“发表时间”;这时“手动干预排序”可能相当于“让人感觉某个帖子是另一个时间点发表的”。 因此,sort_order最好体现时间信息。我的做法是:sort_order = 记录创建时间的毫秒数 串接  3位随机数。 这样基本可以保证唯一,而且携带时间信息。你会问为什么不直接用纳秒数?因为系统生成纳秒数需要1毫秒以上的时延,对性能伤害很大。