图解Janusgraph系列-并发安全:锁机制(本地锁+分布式锁)分析

大家好,我是洋仔,JanusGraph图解系列文章,实时更新~

图数据库文章总目录:

源码分析相关可查看github(码文不易,求个star~)https://github.com/YYDreamer/janusgraph

下述流程高清大图地址:https://www.processon.com/view/link/5f471b2e7d9c086b9903b629

版本:JanusGraph-0.5.2

转载文章请保留以下声明:

作者:洋仔聊编程

微信公众号:匠心Java

原文地址:https://liyangyang.blog.csdn.net/


在分布式系统中,难免涉及到对同一数据的并发操作,如何保证分布式系统中数据的并发安全呢?分布式锁!

一:分布式锁

常用的分布式锁实现方式:

1、基于数据库实现分布式锁

​ 针对于数据库实现的分布式锁,如mysql使用使用for update共同竞争一个行锁来实现; 在JanusGraph中,也是基于数据库实现的分布式锁,这里的数据库指的是我们当前使用的第三方backend storage,具体的实现方式也和mysql有所不同,具体我们会在下文分析

2、基于Redis实现的分布式锁

​ 基于lua脚本+setNx实现

3、基于zk实现的分布式锁

​ 基于znode的有序性和临时节点+zk的watcher机制实现

4、MVCC多版本并发控制乐观锁实现

本文主要介绍Janusgraph的锁机制,其他的实现机制就不在此做详解了

下面我们来分析一下JanusGraph锁机制实现~

二:JanusGraph锁机制

在JanusGraph中使用的锁机制是:本地锁 + 分布式锁来实现的;

2.1 一致性行为

JanusGraph中主要有三种一致性修饰词(Consistency Modifier)来表示3种不同的一致性行为,来控制图库使用过程中的并发问题的控制程度;

  1. public enum ConsistencyModifier {
  2. DEFAULT,
  3. LOCK,
  4. FORK
  5. }

源码中ConsistencyModifier枚举类主要作用:用于控制JanusGraph在最终一致或其他非事务性后端系统上的一致性行为!其作用分别为:

  • DEFAULT:默认的一致性行为,不使用分布式锁进行控制,对配置的存储后端使用由封闭事务保证的默认一致性模型,一致性行为主要取决于存储后端的配置以及封闭事务的(可选)配置;无需显示配置即可使用
  • LOCK:在存储后端支持锁的前提下,显示的获取分布式锁以保证一致性!确切的一致性保证取决于所配置的锁实现;需management.setConsistency(element, ConsistencyModifier.LOCK);语句进行配置
  • FORK:只适用于multi-edgeslist-properties两种情况下使用;使JanusGraph修改数据时,采用先删除后添加新的边/属性的方式,而不是覆盖现有的边/属性,从而避免潜在的并发写入冲突;需management.setConsistency(element, ConsistencyModifier.FORK);进行配置

LOCK

在查询或者插入数据时,是否使用分布式锁进行并发控制,在图shcema的创建过程中,如上述可以通过配置schema元素ConsistencyModifier.LOCK方式控制并发,则在使用过程中就会用分布式锁进行并发控制;

为了提高效率,JanusGraph默认不使用锁定。 因此,用户必须为定义一致性约束的每个架构元素决定是否使用锁定。

使用JanusGraphManagement.setConsistency(element,ConsistencyModifier.LOCK)显式启用对架构元素的锁定

代码如下所示:

  1. mgmt = graph.openManagement()
  2. name = mgmt.makePropertyKey('consistentName').dataType(String.class).make()
  3. index = mgmt.buildIndex('byConsistentName', Vertex.class).addKey(name).unique().buildCompositeIndex()
  4. mgmt.setConsistency(name, ConsistencyModifier.LOCK) // Ensures only one name per vertex
  5. mgmt.setConsistency(index, ConsistencyModifier.LOCK) // Ensures name uniqueness in the graph
  6. mgmt.commit()

FORK

