写在前面

聊一聊MyBatis是如何使用装饰者模式的,顺便回顾下缓存的相关知识,可以看看右侧目录一览内容概述。

装饰者模式

这里就不了它的概念了,总结下就是套娃。利用组合的方式将装饰器组合进来,增强共同的抽象方法(与代理很类似但是又更灵活)

MyBatis缓存

回忆下传统手艺

  <!-- 先进先出,60秒刷新一次,可存储512个引用,返回对象只读,不同线程中的调用者之间修改会导致冲突 -->
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

粗略回顾下MyBatis缓存

一级缓存

MyBatis的一级缓存存在于SqlSession的生命周期中,在同一个SqlSession中查询时,MyBatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map对象中。如果同一个SqlSession中执行的方法和参数完全一致,那么通过算法会生成相同键值,当Map缓存对象中已经存在该键值时,则会返回缓存中的对象。

默认开启

二级缓存

MyBatis的二级缓存非常强大,它不同于一级缓存只存在于SqlSession的生命周期中,而是可以理解为存在于SqlSessionFactory的生命周期中。

默认不开启,需要如下配置后开启全局配置,再在对应的Mapper.xml中添加“传统手艺”-标签

<settings>
<setting name = "cacheEnabled" value="true"/>
</settings> <cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

另一种开启方式-注解

@CacheNamespace(
eviction = FifoCache.class,
flushInterval = 60000,
size = 512,
readWrite = true
)
public interface RoleMapper {
// 接口方法
}
  • eviction(收回策略)

    • LRU(最近最少使用的):移除长时间不使用的对象,这是默认值
    • FIFO(先进先出):按对象进入缓存的顺序来移除它们
    • SOFT(软引用):移除基于垃圾回收器状态和软引用规则的对象
    • WEAK(弱引用):更积极地移除基于垃圾收集器状态和弱引用规则的对象
  • flushInterval(刷新间隔)
  • size(引用数目)
  • readOnly(只读)只读的缓存会给所有调用者返回缓存的相同实例,因此这些对象不能被修改,这提供了很重要的性能优势。可读写的缓存会通过序列化返回缓存对象的拷贝,这种方式会慢一些,但是安全,因此默认是false

集成第三方缓存

MyBatis还支持通过“type”来集成第三方缓存,如下就是集成了Redis缓存,这样就从本地缓存跳跃到了分布式缓存了。

<mapper namespace="xxx.xxx.xxx.mapper.RoleMapper">
<!-- 集成Redis缓存-->
<cache type="org.mybatis.caches.redis.RedisCache" />
</mapper>

二级缓存的问题-脏数据

二级缓存虽然能提高应用效率,减轻数据库服务器的压力,但是如果使用不当,很容易产生脏数据

MyBatis的二级缓存是和命名空间绑定的,所以通常情况下每一个Mapper映射文件都拥有自己的二级缓存,不同Mapper的二级缓存互不影响。在常见的数据库操作中,多表联合查询非常常见,由于关系型数据库的设计,使得很多时候需要关联多个表才能获得想要的数据。在关联多表查询时肯定会将查询放到某个命名空间下的映射文件中,这样一个多表的查询就会缓存在该命名空间的二级缓存中。涉及这些表的增删改操作通常不在一个映射文件中,它们的命名空间不同,因此当有数据变化时,多表查询的缓存未必会被清空,这种情况下就会产生脏数据。

基于MyBatis缓存机制结合源码解析装饰器模式

Cache接口:

Cache核心方法:

  • putObject
  • getObject
  • removeObject

DEMO-实战使用MyBatis的装饰者模式

    public static void main(String[] args) {
final String cacheKey = "cache";
final Cache cache = new LoggingCache(new BlockingCache(new PerpetualCache(cacheKey)));
Object cacheValue = cache.getObject(cacheKey);
if (Objects.isNull(cacheValue)) {
log.debug("缓存未命中 >>>>>>>>> key:[{}]", cacheKey);
cache.putObject(cacheKey, "MyCacheValue");
} cacheValue = cache.getObject(cacheKey);
log.debug("缓存命中 >>>>>>>>> key:[{}],value:[{}]", cacheKey, cacheValue);
}

