原型模式:如何最快速地clone一个HashMap散列表?
原型模式针对创建成本比较大的对象,利用已有对象进行复制的方式进行创建,以达到节省创建时间的目的。
原理与应用
-
如果对象的创建成本比较大,而同一个类不同对象之间的差别不大(大部分字段相同),在这种情况下,我们可以利用已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。
-
什么时候“对象的创建成本比较大”?
普通的对象创建是申请内存、成员变量赋值,本身不会花费太多时间。我们指的是:对象中的数据需要复杂的计算(排序、计算哈希),或者需要从RPC、网络、数据库、文件系统等非常慢速的IO中读取。
-
一个例子:
假设数据库中存储了大约 10 万条“搜索关键词”信息,每条信息包含关键词、关键词被搜索的次数、信息最近被更新的时间等。系统 A 在启动的时候会加载这份数据到内存中,用于处理某些其他的业务需求。为了方便快速地查找某个关键词对应的信息,我们给关键词建立一个散列表索引。
如果你熟悉的是 Java 语言,可以直接使用语言中提供的 HashMap 容器来实现。其中,HashMap 的 key 为搜索关键词,value 为关键词详细信息(比如搜索次数)。我们只需要将数据从数据库中读取出来,放入 HashMap 就可以了。
不过,我们还有另外一个系统 B,专门用来分析搜索日志,定期(比如间隔 10 分钟)批量地更新数据库中的数据,并且标记为新的数据版本。比如,在下面的示例图中,我们对 v2 版本的数据进行更新,得到 v3 版本的数据。这里我们假设只有更新和新添关键词,没有删除关键词的行为。
为了保证系统 A 中数据的实时性(不一定非常实时,但数据也不能太旧),系统 A 需要定期根据数据库中的数据,更新内存中的索引和数据。
需求实现
在系统A中,记录当前数据的版本Va对应的更新时间Ta,从数据库中捞出更新时间大于Ta的所有搜索关键词,针对这个差集中的每个关键词进行处理,以及在散列表的做更新操作(时间戳,次数),不在的添加进去。
public class Demo { private ConcurrentHashMap<String, SearchWord> currentKeywords = new ConcurrentHashMap<>(); private long lastUpdateTime = -1; public void refresh() { // 从数据库中取出更新时间>lastUpdateTime的数据,放入到currentKeywords中 List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime); long maxNewUpdatedTime = lastUpdateTime; for (SearchWord searchWord : toBeUpdatedSearchWords) { if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) { maxNewUpdatedTime = searchWord.getLastUpdateTime(); } if (currentKeywords.containsKey(searchWord.getKeyword())) { currentKeywords.replace(searchWord.getKeyword(), searchWord); } else { currentKeywords.put(searchWord.getKeyword(), searchWord); } } lastUpdateTime = maxNewUpdatedTime; } private List<SearchWord> getSearchWords(long lastUpdateTime) { // TODO: 从数据库中取出更新时间>lastUpdateTime的数据 return null; } }
- 但是要求,任何时刻,系统 A 中的所有数据都必须是同一个版本的,要么都是版本 a,要么都是版本 b,不能有的是版本 a,有的是版本 b。那刚刚的更新方式就不能满足这个要求了。除此之外,我们还要求:在更新内存数据的时候,系统 A 不能处于不可用状态,也就是不能停机更新数据。
- 解法:我们把正在使用的数据的版本定义为“服务版本”,当我们要更新内存中的数据的时候,我们并不是直接在服务版本(假设是版本 a 数据)上更新,而是重新创建另一个版本数据(假设是版本 b 数据),等新的版本数据建好之后,再一次性地将服务版本从版本 a 切换到版本 b。这样既保证了数据一直可用,又避免了中间状态的存在。
public class Demo { private HashMap<String, SearchWord> currentKeywords=new HashMap<>(); public void refresh() { HashMap<String, SearchWord> newKeywords = new LinkedHashMap<>(); // 从数据库中取出所有的数据,放入到newKeywords中 List<SearchWord> toBeUpdatedSearchWords = getSearchWords(); for (SearchWord searchWord : toBeUpdatedSearchWords) { newKeywords.put(searchWord.getKeyword(), searchWord); } currentKeywords = newKeywords; } private List<SearchWord> getSearchWords() { // TODO: 从数据库中取出所有的数据 return null; } }
- 但是,我们这里有十万条数据。要从DB读出来还要算哈希值,显然这个操作很耗时。
- 我们拷贝 currentKeywords 数据到 newKeywords 中,然后从数据库中只捞出新增或者有更新的关键词,更新到 newKeywords 中。而相对于 10 万条数据来说,每次新增或者更新的关键词个数是比较少的,所以,这种策略大大提高了数据更新的效率。
public class Demo { private HashMap<String, SearchWord> currentKeywords=new HashMap<>(); private long lastUpdateTime = -1; public void refresh() { // 原型模式就这么简单,拷贝已有对象的数据,更新少量差值 HashMap<String, SearchWord> newKeywords = (HashMap<String, SearchWord>) currentKeywords.clone(); // 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeywords中 List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime); long maxNewUpdatedTime = lastUpdateTime; for (SearchWord searchWord : toBeUpdatedSearchWords) { if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) { maxNewUpdatedTime = searchWord.getLastUpdateTime(); } if (newKeywords.containsKey(searchWord.getKeyword())) { SearchWord oldSearchWord = newKeywords.get(searchWord.getKeyword()); oldSearchWord.setCount(searchWord.getCount()); oldSearchWord.setLastUpdateTime(searchWord.getLastUpdateTime()); } else { newKeywords.put(searchWord.getKeyword(), searchWord); } } lastUpdateTime = maxNewUpdatedTime; currentKeywords = newKeywords; } private List<SearchWord> getSearchWords(long lastUpdateTime) { // TODO: 从数据库中取出更新时间>lastUpdateTime的数据 return null; } }
原型模式的实现方式:深拷贝和浅拷贝
浅拷贝:复制索引,不复制数据,新旧对象还是指向相同数据;
深拷贝:创建新索引,复制数据,新对象根据索引指向新数据。
- 所以之前的代码使用Java中的clone还是浅拷贝,指向的还是新旧版本混合的数据。所以需要使用深拷贝。
- 两种深拷贝的实现:
- 递归拷贝
- 序列化之后反序列化成新对象。
- 递归拷贝:
public class Demo { private HashMap<String, SearchWord> currentKeywords=new HashMap<>(); private long lastUpdateTime = -1; public void refresh() { // Deep copy HashMap<String, SearchWord> newKeywords = new HashMap<>(); for (HashMap.Entry<String, SearchWord> e : currentKeywords.entrySet()) { SearchWord searchWord = e.getValue(); SearchWord newSearchWord = new SearchWord( searchWord.getKeyword(), searchWord.getCount(), searchWord.getLastUpdateTime()); newKeywords.put(e.getKey(), newSearchWord); } // 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeywords中 List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime); long maxNewUpdatedTime = lastUpdateTime; for (SearchWord searchWord : toBeUpdatedSearchWords) { if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) { maxNewUpdatedTime = searchWord.getLastUpdateTime(); } if (newKeywords.containsKey(searchWord.getKeyword())) { SearchWord oldSearchWord = newKeywords.get(searchWord.getKeyword()); oldSearchWord.setCount(searchWord.getCount()); oldSearchWord.setLastUpdateTime(searchWord.getLastUpdateTime()); } else { newKeywords.put(searchWord.getKeyword(), searchWord); } } lastUpdateTime = maxNewUpdatedTime; currentKeywords = newKeywords; } private List<SearchWord> getSearchWords(long lastUpdateTime) { // TODO: 从数据库中取出更新时间>lastUpdateTime的数据 return null; } }
- 序列化反序列化
public Object deepCopy(Object object) { ByteArrayOutputStream bo = new ByteArrayOutputStream(); ObjectOutputStream oo = new ObjectOutputStream(bo); oo.writeObject(object); ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray()); ObjectInputStream oi = new ObjectInputStream(bi); return oi.readObject(); }
-
但是上述两种方式都很耗时。所以比较好的方式利用深浅拷贝的特性:
先用浅拷贝创建newKeyWords,对于需要更新的SearchWord再使用深拷贝创建一份新对象,替换newKeyWorks中的老对象。毕竟需要更新的数据是很少的。即利用了浅拷贝节省时间、空间的有点(大部分对象),又能保证 currentKeywords 中的中数据都是老版本的数据。具体的代码实现如下所示。这也是标题中讲到的,在我们这个应用场景下,最快速 clone 散列表的方式。
public class Demo { private HashMap<String, SearchWord> currentKeywords=new HashMap<>(); private long lastUpdateTime = -1; public void refresh() { // Shallow copy HashMap<String, SearchWord> newKeywords = (HashMap<String, SearchWord>) currentKeywords.clone(); // 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeywords中 List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime); long maxNewUpdatedTime = lastUpdateTime; for (SearchWord searchWord : toBeUpdatedSearchWords) { if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) { maxNewUpdatedTime = searchWord.getLastUpdateTime(); } if (newKeywords.containsKey(searchWord.getKeyword())) { newKeywords.remove(searchWord.getKeyword()); } newKeywords.put(searchWord.getKeyword(), searchWord); } lastUpdateTime = maxNewUpdatedTime; currentKeywords = newKeywords; } private List<SearchWord> getSearchWords(long lastUpdateTime) { // TODO: 从数据库中取出更新时间>lastUpdateTime的数据 return null; } }