由于边缘作为单个记录存储在基础存储后端中,因此同时修改单个边缘将导致冲突。

FORK就是为了代替LOCK,可以将边缘标签配置为使用ConsistencyModifier.FORK

下面的示例创建一个新的edge label,并将其设置为ConsistencyModifier.FORK

  1. mgmt = graph.openManagement()
  2. related = mgmt.makeEdgeLabel('related').make()
  3. mgmt.setConsistency(related, ConsistencyModifier.FORK)
  4. mgmt.commit()

经过上述配置后,修改标签配置为FORK的edge时,操作步骤为:

  1. 首先,删除该边
  2. 将修改后的边作为新边添加

因此,如果两个并发事务修改了同一边缘,则提交时将存在边缘的两个修改后的副本,可以在查询遍历期间根据需要解决这些副本。

注意edge fork仅适用于MULTI edge。 具有多重性约束的边缘标签不能使用此策略,因为非MULTI的边缘标签定义中内置了一个唯一性约束,该约束需要显式锁定或使用基础存储后端的冲突解决机制

下面我们具体来看一下janusgrph锁机制的实现:

2.2 LoackID

在介绍锁机制之前,先看一下锁应该锁什么东西呢?

我们都知道在janusgraph的底层存储中,vertexId作为Rowkey,属性和边存储在cell中,由column+value组成

当我们修改节点的属性和边+边的属性时,很明显只要锁住对应的Rowkey + Column即可;

Janusgraph中,这个锁的标识的基础部分就是LockID

LockID = RowKey + Column

源码如下:

  1. KeyColumn lockID = new KeyColumn(key, column);

2.3 本地锁

本地锁是在任何情况下都需要获取的一个锁,只有获取成功后,才会进行下述分布式锁的获取!

本地锁是基于图实例维度存在的;主要作用是保证当前图实例下的操作中无冲突!

本地锁的实现是通过ConcurrentHashMap数据结构来实现的,在图实例维度下唯一;

基于当前事务+lockId来作为锁标识

获取的主要流程:

结合源码如下:

上述图建议依照源码一块分析,源码在LocalLockMediator类中的下述方法,下面源码分析模块会详细分析

  1. public boolean lock(KeyColumn kc, T requester, Instant expires) {
  2. }

引入本地锁机制,主要目的: 在图实例维度来做一层锁判断,减少分布式锁的并发冲突,减少分布式锁带来的性能消耗

2.4 分布式锁

本地锁获取成功之后才会去尝试获取分布式锁

分布式锁的获取整体分为两部分流程:

  1. 分布式锁信息插入
  2. 分布式锁信息状态判断

分布式锁信息插入

该部分主要是通过lockID来构造要插入的Rowkey和column并将数据插入到hbase中;插入成功即表示这部分处理成功!

具体流程如下:

分布式锁信息状态判断

该部分在上一部分完成之后才会进行,主要是判断分布式锁是否获取成功!

查询出当前hbase中对应Rowkey的所有column,过滤未过期的column集合,比对集合的第一个column是否等于当前事务插入的column;

等于则获取成功!不等于则获取失败!

具体流程如下:

三:源码分析 与 整体流程

源码分析已经push到github:https://github.com/YYDreamer/janusgraph

