使用Command模式解决单节点cache中lost update

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对象。

弱弱的说,只有这么几个阅读量,难道就没人遇到这个问题吗?!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值