Elgg分布式扩展和性能优化(二)

继续来谈关于Elgg的种种,首先从LAMP的MySQL开始

4. MySQL InnoDB vs MyISAM

传统的LAMP应用默认都使用的是MySQL的MyISAM引擎,关于InnoDB引擎和MyISAM引擎的比较,网上可以搜到的资料很多,这里就不详细展开了,简单来说InnoDB的事务、行级锁定等特性在高并发的情况下对数据库性能有相当的提升。实际上,MySQL在5.5版本之后已经将默认引擎更换为InnoDB了。因此把Elgg的数据库表转换为InnoDB类型是个简单有效的优化手段。

对Elgg而言,大部分的表都可以直接转换为InnoDB类型,不过由于InnoDB不支持MyISAM的全文搜索功能,因为有几个加了全文搜索的表暂时不能转换,仍然保留MyISAM类型。如果想进一步完全转换到InnoDB,就要考虑去掉利用数据库的全文搜索功能,使用自己实现的第三方全文搜索来替代(如Lucene)。

总结一下,Elgg数据库里可以转换为InnoDB类型的表包括(表前缀elgg_)

elgg_access_collection_membership
elgg_access_collections
elgg_annotations
elgg_api_users
elgg_config
elgg_datalists
elgg_entities
elgg_entity_relationships
elgg_entity_subtypes
elgg_metadata
elgg_metastrings
elgg_private_settings
elgg_river
elgg_users_sessions

保留MyISAM类型的表包括

elgg_groups_entity
elgg_objects_entity
elgg_sites_entity
elgg_system_log
elgg_users_entity

还剩下两个是Memory类型的,不需要改变。

5.  Elgg Metadata non thread-safe bug

Elgg的Metadata在实现上有一个很严重的bug,这个bug会导致数据库数据异常,而且这个bug到目前为止官方都没有修复,和数据库也有一定的关系,所以在这里先写出来。

Elgg的metadata提供了一种灵活的给对象附加属性的方式,比如  $blog->author='Daniel' 这样一句话就可以对一个blog对象赋予一个名字为author、值为Daniel的metadata,而在数据库里不用事先申明这个author字段(是不是有点NoSQL的感觉)。并且,Elgg说明可以为一个metadata赋予string或者array类型的值,问题就出在这里。

在实现上,->实际上是PHP5的magic methods,最终会调用ElggEntity类里的setMetaData这个方法

	public function setMetaData($name, $value, $value_type = null, $multiple = false) {

		// normalize value to an array that we will loop over
		// remove indexes if value already an array.
		if (is_array($value)) {
			$value = array_values($value);
		} else {
			$value = array($value);
		}

		// saved entity. persist md to db.
		if ($this->guid) {
			// if overwriting, delete first.
			if (!$multiple) {
                $options = array(
					'guid' => $this->getGUID(),
					'metadata_name' => $name,
					'limit' => 0
				);
				// @todo in 1.9 make this return false if can't add metadata
				// http://trac.elgg.org/ticket/4520
				// 
				// need to remove access restrictions right now to delete
				// because this is the expected behavior
				$ia = elgg_set_ignore_access(true);
				if (false === elgg_delete_metadata($options)) {
					return false;
				}
				elgg_set_ignore_access($ia);
			}

			// add new md
			$result = true;
			foreach ($value as $value_tmp) {
				// at this point $value should be appended because it was cleared above if needed.
				$md_id = create_metadata($this->getGUID(), $name, $value_tmp, $value_type,
						$this->getOwnerGUID(), $this->getAccessId(), $multiple);
				if (!$md_id) {
					return false;
				}
			}

			return $result;
		}

		// unsaved entity. store in temp array
		// returning single entries instead of an array of 1 element is decided in
		// getMetaData(), just like pulling from the db.
		else {
			// if overwrite, delete first
			if (!$multiple || !isset($this->temp_metadata[$name])) {
				$this->temp_metadata[$name] = array();
			}

			// add new md
			$this->temp_metadata[$name] = array_merge($this->temp_metadata[$name], $value);
			return true;
		}
	}

bug出现在:当有多个进程同时并发的写同一个metadata值时,比如有100个进程同时写$blog->author=’Daniel’ ,在数据库中就会重复出现相同的多条记录,如果并发持续的写的话,重复记录会迅速增长,严重的情况下,取该条metadata值会耗尽php可用内存,直接导致网站崩溃。

这是一个明显的race condition问题,其原因就是在上面的那段代码里,每当为一个metadata赋值时,会首先检查是否存在该metadata,如果存在,那么就先删除该条metadata的所有记录,然后再创建新值。在并发写的情况下,可能某个进程已经删除了该metadata,另一个进程在查询时会发现没有该记录,于是直接创建一条新记录,而此时前一个进程又会继续执行创建新记录,这样就导致了数据的重复。

实际上,“先删除再创建”这种机制最好的解决方法就是 – 使用事务(还记得之前我们为什么要把数据库换成InnoDB么),把上面的过程用事务-回滚的机制改写,就能彻底解决这个问题。

这个问题出现的根本原因,实际上是由于Elgg没有限制metadata为一个值或多个值,导致无法事先确定该metadata会有几条记录存在,因此只能用“先删除后创建”的笨办法保证逻辑的正确。所以这是一个设计上的问题,而Elgg的开发者现在也没有想出什么好的办法来解决,除非使用事务,这个bug也一直存在于Elgg的代码中:(

如果不想使用事务这种相对复杂的逻辑来解决这个问题,我也想出了一个临时的解决方案,不过首先要保证避免在开发中使用metadata来存储array,使得metadata的逻辑更简单。然后只需要在上述代码中注释掉

	// if overwriting, delete first.
			if (!$multiple) {
                $options = array(
					'guid' => $this->getGUID(),
					'metadata_name' => $name,
					'limit' => 0
				);
				// @todo in 1.9 make this return false if can't add metadata
				// http://trac.elgg.org/ticket/4520
				// 
				// need to remove access restrictions right now to delete
				// because this is the expected behavior
				$ia = elgg_set_ignore_access(true);
				if (false === elgg_delete_metadata($options)) {
					return false;
				}
				elgg_set_ignore_access($ia);
			}

这一部分就可以了,因为后面create_metadata方法实际上会检查如果已存在值,就更新,如果没有,就插入。这样做就能保证在并发写的情况下,只有一条记录被创建或更新,当然,最终的赋值是什么,要取决于最终是哪个进程最后调用并成功执行了sql语句。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值