1、获取锁的入口

  1. public void acquireLock(StaticBuffer key, StaticBuffer column, StaticBuffer expectedValue, StoreTransaction txh) throws BackendException {
  2. // locker是一个一致性key锁对象
  3. if (locker != null) {
  4. // 获取当前事务对象
  5. ExpectedValueCheckingTransaction tx = (ExpectedValueCheckingTransaction) txh;
  6. // 判断:当前的获取锁操作是否当前事务的操作中存在增删改的操作
  7. if (tx.isMutationStarted())
  8. throw new PermanentLockingException("Attempted to obtain a lock after mutations had been persisted");
  9. // 使用key+column组装为lockID,供下述加锁使用!!!!!
  10. KeyColumn lockID = new KeyColumn(key, column);
  11. log.debug("Attempting to acquireLock on {} ev={}", lockID, expectedValue);
  12. // 获取本地当前jvm进程中的写锁(看下述的 1:写锁获取分析)
  13. // (此处的获取锁只是将对应的KLV存储到Hbase中!存储成功并不代表获取锁成功)
  14. // 1. 获取成功(等同于存储成功)则继续执行
  15. // 2. 获取失败(等同于存储失败),会抛出异常,抛出到最上层,打印错误日志“Could not commit transaction ["+transactionId+"] due to exception” 并抛出对应的异常,本次插入数据结束
  16. locker.writeLock(lockID, tx.getConsistentTx());
  17. // 执行前提:上述获取锁成功!
  18. // 存储期望值,此处为了实现当相同的key + value + tx多个加锁时,只处理第一个
  19. // 存储在事务对象中,标识在commit判断锁是否获取成功时,当前事务插入的是哪个锁信息
  20. tx.storeExpectedValue(this, lockID, expectedValue);
  21. } else {
  22. // locker为空情况下,直接抛出一个运行时异常,终止程序
  23. store.acquireLock(key, column, expectedValue, unwrapTx(txh));
  24. }
  25. }

2、执行 locker.writeLock(lockID, tx.getConsistentTx()) 触发锁获取

  1. public void writeLock(KeyColumn lockID, StoreTransaction tx) throws TemporaryLockingException, PermanentLockingException {
  2. if (null != tx.getConfiguration().getGroupName()) {
  3. MetricManager.INSTANCE.getCounter(tx.getConfiguration().getGroupName(), M_LOCKS, M_WRITE, M_CALLS).inc();
  4. }
  5. // 判断当前事务是否在图实例的维度 已经占据了lockID的锁
  6. // 此处的lockState在一个事务成功获取本地锁+分布式锁后,以事务为key、value为map,其中key为lockID,value为加锁状态(开始时间、过期时间等)
  7. if (lockState.has(tx, lockID)) {
  8. log.debug("Transaction {} already wrote lock on {}", tx, lockID);
  9. return;
  10. }
  11. // 当前事务没有占据lockID对应的锁
  12. // 进行(lockLocally(lockID, tx) 本地加锁锁定操作,
  13. if (lockLocally(lockID, tx)) {
  14. boolean ok = false;
  15. try {
  16. // 在本地锁获取成功的前提下:
  17. // 尝试获取基于Hbase实现的分布式锁;
  18. // 注意!!!(此处的获取锁只是将对应的KLV存储到Hbase中!存储成功并不代表获取锁成功)
  19. S stat = writeSingleLock(lockID, tx);
  20. // 获取锁分布式锁成功后(即写入成功后),更新本地锁的过期时间为分布式锁的过期时间
  21. lockLocally(lockID, stat.getExpirationTimestamp(), tx); // update local lock expiration time
  22. // 将上述获取的锁,存储在标识当前存在锁的集合中Map<tx,Map<lockID,S>>, key为事务、value中的map为当前事务获取的锁,key为lockID,value为当前获取分布式锁的ConsistentKeyStatus(一致性密匙状态)对象
  23. lockState.take(tx, lockID, stat);
  24. ok = true;
  25. } catch (TemporaryBackendException tse) {
  26. // 在获取分布式锁失败后,捕获该异常,并抛出该异常
  27. throw new TemporaryLockingException(tse);
  28. } catch (AssertionError ae) {
  29. // Concession to ease testing with mocks & behavior verification
  30. ok = true;
  31. throw ae;
  32. } catch (Throwable t) {
  33. // 出现底层存储错误! 则直接加锁失败!
  34. throw new PermanentLockingException(t);
  35. } finally {
  36. // 判断是否成功获取锁,没有获分布式锁的,则释放本地锁
  37. if (!ok) {
  38. // 没有成功获取锁,则释放本地锁
  39. // lockState.release(tx, lockID); // has no effect
  40. unlockLocally(lockID, tx);
  41. if (null != tx.getConfiguration().getGroupName()) {
  42. MetricManager.INSTANCE.getCounter(tx.getConfiguration().getGroupName(), M_LOCKS, M_WRITE, M_EXCEPTIONS).inc();
  43. }
  44. }
  45. }
  46. } else {
  47. // 如果获取本地锁失败,则直接抛出异常,不进行重新本地争用
  48. // Fail immediately with no retries on local contention
  49. throw new PermanentLockingException("Local lock contention");
  50. }
  51. }

