最近有一个需求,对数据的实时性要求比较高,之前寻找过一些内存数据库,首先将收费的产品先排除掉,然后再排除一些嵌入式产品,最终留下两个产品:
1:Mysql内存引擎;
2:基于内存文件映射的文档数据库Mongodb;
针对以上两种产品,mysql内存引擎有如下缺点是我们放弃的理由:
1:数据量比较大的情况,比之innodb引擎优势并不明显,数据小的时候采用B-Tree索引结构性能还是可以接受的。
2:内存引擎对于持久化是非常吃力的,如果大量数据操作均在内存引擎中完成,那么就需要我们来做数据的持久化,无论是复制订阅方式还是程序方式均存在比较大的困难。
3:内存引擎所占用的数据空间比较大,特别是默认的hash结构,同数据量的数据是sql server的6倍,B-Tree结构未做具体比较。
选择Mongodb的理由:
1:自身具备数据的持久化;
2:对于数据的插入性能优越,特别是采用不返回值的模式,理论上来讲是一种异步操作;
3:由于文档存储是k-value方式,所以对于单条记录的查询操作性能不用考虑;
4:对于水平扩展,数据的主从备份均非常容易。
使用Mongodb过程中遇到过的问题:
1:由于存储的内容完全由客户端决定,于是对于一些对字段做更新的操作,比如sql 中的set count=count+1;存在一个并发更新问题,当两个线程同时需要对count字段加1时,两个线程同时查询到的count内容为1,当线程A完成加1后,数据库中实际已经变成2,此时线程B对count进行加1,此时也是2,于时原来是3的内容,最终会变成2。主要原因就在于更新字段时并不会在服务端进行,字段的内容完全是由客户端决定,即更新时并不会像sql server一样,在服务端变量基础上再做操作。
解决方案:对记录增加自定义的锁标识,尝试在记录上增加锁,如果加锁成功,然后再查询,如果锁标识证明是当前线程才进行更新,否则循环尝试加锁。这里有发生死锁的可能,为此在加锁标记时,可以增加一个锁时间,当超过锁时间后,自动解锁,这样可以避免死锁。
2:数据的插入,主要有两种模式,一种是不返回处理结果,一种是返回处理结果,如果我们不太关心返回结果,那么可以获得最高插入性能,对于一些关注处理结果的业务场景,就需要采取返回结果方式。
3:最好自定义主键,不采用默认的_id值,我们处理一条学生记录,学生记录是唯一的,比如学生ID,如果我们采用默认_id值,那么在插入数据时也会存在并发问题:两个线程同时处理学生A,当时查询时发现数据库中没有学生A的记录,于时就进行插入,由于默认的_id值是自动生成,类似guid或者是自增id,说的通俗点就是会生成一个不重复的key,此时就会插入学生A记录两次,如果我们选用学生ID做为主键,就可以避免此种情况,当第二次尝试插入时,系统会抛出主键重复的异常。
为什么需要对mongodb驱动重新封装?
其实无论是samus驱动还是官方驱动,其实功能都各有秋千,samus驱动对数据操作进行了Linq封装,即我们在操作List时,完全可以采用类似Linq一样的语法,这样可以使我们的学习成本降低,官方驱动的特点是针对数据处理有返回值。我们的需求是即需要操作返回值也需要Linq封装,于时我找到了老赵写的easyMongo,但在它的基本上做了一小部分取舍,取舍内容如下:
1:去掉了如下逻辑,原意是将大多数操作均放在同一连接中处理,但不满足我们的需求;我们直接对外提供一次性执行方法
public void SubmitChanges()
{
using (this.Database.Server.RequestStart(this.Database))
{
this.DeleteEntities();
this.UpdateEntites();
this.InsertEntities();
}
}
2:对上面所提到的insert,update ,delete要求有返回值,对于insert提供两种模式,一类是需要返回值一类则不需要。特别是像更新和删除,我们是非常有必要知道它的处理结果,如果不关心结果,我认为可以是一些数据准确性要求不高的场景。
{
if (entity == null) throw new ArgumentNullException();
return this.InsertEntities(entity,ESafeMode.False);
}
public EInsertstatus InsertOnSubmit(TEntity entity, ESafeMode safeMode)
{
if (entity == null) throw new ArgumentNullException();
return this.InsertEntities(entity, safeMode);
}
public void InsertBatchOnSubmit(List<TEntity> entity)
{
if (entity == null) throw new ArgumentNullException();
this.InsertBatchEntities(entity);
}
public bool UpdateOnSubmit(TEntity entity)
{
if (entity == null) throw new ArgumentNullException();
return this.UpdateEntites(entity);
}
public bool DeleteOnSubmit(TEntity entity)
{
if (entity == null) throw new ArgumentNullException();
return this.DeleteEntities(entity);
}
3:对查询的修改,在将实体映射为mongo文档时,需要对我们自定义的实体提供一个Map,这个Map主要是为了标识哪个是主键,哪些字段是可以被Linq识别的字段,比如我们要按学生ID查询,就需要在Map中增加此字段,示例如下:
{
public AggregateCompanyRecruitStudentInfoMap()
{
Collection("AggregateCompanyRecruitStudentInfo");
Property(n => n.OrganizationID).Identity();
Property(n => n.LockTime);
Property(n => n.LockFiled);
}
}
但源码中的查询,如果没有提供查询表达式,则默认只返回Map中提到的字段,对其它实体字段则不处理,为此修改如下:即没有查询表达式的情况返回完整文档。
if (null != selector)
{
fieldsDoc = mapper.GetFields(selector);
}
4:去掉原有复杂的更新逻辑,删除对我们没用的状态跟踪,要的就是简单直接,过于复杂的逻辑不利于开发。
Mymongo的一个简化结构图分享给大家:
说明:最后欢迎大家交流mongodb,文中如有不对的地方,希望批评指正,大家共同进步。