如代码所示,是不是看到了“装饰者模式”的影子了,在构造函数中疯狂套娃。使用的是MyBatis的API,给基本缓存组件装饰了“日志打印”、“阻塞“的能力。

结果演示:



可以看到,LogginCache在读缓存的时候还会打印出缓存命中率。 好了,接下来进入正题,看看其他缓存是怎么实现的吧。以下源码基于MyBatis3.4.5

PerpetualCache

  private final Map<Object, Object> cache = new HashMap<>();

  @Override
public void putObject(Object key, Object value) {
cache.put(key, value);
} @Override
public Object getObject(Object key) {
return cache.get(key);
} @Override
public Object removeObject(Object key) {
return cache.remove(key);
}

这是MyBatis的基础缓存,套娃的基本得有它,它的核心就是个HashMap来作为缓存容器,其实现的Cache接口的几个核心方法也都是委托给了HashMap去做。

FifoCache

一个支持先进先出的缓存策略的MyBatisCache

  private final Cache delegate;
//维护一个key的双端队列
private final Deque<Object> keyList;
private int size; public FifoCache(Cache delegate) {
//通过构造函数,将Cache组合进来,取名”委托“
this.delegate = delegate;
this.keyList = new LinkedList<>();
this.size = 1024;
} @Override
public void putObject(Object key, Object value) {
//先走自己的增强
cycleKeyList(key);
//真实的写缓存交给”委托“去做
delegate.putObject(key, value);
} @Override
public Object getObject(Object key) {
return delegate.getObject(key);
} @Override
public Object removeObject(Object key) {
return delegate.removeObject(key);
} private void cycleKeyList(Object key) {
//将新写的缓存key添加到双端队列末尾
keyList.addLast(key);
// 如果key的大小大于了1024(构造函数中默认赋值1024)则会移除最早添加的缓存
// 1. 移除自身维护的key队列的队头 2.委托给“委托”去真实删除队头缓存对象
if (keyList.size() > size) {
Object oldestKey = keyList.removeFirst();
delegate.removeObject(oldestKey);
}
}

以上就是MyBatis先进先出缓存的实现了,FifoCache维护了key的双端队列,每次写缓存的时候会判断大小如果大于阈值则会先移除队头的key,再委托给组合进来的Cache来删除对应缓存操作,完成“先进先出”的增强(装饰)

LruCache

一个支持LRU(Least Recently Used ,最近最少使用)缓存策略的MyBatisCache

回忆下缓存策略

  • LRU:Least Recently Used,最近最少使用
  • LFU:Least Frequently Used,最近不常被使用

LRU 算法有一个缺点,比如说很久没有使用的一个键值,如果最近被访问了一次,那么即使它是使用次数最少的缓存,它也不会被淘汰;而 LFU 算法解决了偶尔被访问一次之后,数据就不会被淘汰的问题,它是根据总访问次数来淘汰数据的,其核心思想是“如果数据过去被访问多次,那么将来它被访问次数也会比较多”。因此 LFU 可以理解为比 LRU 更加合理的淘汰算法。

回忆下LinkedHashMap的核心机制-LRU

LinkedHashMap相比HashMap多了两个节点,before,after这样就能够维护节点之间的顺序了。

我们看看LinkedHashMap的get方法,它内部有LinkedHashMap开启LRU机制的秘密。

    public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder) // 为true则会执行afterNodeAccess(将节点移动到队尾)
afterNodeAccess(e);
return e.value;
} void afterNodeAccess(Node<K,V> e) { // move node to last (官方注释 言简意赅 -> 将节点移动到队尾)
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}

那么这个accessOrder变量是怎么维护的呢?看代码

    public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}

你会发现,LinkedHashMap有这么一个构造函数,第三个参数便是accessOrder,所以决定是否开启LRU是你在运行时传参决定的!开启后则会在每次读取键值对之后将读取的节点移动至队尾,那么队头就是最近最少使用的了,队尾就是刚刚使用的了,当需要删除最近最少使用的节点的时候,直接删除队头的即可。

回忆下LinkedHashMap的核心方法-removeEldestEntry

LinkedHashMap是一个有顺序的HashMap,它可以使得你的k,v能够按照某种顺序写入和读取,它的核心方法removeEldestEntry功不可没。