包含两个部分:

  1. 本地锁的获取lockLocally(lockID, tx)
  2. 分布式锁的获取writeSingleLock(lockID, tx) 注意此处只是将锁信息写入到Hbase中,并不代表获取分布式锁成功,只是做了上述介绍的第一个阶段分布式锁信息插入

3、本地锁获取 lockLocally(lockID, tx)

  1. public boolean lock(KeyColumn kc, T requester, Instant expires) {
  2. assert null != kc;
  3. assert null != requester;
  4. final StackTraceElement[] acquiredAt = log.isTraceEnabled() ?
  5. new Throwable("Lock acquisition by " + requester).getStackTrace() : null;
  6. // map的value,以事务为核心
  7. final AuditRecord<T> audit = new AuditRecord<>(requester, expires, acquiredAt);
  8. // ConcurrentHashMap实现locks, 以lockID为key,事务为核心value
  9. final AuditRecord<T> inMap = locks.putIfAbsent(kc, audit);
  10. boolean success = false;
  11. // 代表当前map中不存在lockID,标识着锁没有被占用,成功获取锁
  12. if (null == inMap) {
  13. // Uncontended lock succeeded
  14. if (log.isTraceEnabled()) {
  15. log.trace("New local lock created: {} namespace={} txn={}",
  16. kc, name, requester);
  17. }
  18. success = true;
  19. } else if (inMap.equals(audit)) {
  20. // 代表当前存在lockID,比对旧value和新value中的事务对象是否是同一个
  21. // requester has already locked kc; update expiresAt
  22. // 上述判断后,事务对象为同一个,标识当前事务已经获取这个lockID的锁;
  23. // 1. 这一步进行cas替换,作用是为了刷新过期时间
  24. // 2. 并发处理,如果因为锁过期被其他事务占据,则占用锁失败
  25. success = locks.replace(kc, inMap, audit);
  26. if (log.isTraceEnabled()) {
  27. if (success) {
  28. log.trace("Updated local lock expiration: {} namespace={} txn={} oldexp={} newexp={}",
  29. kc, name, requester, inMap.expires, audit.expires);
  30. } else {
  31. log.trace("Failed to update local lock expiration: {} namespace={} txn={} oldexp={} newexp={}",
  32. kc, name, requester, inMap.expires, audit.expires);
  33. }
  34. }
  35. } else if (0 > inMap.expires.compareTo(times.getTime())) {
  36. // 比较过期时间,如果锁已经过期,则当前事务可以占用该锁
  37. // the recorded lock has expired; replace it
  38. // 1. 当前事务占用锁
  39. // 2. 并发处理,如果因为锁过期被其他事务占据,则占用锁失败
  40. success = locks.replace(kc, inMap, audit);
  41. if (log.isTraceEnabled()) {
  42. log.trace("Discarding expired lock: {} namespace={} txn={} expired={}",
  43. kc, name, inMap.holder, inMap.expires);
  44. }
  45. } else {
  46. // 标识:锁被其他事务占用,并且未过期,则占用锁失败
  47. // we lost to a valid lock
  48. if (log.isTraceEnabled()) {
  49. log.trace("Local lock failed: {} namespace={} txn={} (already owned by {})",
  50. kc, name, requester, inMap);
  51. log.trace("Owner stacktrace:\n {}", Joiner.on("\n ").join(inMap.acquiredAt));
  52. }
  53. }
  54. return success;
  55. }

如上述介绍,本地锁的实现是通过ConcurrentHashMap数据结构来实现的,在图实例维度下唯一!

