GreenDao缓存及其问题

1,324 阅读5分钟

前言

上次针对GreenDao使用的源码流程进行了一个分析,了解了它内部是如何来实现对数据库的读写,对于缓存相关的IdentityScope只做了一个简单的概括。本节将针对该数据库的缓存进行一个分析。

ps 因为最近刚好被问到过这个问题,第一反应是GreenDao有缓存??文件缓存还是内存缓存?脑海里搜索了半天,把IdentityScope给忘了。

解决的问题

GreenDao的一个优点是性能高,存取速度快,为什么?存的时候我们知道是利用了SQLiteStatement预编译,解决了SQL注入问题和提高了效率,那么取数据又如何速度快?体现在哪呢?

针对GreenDao的增、删、改、查源码分析见**Android GreenDao 源码分析**

本文关注点:缓存

目录

一、从缓存中取数据

具体源码分析可见 Android GreenDao 源码分析

为了探究取数据为何速度块?我们主要针对取数据进行分析

下面贴的代码是 Android GreenDao 源码分析 中简单使用部分的查找

  //查找
    @Override
    public List<Custom> find(Object... args) {

        if (args == null || args.length == 0) {
        //注释  1
            return getDaoSession().getCustomDao().loadAll();
        }
        QueryBuilder<Custom> queryBuilder = getDaoSession().getCustomDao().queryBuilder();
        if (args.length == 1 && args[0] instanceof String) {
            return queryBuilder.where(CustomDao.Properties.ShopName.eq(args[0])).list();
        } else if (args.length == 2 ) {
            if (args[0] instanceof String && args[1] instanceof String) {
                return queryBuilder.where(CustomDao.Properties.ShopName.eq(args[0]), CustomDao.Properties.ShopDescription.eq(args[1])).list();  
            }
        }
        return null;
    }

注释 1 处代码,在之前的文章中已经分析过了,它内部会调用到loadCurrent方法

    final protected T loadCurrent(Cursor cursor, int offset, boolean lock) {
        //注释 1
        if (identityScopeLong != null) {
            if (offset != 0) {
                // Occurs with deep loads (left outer joins)
                if (cursor.isNull(pkOrdinal + offset)) {
                    return null;
                }
            }
            //key就是rowId,先取出rowId
            long key = cursor.getLong(pkOrdinal + offset);
            //注释 2 
            T entity = lock ? identityScopeLong.get2(key) : identityScopeLong.get2NoLock(key);
            if (entity != null) {
                return entity;
            } else {
                //注释 3
                entity = readEntity(cursor, offset);
                attachEntity(entity);
                if (lock) {
                    identityScopeLong.put2(key, entity);
                } else {
                    identityScopeLong.put2NoLock(key, entity);
                }
                return entity;
            }
        } 
        .....
    }

我们看注释2处,先从这个 IdentityScope 中找数据,没有的话,走到注释 3 处,调用 readEntity 读取数据。

然而lockfalse,所以是从identityScopeLong.get2NoLock(key) 方法取出数据,那么 这个 get2NoLock 方法做了什么?

    public T get2NoLock(long key) {
        Reference<T> ref = map.get(key);
        if (ref != null) {
            return ref.get();
        } else {
            return null;
        }
    }
     private final LongHashMap<Reference<T>> map;

IdentityScopeLong是持有一个 LongHashMap 结构的 map,内部结构为数据 + 链表形式,并不是继承了HashMap

到这里,取缓存结束了,那你可能会问为什么上面 loadCurrent 方法注释 1 处 identityScopeLong != null

因为在API使用过程中,会创建DaoSession,这个过程内部 指定了 IdentityScopeType 的值为 IdentityScopeType.Session,然后会根据你有没有设置primaryKey,或者设置的Keyint类型还是short类型或者是long类型,来初始化为 IdentityScopeLong类型。对应于实体类中 @Id(autoincrement = true) 注解。

代码如下

//DaoMaster
public DaoSession newSession() {
    return new DaoSession(db, IdentityScopeType.Session, daoConfigMap);
}
public DaoSession(Database db, IdentityScopeType type, Map<Class<? extends AbstractDao<?, ?>>, DaoConfig>
        daoConfigMap) {
    super(db);

    customDaoConfig = daoConfigMap.get(CustomDao.class).clone();
    customDaoConfig.initIdentityScope(type);

    customDao = new CustomDao(customDaoConfig, this);

    registerDao(Custom.class, customDao);
}
//DaoConfig
public void initIdentityScope(IdentityScopeType type) {    if (type == IdentityScopeType.None) {
        identityScope = null;
    } else if (type == IdentityScopeType.Session) {
        if (keyIsNumeric) {
            identityScope = new IdentityScopeLong();
        } else {
            identityScope = new IdentityScopeObject();
        }
    } else {
        throw new IllegalArgumentException("Unsupported type: " + type);
    }
}//keyIsNumeric判断
//在DaoConfig初始化构造函数时执行
if (pkProperty != null) {    Class<?> type = pkProperty.type;
    keyIsNumeric = type.equals(long.class) || type.equals(Long.class) || type.equals(int.class)
            || type.equals(Integer.class) || type.equals(short.class) || type.equals(Short.class)
            || type.equals(byte.class) || type.equals(Byte.class);
} else {
    keyIsNumeric = false;
}

