关于jivedon的并发问题,请板桥老师指点。

09-05-13 xmuzyu
jivedon中获取对象是在dao的装饰器中获取的,ForumDaoCache代码如下:

Forum forum = (Forum) containerUtil.getModelFromCache(forumId, Forum.class);

if (forum == null){

forum = super.getForum(forumId);

containerUtil.addModeltoCache(forumId, forum);

}

return forum;

这个时候加有两个并发用户同时调用ForumDaoCache的getForum方法,此时有可能两个线程都获得了一个不同的Forum对象,这个时候是最后的线程获取的对象会覆盖缓存中以前的值,而后在ForumDirector中判断forum.isEmbedded(),其实这个时候不同的线程已经有不同的对象,这个判断也就失去了意义了。

请banq老师指点,是我理解有误还是本来就有这个问题。我在项目中通过一个MonitorObject解决了这个问题,保证了并发线程获取全局唯一的对象实例。

banq
2009-05-13 20:13
这里面有一个性能和安全综合,是有可能出现你说的情况,但是出现两个Forum只是系统开始一瞬间,几率很小,正常情况,都是对缓存中的Forum读取,另外这还看看Forum是否是聚合根实体,如果不是,就当作是两个值对象,复制克隆也无非不可。

相反,如果为了避免这个情况,目前通过锁或其他控制总是会损失性能,再加上如果这个Forum获取出来只是读共享,可以忽略不计。

当然如果是需要缓存写,那也不会影响写,因为不可能在Forum创建的同一个线程中有业务写事件,这是不可能发生的。

如果不介意,可以谈谈你的MonitorObject是怎么实现的,这样做得很细致完美当然好。

xmuzyu
2009-05-13 20:35
>>相反,如果为了避免这个情况,目前通过锁或其他控制总是会损失性能,再加上如果这个Forum获取出来只是读共享,可以忽略不计。

呵呵,多谢老师解答。我的项目中因为要对获取的对象进行写操作,所以必须保证获得对象是唯一的。

我的办法是新建一个监视器对象MonitorObject:如下:

public class MonitorObject {

private Class<?> modelClass;

private Serializable key;

public Class<?> getModelClass() {

// TODO Auto-generated method stub

return modelClass;

}

public boolean needSynchroize(Class<PersonalPage> modelClass, String id) {

return this.modelClass.equals(modelClass) && this.key.equals(id);

}

public MonitorObject(Class<?> modelClass, Serializable key) {

super();

this.modelClass = modelClass;

this.key = key;

}

}

有一个ModelManager负责管理Model,ModelManager首先从缓存中获取对象,如果没有不是返回null,而是返回MonitorObject,这个MnitorObject负责检测并发线程想要获得的对象是否是同一个Model,而我判断是否是同一个Model就是简单的判断对应的class和model主键是否一致,如果一致就同步。

客户端代码:

Object model = modelManager.getModel(PersonalPage.class, owner.getId());

if (model instanceof PersonalPage) {

return (PersonalPage) model;

}

//如果缓存中没有对应的对象,那么就从数据库构建,然后放回缓存

PersonalPage page = null;

if (model instanceof MonitorObject) {

MonitorObject monitor = (MonitorObject) model;

if (monitor.needSynchroize(PersonalPage.class, owner.getId())) {

synchronized (PersonalPage.class) {

Object o = modelManager.getModel(PersonalPage.class, owner

.getId());

//fireGetEvent(event);

//Object o = event.getModel();

if (o instanceof PersonalPage) {

return (PersonalPage) o;

}

construct(page, owner);

modelManager.addModle(page.getId(), page);

}

}

}

return page;

ACoder
2009-05-13 20:45
我没有看过jdon的源代码

》》但是出现两个Forum只是系统开始一瞬间,几率很小,正常情况,都是对缓存中的Forum读取

我觉得这样的说法不负责任,不能因为概率小就不去维护。

其实只要在addModeltoCache的方法里面加上锁,然后再读取一次cache,如果无该对象,则添加对象,然后释放锁。毕竟读多写少,加锁性能损失不大。

xmuzyu
2009-05-13 20:55
>>其实只要在addModeltoCache的方法里面加上锁,然后再读取一次cache,如果无该对象,则添加对象,然后释放锁。毕竟读多写少,加锁性能损失不大。

恩,这样保证了放入缓存对象是唯一的,但是不能保证所有并发线程获得的对象都是唯一,并且会造成一些不必要的构造过程,构造过程也许很费时,不需要每个并发线程都构造一下。

ACoder
2009-05-13 21:32
从代码来看containerUtil应当是一个单体模式,由他来进行Cache的管理,forum对象代表什么??是整个论坛的跟还是一个子栏目??每个人取得的不一样么??我感觉应当是一样的东西。

>>构造过程也许很费时,不需要每个并发线程都构造一下。

这句话没有理解,每个并发线程只是获取而已啊。

xmuzyu
2009-05-13 21:42
>>这句话没有理解,每个并发线程只是获取而已啊。

在应用中,我对于复杂的聚合根,采用builder模式,而对于不太复杂简单点的实体或者聚合根则采用Factory进行,无论是Builder还是Factory,他们构造对象都是首先从缓存去,如果有就返回,如果没有就根据数据库来构造。假如有两个并发的线程都要获取同一个MODEL,结果发现这个Model不在缓存,这样两个并发线程就都需要从数据库构造,并且最后完成的线程会覆盖先前线程在缓存中写入得值(用前辈您的方法就可以实现不会覆盖)当然了覆盖也未尝不可,如果这个时候获取的对象不需要全局的写操作就可以忽略这种情况,但是如果获取的对象要进行写操作就必须要保证所有并发线程获得同一个对象。所以我才采用了那个监视器对象。