在HashMap新增k,v之后会回调一个方法“afterNodeInsertion”,这个方法在HashMap中是一个空实现(俗称钩子方法),它的子类LinkedHashMap重写了它,代码如下。

    void afterNodeInsertion(boolean evict) { // possibly remove eldest     这是官方注释,言简意赅(可能会删除老key)
LinkedHashMap.Entry<K,V> first;
//前面的短路方法不管,我们关注removeEldestEntry方法 -> 如果该方法也返回true,则会走方法体中的removeNode方法(删除first节点的元素)。
// 当开启LinkedHashMap的LRU模式,则队头的元素是“最近最少使用的元素”,因为每次读取k,v后都会将元素调整至队尾,所以队头的元素是“最近最少使用的元素“
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}

进入正题

  private final Cache delegate;
// 维护一个key和value都是缓存key的map
private Map<Object, Object> keyMap;
//最近最少使用的Key
private Object eldestKey; public LruCache(Cache delegate) {
//通过构造函数,将Cache组合进来,取名”委托“
this.delegate = delegate;
//初始化keyMap(重要)
setSize(1024);
} public void setSize(final int size) {
// 构造函数第三个参数传递true(accessOrder),如上所述将开启LRU模式
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L; // 重写了LinkedHashMap的方法
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
// 大小超过阈值,将队头(最近最少使用)的key更新至自身维护的"eldestKey" (重要)
eldestKey = eldest.getKey();
}
return tooBig;
}
};
} @Override
public void putObject(Object key, Object value) {
// 委托写入缓存
delegate.putObject(key, value);
// 删除最近最少使用的缓存
cycleKeyList(key);
} @Override
public Object getObject(Object key) {
keyMap.get(key); // touch
return delegate.getObject(key);
} @Override
public Object removeObject(Object key) {
return delegate.removeObject(key);
} private void cycleKeyList(Object key) {
// 因为重写了LinkedHashMap的removeEldestEntry方法,如上所述,超过阈值后eldestKey指向的就是最近最少使用的key
keyMap.put(key, key);
if (eldestKey != null) {
// 委托移除最近最少使用的缓存
delegate.removeObject(eldestKey);
// 置空
eldestKey = null;
}
}

以上就是MyBatis中的LRU缓存的机制了,自身维护了一个LinkedHashMap,开启了LRU机制,重写了removeEldestEntry方法,当大小触发阈值的时候维护最近最少使用的元素key,委托给组合进来的Cache对象移除,整个流程下来就使得被装饰着有了LRU的增强。

SoftCache

一个软引用的MyBatisCache

弱引用

弱引用比强引用稍弱一些。当JVM内存不足时,GC才会回收那些只被软引用指向的对象,从而避免OutOfMemoryError。当GC将只被软引用指向的对象全部回收之后,内存依然不足时,JVM才会抛出OutOfMemoryError。(这一特性非常适合做缓存,毕竟最终数据源在DB,还能保护JVM进程)

  // 维护最近经常使用的缓存数据,该集合会使用强引用指向其中的每个缓存Value,防止被GC回收
private final Deque<Object> hardLinksToAvoidGarbageCollection;
//与SortEntry对象关联,用于记录已经被回收的缓存条目
private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
private final Cache delegate;
//强引用的个数,默认256。即有256个热点数据无法直接被GC回收
private int numberOfHardLinks; public SoftCache(Cache delegate) {
this.delegate = delegate;
this.numberOfHardLinks = 256;
this.hardLinksToAvoidGarbageCollection = new LinkedList<Object>();
this.queueOfGarbageCollectedEntries = new ReferenceQueue<Object>();
} @Override
public void putObject(Object key, Object value) {
// 同步删除已经被GC回收的Value
removeGarbageCollectedItems();
delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
} private static class SoftEntry extends SoftReference<Object> {
private final Object key; SoftEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {
// 关联引用队列。
// 当SoftReference指向的对象被回收的时候,JVM就会将这个SoftReference作为通知,添加到与其关联的引用队列
super(value, garbageCollectionQueue);
this.key = key;
}
} @Override
public Object getObject(Object key) {
Object result = null;
@SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache
SoftReference<Object> softReference = (SoftReference<Object>) delegate.getObject(key); // 委托获取缓存
if (softReference != null) {
result = softReference.get();
if (result == null) {
// 重要的一步!判断Value是否为空,为空则表示弱引用指向的对象已经被GC回收了,就需要同步删除该缓存。
delegate.removeObject(key);
} else {
// See #586 (and #335) modifications need more than a read lock
// 读取缓存后,维护“强引用”的数据。
synchronized (hardLinksToAvoidGarbageCollection) {
hardLinksToAvoidGarbageCollection.addFirst(result); // 将缓存添加进强引用队列(热点数据)
if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
hardLinksToAvoidGarbageCollection.removeLast(); // 维护队列个数
}
}
}
}
return result;
} @Override
public Object removeObject(Object key) {
removeGarbageCollectedItems(); // 删除被GC回收的Value
return delegate.removeObject(key); // 委托删除缓存
} private void removeGarbageCollectedItems() {
SoftEntry sv;
// 引用关联的队列如果有值,则说明有被GC回收的Value
while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {
delegate.removeObject(sv.key);
}
}