4、分布式锁获取第一个阶段:分布式锁信息插入

  1. protected ConsistentKeyLockStatus writeSingleLock(KeyColumn lockID, StoreTransaction txh) throws Throwable {
  2. // 组装插入hbase数据的Rowkey
  3. final StaticBuffer lockKey = serializer.toLockKey(lockID.getKey(), lockID.getColumn());
  4. StaticBuffer oldLockCol = null;
  5. // 进行尝试插入 ,默认尝试次数3次
  6. for (int i = 0; i < lockRetryCount; i++) {
  7. // 尝试将数据插入到hbase中;oldLockCol表示要删除的column代表上一次尝试插入的数据
  8. WriteResult wr = tryWriteLockOnce(lockKey, oldLockCol, txh);
  9. // 如果插入成功
  10. if (wr.isSuccessful() && wr.getDuration().compareTo(lockWait) <= 0) {
  11. final Instant writeInstant = wr.getWriteTimestamp(); // 写入时间
  12. final Instant expireInstant = writeInstant.plus(lockExpire);// 过期时间
  13. return new ConsistentKeyLockStatus(writeInstant, expireInstant); // 返回插入对象
  14. }
  15. // 赋值当前的尝试插入的数据,要在下一次尝试时删除
  16. oldLockCol = wr.getLockCol();
  17. // 判断插入失败原因,临时异常进行尝试,非临时异常停止尝试!
  18. handleMutationFailure(lockID, lockKey, wr, txh);
  19. }
  20. // 处理在尝试了3次之后还是没插入成功的情况,删除最后一次尝试插入的数据
  21. tryDeleteLockOnce(lockKey, oldLockCol, txh);
  22. // TODO log exception or successful too-slow write here
  23. // 抛出异常,标识导入数据失败
  24. throw new TemporaryBackendException("Lock write retry count exceeded");
  25. }

上述只是将锁信息插入,插入成功标识该流程结束

5、分布式锁获取第一个阶段:分布式锁锁定是否成功判定

这一步,是在commit阶段进行的验证

  1. public void commit() throws BackendException {
  2. // 此方法内调用checkSingleLock 检查分布式锁的获取结果
  3. flushInternal();
  4. tx.commit();
  5. }

最终会调用checkSingleLock方法,判断获取锁的状态!

  1. protected void checkSingleLock(final KeyColumn kc, final ConsistentKeyLockStatus ls,
  2. final StoreTransaction tx) throws BackendException, InterruptedException {
  3. // 检查是否被检查过
  4. if (ls.isChecked())
  5. return;
  6. // Slice the store
  7. KeySliceQuery ksq = new KeySliceQuery(serializer.toLockKey(kc.getKey(), kc.getColumn()), LOCK_COL_START,
  8. LOCK_COL_END);
  9. // 此处从hbase中查询出锁定的行的所有列! 默认查询重试次数3
  10. List<Entry> claimEntries = getSliceWithRetries(ksq, tx);
  11. // 从每个返回条目的列中提取timestamp和rid,然后过滤出带有过期时间戳的timestamp对象
  12. final Iterable<TimestampRid> iterable = Iterables.transform(claimEntries,
  13. e -> serializer.fromLockColumn(e.getColumnAs(StaticBuffer.STATIC_FACTORY), times));
  14. final List<TimestampRid> unexpiredTRs = new ArrayList<>(Iterables.size(iterable));
  15. for (TimestampRid tr : iterable) { // 过滤获取未过期的锁!
  16. final Instant cutoffTime = now.minus(lockExpire);
  17. if (tr.getTimestamp().isBefore(cutoffTime)) {
  18. ...
  19. }
  20. // 将还未过期的锁记录存储到一个集合中
  21. unexpiredTRs.add(tr);
  22. }
  23. // 判断当前tx是否成功持有锁! 如果我们插入的列是读取的第一个列,或者前面的列只包含我们自己的rid(因为我们是在第一部分的前提下获取的锁,第一部分我们成功获取了基于当前进程的锁,所以如果rid相同,代表着我们也成功获取到了当前的分布式锁),那么我们持有锁。否则,另一个进程持有该锁,我们无法获得锁
  24. // 如果,获取锁失败,抛出TemporaryLockingException异常!!!! 抛出到顶层的mutator.commitStorage()处,最终导入失败进行事务回滚等操作
  25. checkSeniority(kc, ls, unexpiredTRs);
  26. // 如果上述步骤未抛出异常,则标识当前的tx已经成功获取锁!
  27. ls.setChecked();
  28. }

