作者:AmyliaY

出自:Doocs开源社区

原文:my.oschina.net/doocs/blog/4549852


MyBatis 中的缓存分为一级缓存、二级缓存,但在本质上是相同的,它们使用的都是 Cache 接口的实现。在这篇文章里,我们就来分析 Cache 接口以及多个实现类的具体实现。

1、Cache 组件

MyBatis 中缓存模块相关的代码位于 org.apache.ibatis.cache 包 下,其中 Cache 接口 是缓存模块中最核心的接口,它定义了所有缓存的基本行为。

public interface Cache {/*** 获取当前缓存的 Id*/String getId();/*** 存入缓存的 key 和 value,key 一般为 CacheKey对象*/void putObject(Object key, Object value);/*** 根据 key 获取缓存值*/Object getObject(Object key);/*** 删除指定的缓存项*/Object removeObject(Object key);/*** 清空缓存*/void clear();/*** 获取缓存的大小*/int getSize();/*** !!!!!!!!!!!!!!!!!!!!!!!!!!* 获取读写锁,可以看到,这个接口方法提供了默认的实现!!* 这是 Java8 的新特性!!只是平时开发时很少用到!!!* !!!!!!!!!!!!!!!!!!!!!!!!!!*/default ReadWriteLock getReadWriteLock() {return null;}
}

如下图所示,Cache 接口 的实现类有很多,但大部分都是装饰器,只有 PerpetualCache 提供了 Cache 接口 的基本实现。

PerpetualCache

PerpetualCache(Perpetual:永恒的,持续的)在缓存模块中扮演着被装饰的角色,其实现比较简单,底层使用 HashMap 记录缓存项,也是通过该 HashMap 对象 的方法实现的 Cache 接口 中定义的相应方法。

public class PerpetualCache implements Cache {// Cache对象 的唯一标识private final String id;// 其所有的缓存功能实现,都是基于 JDK 的 HashMap 提供的方法private Map<Object, Object> cache = new HashMap<>();public PerpetualCache(String id) {this.id = id;}@Overridepublic String getId() {return id;}@Overridepublic int getSize() {return cache.size();}@Overridepublic void putObject(Object key, Object value) {cache.put(key, value);}@Overridepublic Object getObject(Object key) {return cache.get(key);}@Overridepublic Object removeObject(Object key) {return cache.remove(key);}@Overridepublic void clear() {cache.clear();}/*** 其重写了 Object 中的 equals() 和 hashCode()方法,两者都只关心 id字段*/@Overridepublic boolean equals(Object o) {if (getId() == null) {throw new CacheException("Cache instances require an ID.");}if (this == o) {return true;}if (!(o instanceof Cache)) {return false;}Cache otherCache = (Cache) o;return getId().equals(otherCache.getId());}@Overridepublic int hashCode() {if (getId() == null) {throw new CacheException("Cache instances require an ID.");}return getId().hashCode();}
}

下面来看一下 cache.decorators 包 下提供的装饰器,它们都直接实现了 Cache 接口,扮演着装饰器的角色。这些装饰器会在 PerpetualCache 的基础上提供一些额外的功能,通过多个组合后满足一个特定的需求。

BlockingCache

BlockingCache 是阻塞版本的缓存装饰器,它会保证只有一个线程到数据库中查找指定 key 对应的数据。

public class BlockingCache implements Cache {// 阻塞超时时长private long timeout;// 持有的被装饰者private final Cache delegate;// 每个 key 都有其对应的 ReentrantLock锁对象private final ConcurrentHashMap<Object, ReentrantLock> locks;// 初始化 持有的持有的被装饰者 和 锁集合public BlockingCache(Cache delegate) {this.delegate = delegate;this.locks = new ConcurrentHashMap<>();}
}

假设 线程 A 在 BlockingCache 中未查找到 keyA 对应的缓存项时,线程 A 会获取 keyA 对应的锁,这样,线程 A 在后续查找 keyA 时,其它线程会被阻塞。