WeakCache

一个弱引用的MyBatisCache

与弱引用类似(基本相同),不过多介绍了。

弱引用

弱引用比软引用的引用强度还要弱。弱引用可以引用一个对象,但无法阻止这个对象被GC回收,也就是说,在JVM进行垃圾回收的时候,若发现某个对象只有一个弱引用指向它,那么这个对象会被GC立刻回收。(即遇GC比死,存活的时间为两次GC之间)

  // Entry继承的是WeakReference。
// 其他内容参考弱引用Cache
private static class WeakEntry extends WeakReference<Object> {
private final Object key; private WeakEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {
super(value, garbageCollectionQueue);
this.key = key;
}
}

LoggingCache

一个支持打印Debug级别的缓存命中率的MyBatisCache

  // 日志打印的log对象
private final Log log;
private final Cache delegate;
// 请求数
protected int requests = 0;
// 缓存命中数
protected int hits = 0; public LoggingCache(Cache delegate) {
//通过构造函数,将Cache组合进来,取名”委托“
this.delegate = delegate;
//log通过缓存id作为表示
this.log = LogFactory.getLog(getId());
} @Override
public void putObject(Object key, Object object) {
delegate.putObject(key, object);
} @Override
public Object getObject(Object key) {
requests++; // 请求数增加
final Object value = delegate.getObject(key);
if (value != null) {
hits++; // 缓存命中,命中数增加
}
if (log.isDebugEnabled()) {
log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio()); // 打印缓存命中率
}
return value;
} @Override
public Object removeObject(Object key) {
return delegate.removeObject(key);
} private double getHitRatio() {
// 计算缓存命中率
return (double) hits / (double) requests;
}

LoggingCache使得缓存读取的时候能够有缓存命中率的日志打印,挺实用的增强。

BlockingCache

一个支持阻塞的MyBatisCache

  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<Object, ReentrantLock>();
} @Override
public void putObject(Object key, Object value) {
try {
delegate.putObject(key, value); // 委托写入缓存
} finally {
releaseLock(key); // 释放锁
}
} @Override
public Object getObject(Object key) {
acquireLock(key); // 尝试获取锁
Object value = delegate.getObject(key);
if (value != null) {
releaseLock(key); // 获取到缓存后 释放锁
}
return value;
} @Override
public Object removeObject(Object key) {
// despite of its name, this method is called only to release locks
releaseLock(key); // 释放锁
return null;
} private void acquireLock(Object key) {
Lock lock = getLockForKey(key); // 获取对应的Lock,没有则新增一把Lock
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) {
ReentrantLock lock = new ReentrantLock();
ReentrantLock previous = locks.putIfAbsent(key, lock);
return previous == null ? lock : previous;
} private void releaseLock(Object key) {
ReentrantLock lock = locks.get(key); // 获取Key对应的Lock
if (lock.isHeldByCurrentThread()) { // 如果是当前线程持有lock,则释放锁
lock.unlock();
}
}

SynchronizedCache

一个支持同步的MyBatisCache,从名称就能知道实现原理是synchronized关键字

  public SynchronizedCache(Cache delegate) {
this.delegate = delegate;
} @Override
public synchronized int getSize() {
return delegate.getSize();
} @Override
public synchronized void putObject(Object key, Object object) {
delegate.putObject(key, object);
} @Override
public synchronized Object getObject(Object key) {
return delegate.getObject(key);
} @Override
public synchronized Object removeObject(Object key) {
return delegate.removeObject(key);
}