xmuzyu
2009-05-14 16:22
不好意思,昨天发布的这个客户端代码是错误的,是我以前的工程的代码(郁闷,eclipse里面几十个工程自己把自己都搞晕了),以前的那个版本有错误。

所有的客户端如果发现缓存中没有存在对象,就通过MutexManager获得Metux,然后在此Metux上同步。不过MutexManager要求每一个Model对应一个,如果是全局model对应一个,竞争会非常激烈。不知道banq老师还有什么好的办法,我也改进一下。

以下是:Mutex代码

public class Mutex {

private volatile AtomicInteger usingCount ;

private volatile Serializable key ;

public Mutex(Serializable key) {

this.key = key;

}

public void addCount() {

usingCount.incrementAndGet();

}

public int getCount() {

return usingCount.get();

}

public void decrementCount() {

usingCount.decrementAndGet();

}

public Object getKey() {

return key;

}

}

以下是MutexManager代码:

public class MutexManagerImpl implements MutexManager {

private final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(

true);

private final Lock write = reentrantReadWriteLock.writeLock();

private final Map<Serializable, Mutex> mutexs = new ConcurrentHashMap<Serializable, Mutex>();

@Override

public Mutex getMutex(Serializable key) {

Mutex mutex = mutexs.get(key);

if (mutex != null) {

mutex.addCount();

return mutex;

}

return createMutex(key);

}

@Override

public void release(Mutex mutex) {

mutex.decrementCount();

if (mutex.getCount() > 0) {

return;

}

mutexs.remove(mutex.getKey());

}

private Mutex createMutex(Serializable key) {

//此锁只有在MODEL不在缓存,并且在针对第一个获得特定Id的线程才会用。

write.lock();

try {

Mutex mutex = mutexs.get(key);

if (mutex == null) {

mutex = new Mutex(key);

mutexs.put(key, mutex);

}

mutex.addCount();

return mutex;

} finally {

write.lock();

}

}

}

[该贴被xmuzyu于2009-05-14 16:23修改过]

[该贴被xmuzyu于2009-05-14 16:33修改过]

ACoder
2009-05-14 21:16
》》在应用中,我对于复杂的聚合根,采用builder模式,而对于不太复杂简单点的实体或者聚合根则采用Factory进行,无论是Builder还是Factory,他们构造对象都是首先从缓存去,如果有就返回,如果没有就根据数据库来构造。假如有两个并发的线程都要获取同一个MODEL,结果发现这个Model不在缓存,这样两个并发线程就都需要从数据库构造,并且最后完成的线程会覆盖先前线程在缓存中写入得值(用前辈您的方法就可以实现不会覆盖)当然了覆盖也未尝不可,如果这个时候获取的对象不需要全局的写操作就可以忽略这种情况,但是如果获取的对象要进行写操作就必须要保证所有并发线程获得同一个对象。所以我才采用了那个监视器对象。

我觉得可以这样设计,设计一个Model构造者,专门负责Model的构造,为单独的线程(ModelMakerThread)。对象获取者从缓存中未获取对象,则唤醒ModelMakerThread线程,向其任务队列中提交一条命令,然后该线程sleep,ModelMakerThread先检测缓存中是否存在Model,如果不存在则从数据库中生成Model,然后添加到Cache,最后唤醒等待线程。

banq
2009-05-15 14:08
>不过MutexManager要求每一个Model对应一个,如果是全局model对应一个,竞争会非常激烈。

没有绝对的安全,安全事务和性能是一对矛盾,适度就可以了,就象数据库的ACID锁策略选择一样,最安全的排他锁是最慢的。

有些问题要根据需求具体情况来决定这个适度,比如论坛这个应用,就可以要求松些,如果你是关键业务,那么要求严些。

加锁是为了写,如果写竞争会非常激烈,那么按照DDD思想,就将锁的范围缩小,将那些会发生写竞争字段独立成一个对象,然后在这个对象内部用不变性约束,加锁加在这个对象内,这样,能避免无辜,提高精准率。

[该贴被banq于2009-05-15 14:09修改过]

xmuzyu
2009-05-15 16:49
>>加锁是为了写,如果写竞争会非常激烈,那么按照DDD思想,就将锁的范围缩小,将那些会发生写竞争字段独立成一个对象,然后在这个对象内部用不变性约束,加锁加在这个对象内,这样,能避免无辜,提高精准率。

多谢banq老师。我目前就是您说的这样做的。

我现在一个项目中,我采用了基于事件的设计,所有系统操作都产生事件,事件处理器负责进行处理事件,而事件处理器又可以采用不同的事件处理策略,比如同步和异步的,对于数据库操作可以通过JMS异步实现,或者也可以封装Executor框架来进行异步处理。而对于缓存的操作,就直接采用在事件处理器同步执行。至于事件的触发可以通过手动触发或者利用spring的aspectj语法,通过AOP来触发。

xmuzyu
2009-05-15 16:52
to acoder.

这种方法还没试过。不过我觉得这样的话,就要对容器管理的线程操作了,因为获取操作是有web 容器管理的线程来执行的。

ACoder
2009-05-15 20:46
我只是提供一种思路,而且这样可以让那一部分更具备可伸缩性以及性能控制,而且可以相对容易的迁移到其他项目中。如果想不控制线程只需要在命令加入队列以后sleep固定时间后继续读取,不过这种方法不推荐。

猜你喜欢