缓存数据生成到memcache的过程如下:
一、查询数据:
1、根据执行的sql语句转换为缓存的sqlKey值
2、根据sqlKey值查询memcache的缓存数据
3、如果查询到缓存数据返回缓存数据
4、如果查询不到缓存数据则执行sql查询
5、根据执行的sql语句转换为缓存的sqlKey值
6、根据 mapper namespace 生成 groupKey
7、sql查询结果放入缓存,数据结构【sqlKey(sql语句转换为的缓存sqlKey值),object(sql查询结果)】
8、根据groupKey查询sqlKey的集合
9、sql语句转换为的缓存sqlKey值关联到groupKey,数据结构【groupKey,[sqlKey1,sqlKey2,sqlKey3,...](set结果集)】
二、增加、删除、修改数据:
1、执行插入、删除或更新sql
2、根据 mapper namespace 生成 groupKey
3、根据groupKey查询sqlKey
4、根据sqlKey集合大小循环删除sqlKey缓存数据
5、最后删除groupKey缓存数据
以上是MyBatis与Memcache集成后缓存生成与清除的过程,但是使用源码在生产环境出现并发导致数据库和缓存数据不一致问题。具体原因经过查看源码发现以下两个问题:
一、并发查询导致不一致分析
1、 A、B两个用户分别点击查询数据页面,系统执行sql-A和sql-B。
2、 同时执行以上查询逻辑的第8步:根据groupKey查询sqlKey的集合
3、写入memcache的缓存源码如下:
1 public void putObject(Object key, Object value, String id) { 2 String keyString = toKeyString(key); 3 String groupKey = toKeyString(id); 4 5 if (log.isDebugEnabled()) { 6 log.debug("Putting object (" 7 + keyString 8 + ", " 9 + value 10 + ")"); 11 } 12 13 storeInMemcached(keyString, value); 14 15 // add namespace key into memcached 16 Set<String> group = getGroup(groupKey); 17 if (group == null) { 18 group = new HashSet<String>(); 19 } 20 group.add(keyString); 21 22 if (log.isDebugEnabled()) { 23 log.debug("Insert/Updating object (" 24 + groupKey 25 + ", " 26 + group 27 + ")"); 28 } 29 30 storeInMemcached(groupKey, group); 31 }
4、假设sql-A执行到第20行代码,sql-B执行到第16行,导致最后存储到memcache的数据变成了如下情况:
先执行存储:【groupKey,sql-A】;再执行【groupKey,sql-B】
5、结论:后执行的缓存数据覆盖先前的缓存数据,导致查询sql-A、sql-B可以查询到缓存数据,如果sql-A的数据发生修改会根据groupKey查询sqlKey的集合然后循环删除sqlKey缓存数据,导致只能删除sql-B,sql-A的缓存不删除导致数据不一致。
二、并发查询和新增(删除或修改)导致不一致分析
1、 A、B两个用户分别点击查询、新增(删除或修改)页面数据,系统执行sql-A和sql-B。
2、 清除memcache的缓存源码如下:
1 public void removeGroup(String id) { 2 String groupKey = toKeyString(id); 3 4 Set<String> group = getGroup(groupKey); 5 6 if (group == null) { 7 if (log.isDebugEnabled()) { 8 log.debug("No need to flush cached entries for group '" 9 + id 10 + "' because is empty"); 11 } 12 return; 13 } 14 15 if (log.isDebugEnabled()) { 16 log.debug("Flushing keys: " + group); 17 } 18 19 for (String key : group) { 20 client.delete(key); 21 } 22 23 if (log.isDebugEnabled()) { 24 log.debug("Flushing group: " + groupKey); 25 } 26 27 client.delete(groupKey); 28 }
3、sql-A执行到写入逻辑的第30行代码完成,sql-B执行清除逻辑到27行代码,导致最后存储到memcache的数据变成了如下情况:
先执行存储:【groupKey,sql-A】;再执行删除groupKey
4、结论:删除groupKey的缓存后导致sql-A与groupKey失去了关联关系导致后面无聊更新、删除或写入都不会清除sql-A的缓存数据导致数据不一致。
最后是我的解决方案:
1、写入缓存的源码修改如下:
1 public void putObject(Object key, Object value, String id) { 2 String keyString = toKeyString(key); 3 String groupKey = toKeyString(id); 4 5 if (log.isDebugEnabled()) { 6 log.debug("Putting object (" 7 + keyString 8 + ", " 9 + value 10 + ")"); 11 } 12 13 storeInMemcached(keyString, value); 14 15 try { 16 //因为读写锁互斥,该处加读锁是为了防止多线程同时写和删除 17 GROUP_LOCK.readLock().lock(); 18 //为防止多线程同时获取group记录写入缓存覆盖掉其他线程的内容需要加同步锁 19 synchronized (groupKey.intern()) { 20 // add namespace key into memcached 21 Set<String> group = getGroup(groupKey); 22 if (group == null) { 23 group = new HashSet<String>(); 24 } 25 group.add(keyString); 26 27 if (log.isDebugEnabled()) { 28 log.debug("Insert/Updating object (" 29 + groupKey 30 + ", " 31 + group 32 + ")"); 33 } 34 35 storeInMemcached(groupKey, group); 36 } 37 } finally { 38 GROUP_LOCK.readLock().unlock(); 39 } 40 }
2、清除缓存的源码修改如下:
1 public void removeGroup(String id) { 2 String groupKey = toKeyString(id); 3 4 try { 5 //因为读写锁互斥,该处加读锁是为了防止多线程同时写和删除 6 GROUP_LOCK.writeLock().lock(); 7 Set<String> group = getGroup(groupKey); 8 9 if (group == null) { 10 if (log.isDebugEnabled()) { 11 log.debug("No need to flush cached entries for group '" 12 + id 13 + "' because is empty"); 14 } 15 return; 16 } 17 18 if (log.isDebugEnabled()) { 19 log.debug("Flushing keys: " + group); 20 } 21 22 for (String key : group) { 23 client.delete(key); 24 } 25 26 if (log.isDebugEnabled()) { 27 log.debug("Flushing group: " + groupKey); 28 } 29 30 client.delete(groupKey); 31 } finally { 32 GROUP_LOCK.writeLock().unlock(); 33 } 34 }