同步缓存就是给核心方法加上了同步锁,保证了线程安全。

跟随源码看看解析-装饰过程

cacheElement方法解析cache标签

可以看出最底层是PerpetualCache,默认装饰的是LruCache。

如下就是将剩下的装饰器循环装饰的过程了,细节就不追进去了。

以上就是MyBatis对于缓存的装饰者设计模式的实践相关的源码简单追踪了。

跟随源码看看缓存的使用的地方

先随便点击Cache接口的一方法,看看在哪里有使用。很明显,那个BaseExecutor的类就是正儿八经使用的地方。

query方法中很明显表示了先从缓存中获取,如果没有则走DB(还会写缓存)

代码也很简单,就是从DB获取然后写入缓存

总结

笔者先简单描述了装饰者模式,随后回忆了MyBatis的缓存传统手艺-cache标签的使用,以及一级二级缓存,描述了集成第三方缓存(解决JVM缓存的单点问题)。

随后结合源码介绍了MyBatis的Cache接口及其相关的实现类,首先通过Demo言简意赅地表达了装饰者模式的使用以及MyBatisCache装饰者模式使用的效果(LoggingCache)

紧接着笔者介绍了

  • PerpetualCache这个最关键最核心的缓存实现类,它的核心是一个HashMap;
  • FifoCache先进先出淘汰策略的缓存实现类,它的核心是一个维护key的双端队列,添加缓存前先维护这个双端队列,如果size到达阈值则移除队头的元素;
  • LruCache最近最少使用淘汰策略的缓存实现类,它的核心是基于LinkedHashMap实现LRU机制,我们也回忆了LRU以及LinkedHashMap相关的知识点,其关键点就是一个继承了LinkedHashMap的keyMap(KV都是缓存Key),重写了LinkedHashMap的重要方法removeEldestEntry,用于记录最近最少使用的key,在适当时机删除该缓存;
  • SoftCache、WeakCache我们回忆了软引用、弱引用的相关知识,其核心就是对应的Value组件Entry继承了SoftReference、WeakReference;
  • BlockingCache这个阻塞缓存的核心就是大名鼎鼎的ReentrantLock;
  • SynchronizedCache这个缓存顾名思义就是核心方法追加了synchronized的关键字,事实也确实如此。

为什么要使用缓存?走DB的链路上层用缓存抗一抗再正常不过了。 为什么用装饰者模式?这个场景它的核心就是缓存策略有很多,它们互相可以叠加,可以在配置的时候灵活配置,那么就可以通过解析配置后在运行时灵活的“装饰”起来,达到最后的预期效果,挺妙的。

关于多种Cache的核心实现,以及相关的周边技术可以反复琢磨,比如锁的使用、缓存的读写、LinkedHashMap、JVM的GC等等,毕竟这是开源框架的实战代码,这些都是值得我们像骆驼一样反复咀嚼,反复反刍的,至少了解了这一块,后续你真的有类似实战的时候之前可以先参考参考了!

好了,以上就是MyBatis缓存解析-装饰者设计模式了。欢迎多多交流,希望对你有帮助。原创不易..(没想到这么难,本来想总结下,发现一两次还写不完,光扣字都扣傻了 哈哈..)