// 根据 key 获取锁对象,然后上锁private void acquireLock(Object key) {// 获取 key 对应的锁对象Lock lock = getLockForKey(key);// 获取锁,带超时时长if (timeout > 0) {try {boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);if (!acquired) { // 超时,则抛出异常throw new CacheException("Couldn't get a lock in " + timeout + " for the key " +  key + " at the cache " + delegate.getId());}} catch (InterruptedException e) {// 如果获取锁失败,则阻塞一段时间throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);}} else {// 上锁lock.lock();}}private ReentrantLock getLockForKey(Object key) {// Java8 新特性,Map系列类 中新增的方法// V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)// 表示,若 key 对应的 value 为空,则将第二个参数的返回值存入该 Map集合 并返回return locks.computeIfAbsent(key, k -> new ReentrantLock());}

假设 线程 A 从数据库中查找到 keyA 对应的结果对象后,将结果对象放入到 BlockingCache 中,此时 线程 A 会释放 keyA 对应的锁,唤醒阻塞在该锁上的线程。其它线程即可从 BlockingCache 中获取 keyA 对应的数据,而不是再次访问数据库。

 @Overridepublic void putObject(Object key, Object value) {try {// 存入 key 和其对应的缓存项delegate.putObject(key, value);} finally {// 最后释放锁releaseLock(key);}}private void releaseLock(Object key) {ReentrantLock lock = locks.get(key);// 锁是否被当前线程持有if (lock.isHeldByCurrentThread()) {// 是,则释放锁lock.unlock();}}

FifoCache 和 LruCache

在很多场景中,为了控制缓存的大小,系统需要按照一定的规则清理缓存。FifoCache 是先入先出版本的装饰器,当向缓存添加数据时,如果缓存项的个数已经达到上限,则会将缓存中最老(即最早进入缓存)的缓存项删除。

public class FifoCache implements Cache {// 被装饰对象private final Cache delegate;// 用一个 FIFO 的队列记录 key 的顺序,其具体实现为 LinkedListprivate final Deque<Object> keyList;// 决定了缓存的容量上限private int size;// 国际惯例,通过构造方法初始化自己的属性,缓存容量上限默认为 1024个public FifoCache(Cache delegate) {this.delegate = delegate;this.keyList = new LinkedList<>();this.size = 1024;}@Overridepublic String getId() {return delegate.getId();}@Overridepublic int getSize() {return delegate.getSize();}public void setSize(int size) {this.size = size;}@Overridepublic void putObject(Object key, Object value) {// 存储缓存项之前,先在 keyList 中注册cycleKeyList(key);// 存储缓存项delegate.putObject(key, value);}private void cycleKeyList(Object key) {// 在 keyList队列 中注册要添加的 keykeyList.addLast(key);// 如果注册这个 key 会超出容积上限,则把最老的一个缓存项清除掉if (keyList.size() > size) {Object oldestKey = keyList.removeFirst();delegate.removeObject(oldestKey);}}@Overridepublic Object getObject(Object key) {return delegate.getObject(key);}@Overridepublic Object removeObject(Object key) {return delegate.removeObject(key);}// 除了清理缓存项,还要清理 key 的注册列表@Overridepublic void clear() {delegate.clear();keyList.clear();}
}

LruCache 是按照"近期最少使用算法"(Least Recently Used, LRU)进行缓存清理的装饰器,在需要清理缓存时,它会清除最近最少使用的缓存项。

public class LruCache implements Cache {// 被装饰者private final Cache delegate;// 这里使用的是 LinkedHashMap,它继承了 HashMap,但它的元素是有序的private Map<Object, Object> keyMap;// 最近最少被使用的缓存项的 keyprivate Object eldestKey;// 国际惯例,构造方法中进行属性初始化public LruCache(Cache delegate) {this.delegate = delegate;// 这里初始化了 keyMap,并定义了 eldestKey 的取值规则setSize(1024);}public void setSize(final int size) {// 初始化 keyMap,同时指定该 Map 的初始容积及加载因子,第三个参数true 表示 该LinkedHashMap// 记录的顺序是 accessOrder,即,LinkedHashMap.get()方法 会改变其中元素的顺序keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {private static final long serialVersionUID = 4267176411845948333L;// 当调用 LinkedHashMap.put()方法 时,该方法会被调用@Overrideprotected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {boolean tooBig = size() > size;if (tooBig) {// 当已达到缓存上限,更新 eldestKey字段,后面将其删除eldestKey = eldest.getKey();}return tooBig;}};}// 存储缓存项@Overridepublic void putObject(Object key, Object value) {delegate.putObject(key, value);// 记录缓存项的 key,超出容量则清除最久未使用的缓存项cycleKeyList(key);}private void cycleKeyList(Object key) {keyMap.put(key, key);// eldestKey 不为空,则表示已经达到缓存上限if (eldestKey != null) {// 清除最久未使用的缓存delegate.removeObject(eldestKey);// 制空eldestKey = null;}}@Overridepublic Object getObject(Object key) {// 访问 key元素 会改变该元素在 LinkedHashMap 中的顺序keyMap.get(key); //touchreturn delegate.getObject(key);}@Overridepublic String getId() {return delegate.getId();}@Overridepublic int getSize() {return delegate.getSize();}@Overridepublic Object removeObject(Object key) {return delegate.removeObject(key);}@Overridepublic void clear() {delegate.clear();keyMap.clear();}
}

SoftCache 和 WeakCache

在分析 SoftCache 和 WeakCache 实现之前,我们再温习一下 Java 提供的 4 种引用类型,强引用 StrongReference、软引用 SoftReference、弱引用 WeakReference 和虚引用 PhantomReference。

•强引用 平时用的最多的,如 Object obj = new Object(),新建的 Object 对象 就是被强引用的。如果一个对象被强引用,即使是 JVM 内存空间不足,要抛出 OutOfMemoryError 异常,GC 也绝不会回收该对象。•软引用 仅次于强引用的一种引用,它使用类 SoftReference 来表示。当 JVM 内存不足时,GC 会回收那些只被软引用指向的对象,从而避免内存溢出。软引用适合引用那些可以通过其他方式恢复的对象,例如, 数据库缓存中的对象就可以从数据库中恢复,所以软引用可以用来实现缓存,下面要介绍的 SoftCache 就是通过软引用实现的。
另外,由于在程序使用软引用之前的某个时刻,其所指向的对象可能己经被 GC 回收掉了,所以通过 Reference.get()方法 来获取软引用所指向的对象时,总是要通过检查该方法返回值是否为 null,来判断被软引用的对象是否还存活。•弱引用 弱引用使用 WeakReference 表示,它不会阻止所引用的对象被 GC 回收。在 JVM 进行垃圾回收时,如果指向一个对象的所有引用都是弱引用,那么该对象会被回收。所以,只被弱引用所指向的对象,其生存周期是 两次 GC 之间 的这段时间,而只被软引用所指向的对象可以经历多次 GC,直到出现内存紧张的情况才被回收。•虚引用 最弱的一种引用类型,由类 PhantomReference 表示。虚引用可以用来实现比较精细的内存使用控制,但很少使用。•引用队列(ReferenceQueue ) 很多场景下,我们的程序需要在一个对象被 GC 时得到通知,引用队列就是用于收集这些信息的队列。在创建 SoftReference 对象 时,可以为其关联一个引用队列,当 SoftReference 所引用的对象被 GC 时, JVM 就会将该 SoftReference 对象 添加到与之关联的引用队列中。当需要检测这些通知信息时,就可以从引用队列中获取这些 SoftReference 对象。不仅是 SoftReference,弱引用和虚引用都可以关联相应的队列。

现在来看一下 SoftCache 的具体实现。

public class SoftCache implements Cache {// 这里使用了 LinkedList 作为容器,在 SoftCache 中,最近使用的一部分缓存项不会被 GC// 这是通过将其 value 添加到 hardLinksToAvoidGarbageCollection集合 实现的(即,有强引用指向其value)private final Deque<Object> hardLinksToAvoidGarbageCollection;// 引用队列,用于记录已经被 GC 的缓存项所对应的 SoftEntry对象private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;// 持有的被装饰者private final Cache delegate;// 强连接的个数,默认为 256private int numberOfHardLinks;// 构造方法进行属性的初始化public SoftCache(Cache delegate) {this.delegate = delegate;this.numberOfHardLinks = 256;this.hardLinksToAvoidGarbageCollection = new LinkedList<>();this.queueOfGarbageCollectedEntries = new ReferenceQueue<>();}private static class SoftEntry extends SoftReference<Object> {private final Object key;SoftEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {// 指向 value 的引用是软引用,并且关联了 引用队列super(value, garbageCollectionQueue);// 强引用this.key = key;}}@Overridepublic void putObject(Object key, Object value) {// 清除已经被 GC 的缓存项removeGarbageCollectedItems();// 添加缓存delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));}private void removeGarbageCollectedItems() {SoftEntry sv;// 遍历 queueOfGarbageCollectedEntries集合,清除已经被 GC 的缓存项 valuewhile ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {delegate.removeObject(sv.key);}}@Overridepublic Object getObject(Object key) {Object result = null;@SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache// 用一个软引用指向 key 对应的缓存项SoftReference<Object> softReference = (SoftReference<Object>) delegate.getObject(key);// 检测缓存中是否有对应的缓存项if (softReference != null) {// 获取 softReference 引用的 valueresult = softReference.get();// 如果 softReference 引用的对象已经被 GC,则从缓存中清除对应的缓存项if (result == null) {delegate.removeObject(key);} else {synchronized (hardLinksToAvoidGarbageCollection) {// 将缓存项的 value 添加到 hardLinksToAvoidGarbageCollection集合 中保存hardLinksToAvoidGarbageCollection.addFirst(result);// 如果 hardLinksToAvoidGarbageCollection 的容积已经超过 numberOfHardLinks// 则将最老的缓存项从 hardLinksToAvoidGarbageCollection 中清除,FIFOif (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {hardLinksToAvoidGarbageCollection.removeLast();}}}}return result;}@Overridepublic Object removeObject(Object key) {// 清除指定的缓存项之前,也会先清理被 GC 的缓存项removeGarbageCollectedItems();return delegate.removeObject(key);}@Overridepublic void clear() {synchronized (hardLinksToAvoidGarbageCollection) {// 清理强引用集合hardLinksToAvoidGarbageCollection.clear();}// 清理被 GC 的缓存项removeGarbageCollectedItems();// 清理最底层的缓存项delegate.clear();}@Overridepublic String getId() {return delegate.getId();}@Overridepublic int getSize() {removeGarbageCollectedItems();return delegate.getSize();}public void setSize(int size) {this.numberOfHardLinks = size;}
}

WeakCache 的实现与 SoftCache 基本类似,唯一的区别在于其中使用 WeakEntry(继承了 WeakReference)封装真正的 value 对象,其他实现完全一样。

另外,还有 ScheduledCache、LoggingCache、SynchronizedCache、SerializedCache 等。ScheduledCache 是周期性清理缓存的装饰器,它的 clearInterval 字段 记录了两次缓存清理之间的时间间隔,默认是一小时,lastClear 字段 记录了最近一次清理的时间戳。ScheduledCache 的 getObject()、putObject()、removeObject() 等核心方法,在执行时都会根据这两个字段检测是否需要进行清理操作,清理操作会清空缓存中所有缓存项。

LoggingCache 在 Cache 的基础上提供了日志功能,它通过 hit 字段 和 request 字段 记录了 Cache 的命中次数和访问次数。在 LoggingCache.getObject()方法 中,会统计命中次数和访问次数 这两个指标,井按照指定的日志输出方式输出命中率。

SynchronizedCache 通过在每个方法上添加 synchronized 关键字,为 Cache 添加了同步功能,有点类似于 JDK 中 Collections 的 SynchronizedCollection 内部类。

SerializedCache 提供了将 value 对象 序列化的功能。SerializedCache 在添加缓存项时,会将 value 对应的 Java 对象 进行序列化,井将序列化后的 byte[]数组 作为 value 存入缓存 。SerializedCache 在获取缓存项时,会将缓存项中的 byte[]数组 反序列化成 Java 对象。不使用 SerializedCache 装饰器 进行装饰的话,每次从缓存中获取同一 key 对应的对象时,得到的都是同一对象,任意一个线程修改该对象都会影响到其他线程,以及缓存中的对象。而使用 SerializedCache 每次从缓存中获取数据时,都会通过反序列化得到一个全新的对象。SerializedCache 使用的序列化方式是 Java 原生序列化。

2、CacheKey

在 Cache 中唯一确定一个缓存项,需要使用缓存项的 key 进行比较,MyBatis 中因为涉及 动态 SQL 等多方面因素, 其缓存项的 key 不能仅仅通过一个 String 表示,所以 MyBatis 提供了 CacheKey 类 来表示缓存项的 key,在一个 CacheKey 对象 中可以封装多个影响缓存项的因素。CacheKey 中可以添加多个对象,由这些对象共同确定两个 CacheKey 对象 是否相同。

public class CacheKey implements Cloneable, Serializable {private static final long serialVersionUID = 1146682552656046210L;public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();private static final int DEFAULT_MULTIPLYER = 37;private static final int DEFAULT_HASHCODE = 17;// 参与计算hashcode,默认值DEFAULT_MULTIPLYER = 37private final int multiplier;// 当前CacheKey对象的hashcode,默认值DEFAULT_HASHCODE = 17private int hashcode;// 校验和private long checksum;private int count;// 由该集合中的所有元素 共同决定两个CacheKey对象是否相同,一般会使用一下四个元素// MappedStatement的id、查询结果集的范围参数(RowBounds的offset和limit)// SQL语句(其中可能包含占位符"?")、SQL语句中占位符的实际参数private List<Object> updateList;// 构造方法初始化属性public CacheKey() {this.hashcode = DEFAULT_HASHCODE;this.multiplier = DEFAULT_MULTIPLYER;this.count = 0;this.updateList = new ArrayList<>();}public CacheKey(Object[] objects) {this();updateAll(objects);}public void update(Object object) {int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);// 重新计算count、checksum和hashcode的值count++;checksum += baseHashCode;baseHashCode *= count;hashcode = multiplier * hashcode + baseHashCode;// 将object添加到updateList集合updateList.add(object);}public int getUpdateCount() {return updateList.size();}public void updateAll(Object[] objects) {for (Object o : objects) {update(o);}}/*** CacheKey重写了 equals() 和 hashCode()方法,这两个方法使用上面介绍* 的 count、checksum、hashcode、updateList 比较两个 CacheKey对象 是否相同*/@Overridepublic boolean equals(Object object) {// 如果为同一对象,直接返回 trueif (this == object) {return true;}// 如果 object 都不是 CacheKey类型,直接返回 falseif (!(object instanceof CacheKey)) {return false;}// 类型转换一下final CacheKey cacheKey = (CacheKey) object;// 依次比较 hashcode、checksum、count,如果不等,直接返回 falseif (hashcode != cacheKey.hashcode) {return false;}if (checksum != cacheKey.checksum) {return false;}if (count != cacheKey.count) {return false;}// 比较 updateList 中的元素是否相同,不同直接返回 falsefor (int i = 0; i < updateList.size(); i++) {Object thisObject = updateList.get(i);Object thatObject = cacheKey.updateList.get(i);if (!ArrayUtil.equals(thisObject, thatObject)) {return false;}}return true;}@Overridepublic int hashCode() {return hashcode;}@Overridepublic String toString() {StringJoiner returnValue = new StringJoiner(":");returnValue.add(String.valueOf(hashcode));returnValue.add(String.valueOf(checksum));updateList.stream().map(ArrayUtil::toString).forEach(returnValue::add);return returnValue.toString();}@Overridepublic CacheKey clone() throws CloneNotSupportedException {CacheKey clonedCacheKey = (CacheKey) super.clone();clonedCacheKey.updateList = new ArrayList<>(updateList);return clonedCacheKey;}
}

apache缓存清理_深挖 Mybatis 源码:缓存模块相关推荐

  1. java你画我猜源码_为什么看到Mybatis源码就感到烦躁?

    背景 最近,听到很多吐槽:看到源码,心中就感到十分纠结.特别烦恼. 为什么纠结? 因为面试的时候,面试官很喜欢问:你看过什么框架源码?JDK源码也行. 这时候,如果回答没有看过,虽然没让你立马回去等通 ...

  2. Mybatis源码日志模块分析

    看源码需要先下载源码,可以去Mybatis的github上的仓库进行下载,Mybatis 这次就先整理一下日志这一块的源码分析,这块相对来说比较简单而且这个模块是Mybatis的基础模块. 之前的文章 ...

  3. 手把手带你阅读Mybatis源码(三)缓存篇

    点击上方"Java知音",选择"置顶公众号" 技术文章第一时间送达! 前言 大家好,这一篇文章是MyBatis系列的最后一篇文章,前面两篇文章:手把手带你阅读M ...

  4. 【MyBatis源码解析】MyBatis一二级缓存

    MyBatis缓存 我们知道,频繁的数据库操作是非常耗费性能的(主要是因为对于DB而言,数据是持久化在磁盘中的,因此查询操作需要通过IO,IO操作速度相比内存操作速度慢了好几个量级),尤其是对于一些相 ...

  5. Mybatis源码学习笔记之Mybatis二级缓存

    简介   Mybatis一级缓存是会话级的缓存,而二级缓存则是应用级别的缓存,默认关闭,二级缓存使用不慎可能会导致脏读. 开启方式(SpringBoot+Mybatis)   application. ...

  6. mybatis源码考究二(sqlsession线程安全和缓存失效)

    mybatis源码考究二 1.mybatis整合spring解决sqlsession线程安全问题 2.mybatis整合spring一级缓存失效问题 mybatis结合spring使用 1.项目依赖 ...

  7. MyBatis 源码分析 - 缓存原理

    1.简介 在 Web 应用中,缓存是必不可少的组件.通常我们都会用 Redis 或 memcached 等缓存中间件,拦截大量奔向数据库的请求,减轻数据库压力.作为一个重要的组件,MyBatis 自然 ...

  8. delphi查看源码版本_[Mybatis]-IDEA导入Mybatis源码

    该系列文章针对 Mybatis 3.5.1 版本 一.下载 Mybatis 源码 step1.下载 Mybatis-3.5.1 源码 Mybatis 源码仓库地址 下载版本信息如下: 下载后进行解压, ...

  9. 封装成jar包_通用源码阅读指导mybatis源码详解:io包

    io包 io包即输入/输出包,负责完成 MyBatis中与输入/输出相关的操作. 说到输入/输出,首先想到的就是对磁盘文件的读写.在 MyBatis的工作中,与磁盘文件的交互主要是对 xml配置文件的 ...

最新文章

  1. sqlserver中的数据类型[转]
  2. SAP Hybris Enterprise Commerce Platform ECP和SAP CRM架构比较
  3. 入门 Kotlin 和 Java 混合开发
  4. 九大类背包问题专题1---01背包问题(二维和优化一维法附代码)
  5. ubuntu 16.04安装并启动openssh
  6. jmeter笔记02
  7. 关于打印 毕业设计资料
  8. Ubuntu图形化数据库连接工具
  9. android设置adb环境变量,如何配置android的adb环境变量
  10. 人工智能专业世界排名第一的大学,2022最新
  11. 一文彻底搞懂Mybatis系列(二)之mybatis事务管理机制深度剖析
  12. 编程数学-∑(求和符号)-Sigma
  13. Linux操作系统平台
  14. 金笛JDMAIL邮件服务器证券行业邮件归档解决方案
  15. netkeeper客户端 Linux,netkeeper_for_linux
  16. 第三方支付频频被罚款,市场驱动下或是故意为之?
  17. fi选项 电脑没有连接wi,没有电脑怎么设置无线路由器?
  18. 浮点运算单元FPU能给电机控制带来什么?
  19. 阿里easyexcel通过模板导出excel
  20. 【云原生】Docker 架构及工作原理

热门文章

  1. 前端学习(479):html简介
  2. 第一百五十一期:最新计算机技能需求排名出炉:Python仅排第三,第一你猜得到吗?
  3. 实例5:python
  4. php 连接多个数据出错,php连接多个ip信息数据库
  5. swoole 协程channel乱测
  6. NetCore 依赖注入之服务之间的依赖关系
  7. Registered Nurse in the US
  8. 关于我的代码在课上第一时间没有运行出来这件事
  9. Linux 编译安装BIND
  10. Python map/reduce