四:整体流程

总流程如下图:

整体流程为:

  1. 获取本地锁
  2. 获取分布式锁
    1. 插入分布式锁信息
    2. commit阶段判断分布式锁获取是否成功
  3. 获取失败,则重试

五:总结

JanusGraph的锁机制主要是通过本地锁+分布式锁来实现分布式系统下的数据一致性;

分布式锁的控制维度为:property、vertex、edge、index都可以;

JanusGraph支持在数据导入时通过前面一致性行为部分所说的LOCK来开关分布式锁:

  • LOCK:数据导入时开启分布式锁保证分布式一致性
  • DEFAULT、FORK:数据导入时关闭分布式锁

是否开启分布式锁思考:

在开启分布式锁的情况下,数据导入开销非常大;如果是数据不是要求很高的一致性,并且数据量比较大,我们可以选择关闭分布式锁相关,来提高导入速度;

然后,针对于小数据量的要求高一致性的数据,单独开启分布式锁来保证数据安全;

另外,我们在不开启分布式锁定的情况下,可以通过针对于导入的数据的充分探查来减少冲突!

针对于图schema的元素开启还是关闭分布式锁,还是根据实际业务情况来决定。

本文有任何问题,可加博主微信或评论指出,感谢!

码文不易,给个赞和star吧~

本文由博客群发一文多发等运营工具平台 OpenWrite 发布