开源框架是如何使用设计模式的-MyBatis缓存机制之装饰者模式的更多相关文章

  1. mybatis缓存机制与装饰者模式

    mybatis 缓存 MyBatis的二级缓存的设计原理 装饰者模式

  2. 聊聊MyBatis缓存机制【美团-推荐】

    聊聊MyBatis缓存机制 2018年01月19日 作者: 凯伦 文章链接 18778字 38分钟阅读 前言 MyBatis是常见的Java数据库访问层框架.在日常工作中,开发人员多数情况下是使用My ...

  3. 聊聊MyBatis缓存机制

    https://tech.meituan.com/mybatis_cache.html 前言 MyBatis是常见的Java数据库访问层框架.在日常工作中,开发人员多数情况下是使用MyBatis的默认 ...

  4. 【转】MyBatis缓存机制

    转载:https://blog.csdn.net/bjweimengshu/article/details/79988252. 本文转载自公众号 美团技术点评 前言 MyBatis是常见的Java数据 ...

  5. mybatis缓存机制

    目录 mybatis缓存机制 Executor和缓存 一级缓存 小结 二级缓存 小结 mybatis缓存机制 mybatis支持一.二级缓存来提高查询效率,能够正确的使用缓存的前提是熟悉mybatis ...

  6. 《深入理解mybatis原理4》 MyBatis缓存机制的设计与实现

    <深入理解mybatis原理> MyBatis缓存机制的设计与实现 本文主要讲解MyBatis非常棒的缓存机制的设计原理,给读者们介绍一下MyBatis的缓存机制的轮廓,然后会分别针对缓存 ...

  7. Mybatis缓存机制及mybatis的各个组成部分

    Mybatis 一级缓存: 基于PerpetualCache 的 HashMap本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该Session中的所有 ...

  8. MyBatis 缓存机制(十三)

    什么是缓存 缓存就是内存中的一个对象,用于对数据库查询结果的保存,用于减少与数据库的交互次数从而降低数据库的压力,进而提高响应速度. MyBatis 缓存机制原理 Mybatis 缓存机制原理是将第一 ...

  9. Mybatis——缓存机制

    MyBatis 包含一个非常强大的查询缓存特性,它可以非常方便地配置和定制.缓存可以极大的提升查询效率. MyBatis系统中默认定义了两级缓存. 一级缓存和二级缓存. 1.默认情况下,只有一级缓存( ...

随机推荐

  1. 【题解】【洛谷 P1967】 货车运输

    目录 洛谷 P1967 货车运输 原题 题解 思路 代码 洛谷 P1967 货车运输 原题 题面请查看洛谷 P1967 货车运输. 题解 思路 根据题面,假设我们有一个普通的图: 作图工具:Graph ...

  2. SpringBoot数据访问(一) SpringBoot整合Mybatis

    前言 SpringData是Spring提供的一个用于简化数据库访问.支持云服务的开源框架.它是一个伞形项目,包含了大量关系型数据库及非关系型数据库的数据访问解决方案,其设计目的是为了使我们可以快速且 ...

  3. 『无为则无心』Python基础 — 3、搭建Python开发环境

    目录 1.Python开发环境介绍 2.Python解释器的分类 3.下载Python解释器 4.安装Python解释器 5.Python解释器验证 1.Python开发环境介绍 所谓"工欲 ...

  4. 32.qt quick-模仿QQ登录界面实现3D旋转(Rotation、Flipable)

    要想模仿QQ登录界面的3D旋转,我们需要学习Rotation和Flipable.由于没找到QQ的资源图,所以我们以两个图片为例模仿QQ的3D旋转,如下图所示: 最终效果如下所示: 1.Rotation ...

  5. ThreadPoolExecutor参数详解

    ThreadPoolExecutor全部参数的构造函数 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long ke ...

  6. theUnforgiven-冲刺第一天

    每天的冲刺博客分为scrum和PM报告两部分 吴邦天 负责项目整体构思以及对任务安排,承担项目具体设计,编码: 唐嘉诚 负责项目前端页面设计,承担整个项目的后端数据库搭建以及编码 周游 项目美化以及细 ...

  7. 『无为则无心』Python基础 — 12、Python运算符详细介绍

    目录 1.表达式介绍 2.运算符 (1)运算符的分类 (2)算数运算符 (3)赋值运算符 (4)复合赋值运算符 (5)比较运算符 3.逻辑运算符 拓展1:数字之间的逻辑运算 拓展2:Python中逻辑 ...

  8. kube-controller-manager源码分析-PV controller分析

    kubernetes ceph-csi分析目录导航 概述 kube-controller-manager组件中,有两个controller与存储相关,分别是PV controller与AD contr ...

  9. SystemVerilog 中的相等运算符:== or === ?

    1. 四值逻辑的逻辑运算 在对比SystemVerilog中的相等运算符之前,先来看一下三种最基本的逻辑运算符,下文中以·表示与运算,以+表示或运算,以'表示非运算.我们都知道在逻辑代数中,只有0和1 ...

  10. 1、Centos7下安装Oracle11gR2及多实例

    实验环境: 系统:2核8G内存60G硬盘,centos7.4: 优化操作:已经关闭了防火墙.selinux,/etc/hosts文件中以添加"172.16.1.92 slave-node2& ...