Lost Update是在并发情况下经常出现的情况。
对于Android客户端中的ORM,据我了解ORM很难做指定修改列,一般都是整行更新。而ORM一般都会做内存的缓存。
这两个情况会导致,客户端的ORM中会有另一种lost update:AB两个线程同时更新同一个列,而对象不是同一个(某个时刻,B对ORM对象进行了copy),此时,A的修改(对任意列)都会被B覆盖掉。简单的办法有两个:
- 要求外部修改的执行分为三个连续的动作:读-改-写
- 要求外部不允许copy
这两个都可以保证A的修改都能反应到B的对象上,但是,对于外部的口头要求都会导致潜在的威胁。
抽象看这个问题,最合理的就是用引文中的Optimistic Locking,那么就要求调用方理解冲突的发生,并且能够保证操作的重入。而使用第一个方法,关键在于读改写要是一个原子操作,而且改之前的读是必须的。那最好就是使用Template模式,或者说是使用Command模式(作为Template中的可变部分)来进行改的部分,整体在manager层保证串行。
这样外部需要遵循的只有:所有更新逻辑都封装到command,不要依赖更新开始的时间、不要依赖可变的外部数据。这个要求在客户端应该是比较容易遵循的。
上代码:
Command需要实现的接口:
public interface IDataUpdater {
Data update(Data old);
}
DataManager是ORM的缓存和更新管理器:
- 缓存使用命令链,很简单
- 更新管理主要在update函数
- 传入一个命令,这个命令是一个独立的更新功能,不依赖外部
- 使用Handler做串行化,保证了更新的原子性
- updateAsync对每个key进行上锁,锁的粒度小了很多,理论上性能会好
public class DataManager {
private static final String sKeyKey = "key";
private List<IDataManager<String, Data>> mManagers = new ArrayList<>();
private Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
IDataUpdater updater = (IDataUpdater) msg.obj;
String key = msg.getData().getString(sKeyKey);
Data data = getData(key);
onNewData(key, updater.update(data));
return true;
}
});
private ConcurrentHashMap<String, ReentrantLock> mLocks = new ConcurrentHashMap<>();
private DataManager() {
mManagers.add(new CacheManager());
mManagers.add(new MockManager());
Collections.sort(mManagers, new Comparator<IDataManager<String, Data>>() {
@Override
public int compare(IDataManager<String, Data> lhs, IDataManager<String, Data> rhs) {
return lhs.getPriority() - rhs.getPriority();
}
});
}
private static volatile DataManager sInstance;
public static DataManager getInstance() {
if (null == sInstance) {
synchronized (DataManager.class) {
if (null == sInstance) {
sInstance = new DataManager();
}
}
}
return sInstance;
}
public void update(IDataUpdater updater, String key) {
Message msg = mHandler.obtainMessage();
msg.obj = updater;
Bundle bundle = new Bundle();
bundle.putString(sKeyKey, key);
msg.setData(bundle);
msg.sendToTarget();
}
public void updateAsync(IDataUpdater updater, String key) {
mLocks.putIfAbsent(key, new ReentrantLock());
ReentrantLock lock = mLocks.get(key);
try {
lock.lock();
Data data = getData(key);
onNewData(key, updater.update(data));
} finally {
if (0 == lock.getHoldCount()) {
mLocks.remove(key);
}
lock.unlock();
}
}
public Data getData(String id) {
Data data = null;
int priority = IDataManager.PRIORITY_NONE;
for (IDataManager<String, Data> manager : mManagers) {
data = manager.get(id);
if (null != data) {
priority = manager.getPriority();
break;
}
}
notifyNewData(id, data, priority);
return data;
}
private void onNewData(String key, Data update) {
Data data = null;
int priority = IDataManager.PRIORITY_NONE;
for (IDataManager<String, Data> manager : mManagers) {
Data tmp = manager.set(key, update, IDataManager.PRIORITY_WRITE, false);
if (null != tmp) {
data = tmp;
priority = manager.getPriority();
}
}
notifyNewData(key, data, priority);
if (null != data) {
Log.e("GT", data.toString());
}
}
private void notifyNewData(String id, Data data, int priority) {
if (null == data) {
return;
}
for (IDataManager<String, Data> manager : mManagers) {
manager.set(id, data, priority, true);
}
}
private interface IDataManager<K, T> {
int PRIORITY_NONE = 0;
int PRIORITY_CACHE = 100;
int PRIORITY_DB = 200;
int PRIORITY_WRITE = 300;
T get(K key);
T set(K key, T value, int priority, boolean fromRead);
int getPriority();
}
private static class CacheManager implements IDataManager<String, Data> {
private LruCache<String, Data> mCache = new LruCache<>(5);
private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock();
@Override
public Data get(String key) {
ReentrantReadWriteLock.ReadLock readLock = mLock.readLock();
try {
readLock.lock();
return mCache.get(key);
} finally {
readLock.unlock();
}
}
@Override
public Data set(String key, Data value, int priority, boolean fromRead) {
if (priority <= PRIORITY_CACHE) {
return null;
}
ReentrantReadWriteLock.WriteLock writeLock = mLock.writeLock();
try {
writeLock.lock();
mCache.put(key, value);
} finally {
writeLock.unlock();
}
return value;
}
@Override
public int getPriority() {
return PRIORITY_CACHE;
}
}
private static class MockManager implements IDataManager<String, Data> {
@Override
public Data get(String key) {
Data data = new Data();
data.setId(key);
data.setFoo(key);
data.setBar(key);
data.setUpdateTime(SystemClock.uptimeMillis());
return data;
}
@Override
public Data set(String key, Data value, int priority, boolean fromRead) {
if (fromRead || priority <= PRIORITY_DB) {
return null;
}
//write db
value.setUpdateTime(SystemClock.uptimeMillis());
return value;
}
@Override
public int getPriority() {
return PRIORITY_DB;
}
}
}
续
在看了Redux之后,发现这个其实就是一个Reducer-Store模型的实现,缺乏的是Data对象的Immutable。可以用这个办法通融一下:
public class Data {
public interface IDataModifier {
void setId(String id);
void setFoo(String foo);
void setBar(String bar);
void setUpdateTime(long update);
}
private String mId;
//db field
private String mFoo;
private String mBar;
//db field
private long mUpdateTime;
private IDataModifier mModifier = new IDataModifier() {
public void setId(String id) {
mId = id;
}
public void setFoo(String foo) {
mFoo = foo;
}
public void setUpdateTime(long updateTime) {
mUpdateTime = updateTime;
}
public void setBar(String bar) {
mBar = bar;
}
};
public Data() {
}
public Data(Data src) {
mId = src.mId;
mFoo = src.mFoo;
mBar = src.mBar;
mUpdateTime = src.mUpdateTime;
}
public String getId() {
return mId;
}
public long getUpdateTime() {
return mUpdateTime;
}
@Override
public String toString() {
return super.toString() + ", " + mId + ", " + mFoo + ", " + mBar + ", " + mUpdateTime;
}
public String getBar() {
return mBar;
}
protected IDataModifier getModifier() {
return mModifier;
}
public interface IDataUpdater {
boolean update(Data.IDataModifier modifier);
}
Data对象暴露一个public的修改接口对象Modifier,但是Modifier只有一个protected的getter,也就是说,只有DataManager能获得到Modifier,就保证了只有在DataManager的update中才能修改Data对象。
弱弱的说,只有这么几个阅读量,难道就没人遇到这个问题吗?!