二、将数据存入缓存

同样,在之前的文章中分析过,在插入数据过程中,(见 executeInsertInTx 注释 4处) 它会为entity实体类设置ID,并调用attachEntityID和实体类关系映射到IdentityScope中。前提是你设置了主键,主键是否设置从CustomDao中获取。

    protected void updateKeyAfterInsertAndAttach(T entity, long rowId, boolean lock) {
        if (rowId != -1) {
            K key = updateKeyAfterInsert(entity, rowId);
            attachEntity(key, entity, lock);
        } else {
            // TODO When does this actually happen? Should we throw instead?
            DaoLog.w("Could not insert row (executeInsert returned -1)");
        }
    }

只要插入数据成功,rowId就不为 -1,就会执行 if 里面的两行代码

   //CustomDao
   @Override
    protected final Long updateKeyAfterInsert(Custom entity, long rowId) {
        entity.setId(rowId);
        return rowId;
    }

可以看到第一行代码内部直接设置了Customid

接下啦看第二行代码

    protected final void attachEntity(K key, T entity, boolean lock) {
        attachEntity(entity);
        if (identityScope != null && key != null) {
            if (lock) {
                identityScope.put(key, entity);
            } else {
                identityScope.putNoLock(key, entity);
            }
        }
    }

我们先看一下attachEntity做了什么?

    protected void attachEntity(T entity) {
    }

啥也没做。。

因为lock 参数传递的是false ,所以直接看 identityScope.putNoLock方法,这里留意一下,keyrowId,设置给了CustomidentityCustom类。

   //IdentityScopelong类
   @Override
    public void putNoLock(Long key, T entity) {
        put2NoLock(key, entity);
    }
   public void put2NoLock(long key, T entity) {
        map.put(key, new WeakReference<T>(entity));
    }

map的结构是 LongHashMap<Reference<T>>

这里直接将 keyCustom类的弱引用做了一个映射放到map中。

我们看一下是如何存放的

    public T put(long key, T value) {
        //根据key计算出index的位置, long是 8个字节 64位 int 32位 ,高32位 ^ key低32位 然后 &
        //这个计算过程对于hash冲突是友好的,加上避免了 long  % capacity 还是long类型的问题
        final int index = ((((int) (key >>> 32)) ^ ((int) (key))) & 0x7fffffff) % capacity;
        final Entry<T> entryOriginal = table[index];
        for (Entry<T> entry = entryOriginal; entry != null; entry = entry.next) {
            if (entry.key == key) {
                T oldValue = entry.value;
                entry.value = value;
                return oldValue;
            }
        }
        table[index] = new Entry<T>(key, value, entryOriginal);
        size++;
        if (size > threshold) {
        //扩容
            setCapacity(2 * capacity);
        }
        return null;
    }

我们再看一下LongHashMap 是如何扩容的,和HashMap一样,都需要重新计算index位置

    public void setCapacity(int newCapacity) {
        @SuppressWarnings("unchecked")
        Entry<T>[] newTable = new Entry[newCapacity];
        int length = table.length;
        for (int i = 0; i < length; i++) {
            Entry<T> entry = table[i];
            while (entry != null) {
                long key = entry.key;
                //重新根据key分配index
                int index = ((((int) (key >>> 32)) ^ ((int) (key))) & 0x7fffffff) % newCapacity;
​
                Entry<T> originalNext = entry.next;
                entry.next = newTable[index];
                newTable[index] = entry;
                entry = originalNext;
            }
        }
        table = newTable;
        capacity = newCapacity;
        threshold = newCapacity * 4 / 3;
    }

三、引发的问题

1、实体类不支持继承,继承父类的成员变量不能直接存储到数据库中

一般写数据库实体类不会继承,例如Custom类,你会写一个A extends Custom ? 不会。

2、数据库查询缓存不一致问题

你是否遇到过更新数据库后或者是更新数据后,反复查询但是得不到更新后的数据问题?

还有这问题?

反正我没遇到过,平时开发使用的都是同事以前封装好的依赖库,经过查询和验证,确实存在该问题,并且需要在合适的事件清理缓存。

问题发生原由

因为有缓存,每次取数据都是从缓存里面取,所以你更新了数据库,取数据也不是最新的

问题解决

a、在创建DaoSession对象时,指定IdentityScopeType.None,这样,获取的 identityScope为空,缓存就用不了了。

b、在更新数据前,调用  DaoSession  的 clear方法,内部会将 identityScope 缓存清除,一般都是采用这个做法,根据业务需要,在合适的时机清空缓存。

c、通过Dao实现类,例如CustomDao ,调用 deleteAll 方法,不建议,因为内部会删除表。

public void deleteAll() {
    // String sql = SqlUtils.createSqlDelete(config.tablename, null);
    // db.execSQL(sql);

    db.execSQL("DELETE FROM '" + config.tablename + "'");
    if (identityScope != null) {
        identityScope.clear();
    }
}