图解Janusgraph系列-并发安全:锁机制(本地锁+分布式锁)分析的更多相关文章

  1. 图解Janusgraph系列-图数据底层序列化源码分析(Data Serialize)

    图解Janusgraph系列-图数据底层序列化源码分析(Data Serialize) 大家好,我是洋仔,JanusGraph图解系列文章,实时更新~ 图数据库文章总目录: 整理所有图相关文章,请移步 ...

  2. 图解Janusgraph系列-分布式id生成策略分析

    JanusGraph - 分布式id的生成策略 大家好,我是洋仔,JanusGraph图解系列文章,实时更新~ 本次更新时间:2020-9-1 文章为作者跟踪源码和查看官方文档整理,如有任何问题,请联 ...

  3. 图解JanusGraph系列 - 关于JanusGraph图数据批量快速导入的方案和想法(bulk load data)

    大家好,我是洋仔,JanusGraph图解系列文章,实时更新~ 图数据库文章总目录: 整理所有图相关文章,请移步(超链):图数据库系列-文章总目录 源码分析相关可查看github(码文不易,求个sta ...

  4. Java CAS同步机制 原理详解(为什么并发环境下的COUNT自增操作不安全): Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性。

    精彩理解:  https://www.jianshu.com/p/21be831e851e ;  https://blog.csdn.net/heyutao007/article/details/19 ...

  5. [数据库锁机制] 深入理解乐观锁、悲观锁以及CAS乐观锁的实现机制原理分析

    前言: 在并发访问情况下,可能会出现脏读.不可重复读和幻读等读现象,为了应对这些问题,主流数据库都提供了锁机制,并引入了事务隔离级别的概念.数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务 ...

  6. 【面试普通人VS高手系列】请说一下你对分布式锁的理解,以及分布式锁的实现

    一个工作了7年的Java程序员,私信我关于分布式锁的问题. 一上来就两个灵魂拷问: Redis锁超时怎么办? Redis主从切换导致锁失效怎么办? 我说,别着急,这些都是小问题. 那么,关于" ...

  7. 时间轮机制在Redisson分布式锁中的实际应用以及时间轮源码分析

    本篇文章主要基于Redisson中实现的分布式锁机制继续进行展开,分析Redisson中的时间轮机制. 在前面分析的Redisson的分布式锁实现中,有一个Watch Dog机制来对锁键进行续约,代码 ...

  8. SpringBoot--防止重复提交(锁机制---本地锁、分布式锁)

    防止重复提交,主要是使用锁的形式来处理,如果是单机部署,可以使用本地缓存锁(Guava)即可,如果是分布式部署,则需要使用分布式锁(可以使用zk分布式锁或者redis分布式锁),本文的分布式锁以red ...

  9. 什么是分布式锁?Redis实现分布式锁详解

    在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务.分布式锁等.那具体什么是分布式锁,分布式锁应用在哪些业务场景.如何来实现分布式锁呢?今天继续由陈睿|mikeche ...

随机推荐

  1. 使用IDM批量抓取音效素材下载

    IDM下载器的站点抓取功能,能够抓取网站上的图片.音频.视频.PDF.压缩包等等文件.更重要的是,能够实现批量抓取操作,省时省力.今天就来看一下,如何用IDM巧妙的批量抓取音效素材. 1.进入音效合辑 ...

  2. mac中怎么完成移动硬盘分区这个操作

    移动硬盘在出厂时只有一个区,不方便我们存储和查阅文件,移动硬盘分区可以防止硬盘发生错误,以免造成资料丢失,也可以防止产生无用文件. 移动硬盘基本上都是用Windows系统进行分区的,但是现在很多人使用 ...

  3. Boom 3D快捷方式,让3D音效应用更便捷

    快捷方式是一种快速启动程序.打开程序功能的方法,巧妙地利用快捷键,可以大大加快我们使用Boom 3D的速度,可以让我们更好地享受3D音效. 接下来,就让小编演示一下怎么在不打开Boom 3D的情况下使 ...

  4. python中操作excel数据 封装成一个类

    本文用python中openpyxl库,封装成excel数据的读写方法 from openpyxl import load_workbook from openpyxl.worksheet.works ...

  5. Elasticsearch搜索资料汇总

    Elasticsearch 简介 Elasticsearch(ES)是一个基于Lucene 构建的开源分布式搜索分析引擎,可以近实时的索引.检索数据.具备高可靠.易使用.社区活跃等特点,在全文检索.日 ...

  6. HHKB Programming Contest 2020 D - Squares 题解(思维)

    题目链接 题目大意 给你一个边长为n的正方形和边长为a和b的正方形,要求把边长为a和b的正方形放在长度为n的正方形内,且没有覆盖(可以相邻)求有多少种放法(mod 1e9+7) 题目思路 这个思路不是 ...

  7. python2.7,python3.6,python3.8多版本windows10安装,pip共存

    1.官网下载安装包,建议安装32位,自动兼容X64 x86是32位,x86-64是64位. 可以通过下面3种途径获取python: web-based installer 是需要通过联网完成安装的 e ...

  8. cheat.sh在手,天下我有

    前言 作为程序员需要了解的东西有很多,日常编码和写脚本脱离不开各式语言与 Linux 命令.为了记住一些杂乱的或不被经常使用的知识点,我们迫切需要一个"小抄"/备忘录,小抄内容多了 ...

  9. 2016 ACM/ICPC ECNA Regional I.Waif Until Dark(最大流)

    这是一道ECNA的16年题,问有n个小朋友,m个玩具,不同孩子有不同喜好的玩具,每个玩具可能属于一个类别,同一类别的玩具最多只能用一定次数,问最大匹配 这个就很裸的二分图,掏出dinic板子,首先最后 ...

  10. 第15.45节、PyQt输入部件:QKeySequenceEdit快捷键输入部件简介和使用案例

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 老猿学5G博文目录 一.功能简介 Key Sequence Edit输 ...