引言: python中分布式锁与群组管理系列
最近有接触到分布式锁的相关问题。
基于openstack相关组件源码, tooz官网文档和自己对组件使用的一点点心得,
想整理一下这部分的内容。

主要想分为四个部分介绍:
分布式锁与群组管理 1、 tooz介绍
分布式锁与群组管理 2、 tooz应用之负载均衡
分布式锁与群组管理 3、 tooz应用之分布式锁
分布式锁与群组管理 4、 tooz源码分析

本文是第1部分的内容

1 Tooz基础

作用: 
1) 提供分布式协调API来管理群组和群组中的成员
2) 提供分布式锁从而允许分布式节点获取和释放锁来实现同步
解决的问题: 多个分布式进程同步问题

2 Tooz架构

本质: Tooz是zookeeper,  Raft consensus algorithm, Redis等方案的抽象,
    通过驱动(driver)形式来提供后端功能
驱动分类:
zookeeper, Zake, memcached, redis,
SysV IPC(只提供分布式锁功能), PostgreSQL(只提供分布式锁功能), MySQL(只提供分布式锁功能)
驱动特点:
所有驱动都支持分布式进程, Tooz API完全异步,更高校。

3 Tooz功能

3.1 群组管理

管理群组成员。
操作: 群组创建,加入群组,离开群组,查看群组成员,有成员加入或离开群组时通知的功能
应用场景:
ceilometer-notification服务利用群组管理实现负载均衡和真正意义上的服务水平扩展。

3.2 领导选取

每个群组都有领导,所有节点可决定是否参与选举;
领导消失则选取新领导节点;
领导被选取其他成员可能得到通知;
各节点可随时获取当前组的领导。
感悟:
考虑可以使用tooz实现自己的leader选举算法和服务高可用。

3.3 分布式锁

应用场景:
原来ceilometer中通过RPC检测alarm evaluator进程是否存活。
后来ceilometer通过Tooz群组管理来协调多个alarm evaluator进程。
应用场景2:
gnocchi中利用分布式锁操作监控项与监控数据

4 在你的应用中使用Tooz

4.1 创建一个Coordinator

tooz提供的最基本的对象就是coordinator。它允许你使用不同的特性,例如
组成员,leader选举或者分布式锁。
tooz coordinator提供的特性被不同的驱动实现了。当创建一个coordinator,你需要指定
它使用的是哪一个后台驱动。不同的驱动可能提供不同的能力。
如果一个驱动没有实现一个特性,它将会抛出一个NotImplemented异常。
这个样例项目加载了使用ZooKeeper作为驱动的基本coordinator。

from tooz import coordinationcoordinator = coordination.get_coordinator('zake://', b'host-1')
coordinator.start()
coordinator.stop()

传递给coordinator的第二个参数必须是一个唯一的标识符来识别运行的程序。
在coordinator被创建后,它可以被用来使用提供的各种特性。
为了保持连接到coordination的server是存活的,方法heartbeat()
必须被周期性调用。这将确保某个coordinator不会被参与到coordination中
的其他程序认为它已经宕机。除非你想要自己手动调用它,你可以通过传递
start_heart参数来使用tooz内置的heartbeat管理器。

from tooz import coordinationcoordinator = coordination.get_coordinator('zake://', b'host-1')
coordinator.start(start_heart=True)
coordinator.stop()

heartbeat在不同时刻或者间隔。
注意某些驱动,例如memcached很大成都上是基于timeout的,因此用于运行
heartbeat的interval是重要的。

4.2 组成员

4.2.1 基本操作

coordinator提供的一个特性就是管理组成员。一旦一个成员被创建了,
任何coordinator可以加入到group并变成该组的一个成员。任何
coordinator可以在一个成员加入或者离开组的时候被通知。

import uuidimport sixfrom tooz import coordinationcoordinator = coordination.get_coordination('zake://', b'host-1')
coordinationr.start()# create a group
group = six.binary_type(six.text_type(uuid.uuid4()).encode('ascii'))
request = coordinator.create_group(group)
request.get()# join a group
request = coordinator.join_group(group)
request.get()coordinator.stop()

注意所有的操作都是异步的。这意味这你不能确保在你调用
tooz.coordination.CoordAsyncResult.get()方法的时候,你的组已经创建了或者加入了。
你也可以使用tooz.coordination.CoordinationDriver.leave_group()方法来离开一个组。
可以通过tooz.coordination.CoordinationDriver.get_groups()方法被查询所有可以获得的组。

4.2.2 监视组的改变

可以在组中成员发生变化的时候监视到和被通知到。
这在组中一些事情发生后可以运行一些回调方法。

import uuidimport sixfrom tooz import coordinationcoordinator = coordination.get_coordinator('zake://', b'host-1')
coordinator.start()# create a group
group = six.binary_type(six.text_type(uuid.uuid4()).encode('ascii'))
request = coordinator.create_group(group)
request.get()def group_joined(event):# event is an instance of tooz.coordination.MemberJoinedGroupprint "group id: {groupId}, member id: {memberId}".format(groupId=event.group_id, memberId=event.member_id)coordinator.watch_join_group(group, group_joined)
coordinator.stop()

使用tooz.coordination.CoordinationDriver.watch_join_group()和
tooz.coordination.CoordinationDriver.watch_leave_group(),你的应用可以在每次有成员加入组或者
离开组的时候被通知到。为了监视一个事件,两个方法:
tooz.coordination.CoordinationDriver.watch_join_group()和
tooz.coordination.CoordinationDriver.watch_leave_group()允许注销一个特定的回调。

参考:
https://docs.openstack.org/tooz/latest/user/tutorial/group_membership.html

5 leader选举

每个组可以选出自己的leader。一个组中在一个时间段只能有一个leader。
只有那些运行的成员才可以在选举中被选举。一旦leader挂了,一个正在运行的新成
员在选举中会被选举。

import time
import uuidimport sixfrom tooz import coordinationALIVE_TIME = 1
coordinator = coordination.get_coordinator('zake://', b'host-1')
coordinator.start()group = six.binary_type(six.text_type(uuid.uuid4()).encode('ascii'))
request = coordinator.create_group(group)
request.get()request = coordinator.join_group(group)
request.get()def beLeader(event):print "group id: {groupId}, member id: {memberId}".format(groupId=event.group_id, memerId=event.member_id)coordinator.watch_elected_as_leader(group, beLeader)
start = time.time()
while time.time() - start < ALIVE_TIME:coordinator.heartbeat()coordinator.run_watchers()time.sleep(0.1)
coordinator.stop()

方法
tooz.coordination.CoordinatonDriver.watch_elected_as_leader()允许注册
一个方法,该方法可以在成员被选举为leader的时候被回调。使用这个功能意味这
运行用于选举。成员可以通过注销所有的回调:
tooz.coordination.CoordinationDriver.unwatch_elected_as_leader()
来停止运行。
它也可以在成为leader的时候临时运行
tooz.coordination.CoordinatonDriver.stand_down_group_leader()来辞去
leader的职位。如果另一个成员正处与选举中,它可能会取而代之。
为了查找一个组中的leader,可以使用:
tooz.coordination.CoordinationDriver.get_leader()。

参考:
https://docs.openstack.org/tooz/latest/user/tutorial/leader_election.html

6 Lock

Tooz提供了分布式锁。一个锁被一个名字所标识,
并且一个锁仅仅可以被一个coordinator在一个时间获取到。

from tooz import coordination
coordinator = coordination.get_coordinator('zake://', b'host-1')
coordinator.start()# create a lock
lock = coordination.get_lock('foobar')
with lock:print "Do some thing which is distributed"coordinator.stop()

方法:
tooz.coordination.CoordinationDriver.get_lock()允许创建一个用名称标识的锁。
一旦你查询到这个锁,你可以在一个上下文管理或者tooz.locking.Lock.acquire()和
tooz.locking.Lock.release()方法中获取和释放锁。

参考:
https://docs.openstack.org/tooz/latest/user/tutorial/lock.html

7 Hash ring(一致性哈希)

Tooz提供了一个一致性哈希实现。它可以被用于将objects(通过二进制键表示)映射为
一个或者多个节点。当节点列表发生改变,可以通过ring实现再平衡。

from tooz import hashringdef getHashNode():hashringObj = hashring.HashRing({'node1', 'node2', 'node3'})nodeForFoo = hashringObj[b'foo']print nodeForFoonodes = hashringObj.get_nodes(b'foo', replicas=2)print nodesnodes = hashringObj.get_nodes(b'foo', replicas=2, ignore_nodes={'node2'})print nodes

参考:
https://docs.openstack.org/tooz/latest/user/tutorial/hashring.html
https://bugzilla.redhat.com/show_bug.cgi?id=1630445
https://review.opendev.org/#/c/399022/

注意:
hashring的实现是在1.47.0中的
tooz>=1.47.0 # Apache-2.0

8 分区器Partitioner

Tooz基于它的一致性哈希实现提供了一个partitioner对象。
它可以被用于映射Python对象到一个或几个节点。这个partitioner对象自动跟踪
节点加入组和离开组,因此再平衡也被管理了。

from tooz import coordinationcoordinator = coordination.get_coordinator('zake://', b'host-1')
coordinator.start()
partitioner = coordinator.join_partitioned_group("group1")# Returns {'host-1'}
member = partitioner.members_for_object(object())coordinator.leave_partitioned_group(partitioner)
coordinator.stop()

参考:
https://docs.openstack.org/tooz/latest/user/tutorial/partitioner.html

9 API接口

9.1 组管理相关api

watch_join_group()
unwatch_join_group()
watch_leave_group()
unwatch_leave_group()
create_group()
get_groups()
join_group()
leave_group()
delete_group()
get_members()
get_member_capabilities()
update_capabilities()

9.2 leader选举相关api

watch_elected_as_leader()
unwatch_elected_as_leader()
stand_down_group_leader()
get_leader()

9.3 分布式锁相关api

get_lock()

参考:
https://docs.openstack.org/tooz/latest/user/compatibility.html

10 源码demo

基于tooz的分布式锁、群组管理、一致性哈希使用的demo实现如下

# -*- encoding: utf-8 -*-import bisect
import hashlib
import struct
import time
import uuidimport sixfrom tooz import coordinationclass PartionCoordinator(object):def __init__(self, memberId=None):self._coordinator = Noneself._groups = set()self._memberId = memberId or str(uuid.uuid1())self._backendUrl = Noneself._group = Nonedef createCoordinator(self, backendUrl, member=""):try:self._memberId = member if member else self._memberIdself._backendUrl = backendUrlself._coordinator = coordination.get_coordinator(backendUrl, self._memberId)return self._coordinatorexcept Exception as ex:info = "createCoordinator exception: {expe}, message: {mess}".format(expe=ex.__class__.__name__,mess=ex)print infodef startCoordinator(self, startHeart=False):try:self._coordinator.start(start_heart=startHeart)info = "start coordination backend: {backendUrl} sucesssfully".format(backendUrl=self._backendUrl)print infoexcept Exception as ex:info = "start coordination backend: {backendUrl}, exception: {expe}, message: {mess}".format(expe=ex.__class__.__name__,mess=ex,backendUrl=self._backendUrl)print infodef stopCoordinator(self):try:self._coordinator.stop()info = "stop coordination backend: {backendUrl} sucesssfully".format(backendUrl=self._backendUrl)print infoexcept Exception as ex:info = "stop Ccoordination backend: {backendUrl}, exception: {expe}, message: {mess}".format(expe=ex.__class__.__name__,mess=ex,backendUrl=self._backendUrl)print infodef createGroup(self, group=''):group = group if group else six.binary_type(six.text_type(uuid.uuid1()).encode('ascii'))self._group = grouptry:request = self._coordinator.create_group(group)result = request.get()info = "create coordination group: {group}, result: {result}, type: {resultType}".format(result=result,resultType=type(result),group=group)print infoexcept Exception as ex:info = "create coordination group: {group}, exception: {expe}, message: {mess}".format(group=group,expe=ex.__class__.__name__,mess=ex)print infodef joinGroup(self, group):if not group:info = "group is empty, can not join group"print inforeturntry:request = self._coordinator.join_group(group)self._groups.add(group)result = request.get()info = "join coordination group: {group}, result: {result}, type: {resultType}".format(result=result,resultType=type(result),group=group)print infoexcept Exception as ex:info = "join coordination group: {group}, exception: {expe}, message: {mess}".format(group=group,expe=ex.__class__.__name__,mess=ex)print infodef watchJoinGroup(self, group, callback):try:self._coordinator.watch_join_group(group, callback)print "watch join coordination group: {group} successfully".format(group=group)except Exception as ex:info = "watch join coordination group: {group}, exception: {expe}, message: {mess}".format(group=group,expe=ex.__class__.__name__,mess=ex)print infodef watchBeLeader(self, group, callback):try:self._coordinator.watch_elected_as_leader(group, callback)print "watch be leader, coordination group: {group} successfully".format(group=group)except Exception as ex:info = "watch be leader, coordination group: {group}, exception: {expe}, message: {mess}".format(group=group,expe=ex.__class__.__name__,mess=ex)print infodef runWatchers(self):try:self._coordinator.run_watchers()print "run watchers successfully"except Exception as ex:info = "run watchers exception: {expe}, message: {mess}".format(expe=ex.__class__.__name__,mess=ex)print infodef processWithLock(self, lockName):try:lock = self._coordinator.get_lock(lockName)info = "get lock name: {lockName} successfully, lock is: {lock}".format(lockName=lockName,lock=lock)print infowith lock:print "do something which is distributed"except Exception as ex:info = "get lock:  {lockName}, exception: {expe}, message: {mess}".format(lockName=lockName,expe=ex.__class__.__name__,mess=ex)print infodef beLeader(self, event):info = "########## be leader, group id: {groupId}, member id: {memberId}".format(groupId=event.group_id,memberId=event.member_id)print infodef groupJoined(self, event):info = "########## group joined, id: {groupId}, member id: {memberId}".format(groupId=event.group_id,memberId=event.member_id)print infodef joinPartitionedGroup(self, group):try:partitioner = self._coordinator.join_partitioned_group(group)member = partitioner.members_for_object(object())info = "member: {member}, type: {memberType}".format(member=member,memberType=type(member))print infoself._coordinator.leave_partitioned_group(partitioner)except Exception as ex:info = "join partitioned group: {group}, exception: {expe}, message: {mess}".format(group=group,expe=ex.__class__.__name__,mess=ex)print infodef getMembers(self, group):if not self._coordinator:return [self._memberId]while True:request = self._coordinator.get_members(group)try:result = request.get()return resultexcept tooz.coordination.GroupNotCreated():self.joinGroup(group)# TODO()def joinGroupWithRetry(self, group):pass'''作用: 抽取出当前节点上对应的对象主要思想: 从tooz中获取出一组活跃的组成员,然后将锁有对象映射到buckets桶中,并只返回在我们这个bucket中的锁有对象'''def extractMySubset(self, group, iterable):if not group:return iterableif group not in self._groups:self.joinGroup(group)try:members = self.getMembers(group)info = "members: {members},  group: {group}".format(members=members,group=group)print infoif self._memberId not in members:info = "current member: {member} not in members: {members}, group: {group}".format(member=self._memberId,members=members,group=group)print info# it needs to rejoin the groupself.joinGroup(group)if self._memberId not in members:raise Exception(info)# 初始化一致性哈希环hashRing = ConsistentHashRing(members)# 从给定的队列数组(例如: 0~9总共10个队列中)中抽取出落在当前节点上的队列编号列表iterable = list(iterable)results = []for i in iterable:expectedMember = hashRing.getNode(i)if expectedMember == self._memberId:results.append(i)info = "all queue numbers: {allQueues}, located in " \"current member: {member} are these queue " \"numbers: {filterQueues}".format(allQueues=iterable,member=self._memberId,filterQueues=results)print inforeturn resultsexcept Exception as ex:info = "extractMySubset exception: {expe}, message: {mess}".format(expe=ex.__class__.__name__,mess=ex)print infoclass ConsistentHashRing(object):'''作用: 设置节点到其哈希值的映射具体处理步骤:步骤1: 遍历群组中的成员名称列表(可以认为是节点列表),对每个节点1.1 遍历每个副本值1.2 拼接哈希名称为: 节点名称-副本值,并计算该哈希名称的哈希值计算哈希值的步骤具体如下:1.2.1 先将输入数据转换为unicode字符串,并用utf-8编码为字节流1.2.2 对字节流用hashlib.md5处理,并获取处理后的二进制摘要字符串1.2.3 对二进制摘要字符串采用struct.unpack_from处理,设置格式为大端模式,转换为python的整型数据,该整型数据即为输入数据对应的哈希值。1.3 建立映射: <哈希值,节点名称>1.3 将哈希值存入哈希值数组中步骤2: 对哈希值数组排序'''def __init__(self, nodes, replicas=100):self._ring = {}self._sortedKeys = []for node in nodes:for i in range(replicas):key = '{node}-{index}'.format(node=node,index=i)hashKey = self.getHash(key)self._ring[hashKey] = nodeself._sortedKeys.append(hashKey)self._sortedKeys.sort()'''作用: 获取输入数据对应的哈希值处理过程:步骤1: 将数据转换为unicode字符串,然后编码为utf-8的二进制字节流步骤2: 将该数据的二进制字节流通过hashlib.md5的digest摘要算法获取其二进制字节流摘要步骤3: 对二进制字节流采用大端模式利用struct的unpack_from进行拆包,转换为python中的整型数据该整型数据就为输入数据的哈希值。整体过程: 数据->unicode字符串->编码为utf-8->md5获取其二进制摘要->拆包该二进制摘要转换为python整型数据样例哈希值: 1984516612'''@staticmethoddef getHash(data):tempData = six.text_type(data)# print tempDatarealData = decodeUnicode(tempData)# print realDatadigestResult = hashlib.md5(realData).digest()# print digestResult# print type(digestResult)'''1) > 表示大端模式,即高字节对应起始地址例子: 0x1234567812在高字节,大端存入后的结果如下:12 34 56 78----> 内存增长方向即大端模式是符合正常思维, 网络传输默认是大端模式2) I 表示 C语言中的unsigned int, python中的integer 或者 long3) >I 表示按照大端模式,对二进制字节流进行拆包,得到对应python中的整型数据ref: https://blog.csdn.net/qq_32446743/article/details/80163845https://www.cnblogs.com/tonychopper/archive/2010/07/23/1783501.html'''result = struct.unpack_from('>I', digestResult)[0]return result'''作用: 获取数据数据在ring中的位置主要思想: 获取该输入数据对应的哈希值(md5获取数据的二进制摘要字符串,struct对二进制摘要字符串拆包得到对应python中的整型数据);然后用二分查找bisect查找到之前哈希值数组中当前输入数据哈希值所在的位置'''def getPositionInRing(self, data):hashKey = self.getHash(data)position = bisect.bisect(self._sortedKeys, hashKey)result = position if position < len(self._sortedKeys) else 0return result'''作用: 获取数据数据对应的哈希值(整型数据)'''def getNode(self, data):if not self._ring:return'''excellent: 之所以这里不用字典根据键查找到对应的节点,是因为我们需要根据任意给定输入数据,推断出它离哪个节点的哈希值较近,从而将该输入数据分配到这个节点上。'''position = self.getPositionInRing(data)result = self._ring[self._sortedKeys[position]]# if hashKey in self._ring:#     result = self._ring[hashKey]# else:#     result = Nonereturn resultdef decodeUnicode(data):if isinstance(data, dict):temp = {}# 通过排序tempDict = sorted(six.iteritems(data))print tempDictfor key, value in tempDict:print "key: {key}, value: {value}".format(key=key,value=value)# 递归对键和递归对值处理tempKey = decodeUnicode(key)tempValue = decodeUnicode(value)temp[tempKey] = tempValuereturn tempelif isinstance(data, (tuple, list)):results = []for value in data:result = decodeUnicode(value)results.append(result)return resultselif isinstance(data, six.text_type):result = data.encode('utf-8')return resultelif six.PY3 and isinstance(data, six.binary_type):result = data.decode('utf-8')return resultelse:return datadef useCoordinator():backendUrl = 'redis://localhost:6379/''''注意: 这里使用redis作为tooz的后端,必须配置好url,并且启动redis1 安装redis:sudo yum install redis -ysystemctl enable redissystemctl start redissystemctl status redis[root@localhost .pip]# ps -ef|grep 6379redis    15347     1  0 16:47 ?        00:00:00 /usr/bin/redis-server 127.0.0.1:6379发现用 pip install redis,redis启动不了'''member = b'node-1'# member = str(uuid.uuid4())coordinator = PartionCoordinator()coordinator.createCoordinator(backendUrl, member=member)coordinator.startCoordinator()# group = six.binary_type(six.text_type(uuid.uuid1()).encode('ascii'))\group = "ceilometer.notification"coordinator.createGroup(group)callback = coordinator.groupJoinedcoordinator.watchJoinGroup(group, callback)callback = coordinator.beLeadercoordinator.watchBeLeader(group, callback)coordinator.joinGroup(group)coordinator.runWatchers()lockName = "hello"coordinator.processWithLock(lockName)members = coordinator.getMembers(group)print "members: {members}, members type: {membersType}".format(members=members,membersType=type(members))iterable = range(10)queueNumbers = coordinator.extractMySubset(group, iterable)# coordinator.joinPartitionedGroup(group)coordinator.stopCoordinator()def process():useCoordinator()if __name__ == "__main__":process() 

参考总结:
[1]https://docs.openstack.org/tooz/latest/
[2]https://docs.openstack.org/tooz/latest/user/tutorial/index.html
[3]https://docs.openstack.org/tooz/latest/user/tutorial/group_membership.html
[4]https://docs.openstack.org/tooz/latest/user/tutorial/leader_election.html
[5]https://docs.openstack.org/tooz/latest/user/tutorial/lock.html
[6]https://docs.openstack.org/tooz/latest/user/tutorial/hashring.html
[7]https://bugzilla.redhat.com/show_bug.cgi?id=1630445
[8]https://review.opendev.org/#/c/399022/
[9]https://docs.openstack.org/tooz/latest/user/tutorial/partitioner.html
[10]https://docs.openstack.org/tooz/latest/user/compatibility.html

python 64式: 第26式、分布式锁与群组管理__1、 tooz介绍相关推荐

  1. redis setnx 原子性_Redis从入门到深入-分布式锁(26)

    1. 分布式锁 1.1 简介 锁 是一种用来解决多个执行线程 访问共享资源 错误或数据不一致问题的工具 如果 把一台服务器比作一个房子,那么 线程就好比里面的住户,当他们想要共同访问一个共享资源,例如 ...

  2. zookeeper教程,docker 安装,命令,python操作zookeeper,分布式队列,分布式锁

    docker安装zookeeper服务端 首先安装单节点的服务端,如果安装多节点的服务端,需要为每个节点配置其他节点的地址. docker run --privileged=true -d --nam ...

  3. Redis分布式锁(图解 - 秒懂 - 史上最全)

    文章很长,而且持续更新,建议收藏起来,慢慢读! 高并发 发烧友社群:疯狂创客圈(总入口) 奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : 极致经典 + 社群大片好评 < Java 高并发 三 ...

  4. 删除sybase里面的锁_一起来学习分布式锁

    为什么要用分布式锁 我们先来看一个业务场景: 系统 A 是一个电商系统,目前是一台机器部署,系统中有一个用户下订单的接口,但是用户下订单之前一定要去检查一下库存,确保库存 足够了才会给用户下单. 由于 ...

  5. 请列举你了解的分布式锁_这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排!...

    什么是分布式锁? 大家好,我是jack xu,今天跟大家聊一聊分布式锁.首先说下什么是分布式锁,当我们在进行下订单减库存,抢票,选课,抢红包这些业务场景时,如果在此处没有锁的控制,会导致很严重的问题. ...

  6. 《Redis官方文档》用Redis构建分布式锁(悲观锁)

    2019独角兽企业重金招聘Python工程师标准>>> **用Redis构建分布式锁 ** 在不同进程需要互斥地访问共享资源时,分布式锁是一种非常有用的技术手段. 有很多三方库和文章 ...

  7. Zookeeper命令操作(初始Zookeeper、JavaAPI操作、分布式锁实现、模拟12306售票分布式锁、Zookeeper集群搭建、选举投票)

    Zookeeper命令操作(初始Zookeeper.JavaAPI操作.分布式锁实现.模拟12306售票分布式锁.Zookeeper集群搭建.选举投票) 1.初始Zookeeper Zookeeper ...

  8. 面试热点Redis分布式锁,再细说一次

    欢迎关注方志朋的博客,回复"666"获面试宝典 谈起redis锁,下面三个,算是出现最多的高频词汇: setnx redLock redisson | setnx 其实目前通常所说 ...

  9. 细说Redis分布式锁

    来源:https://juejin.cn/post/6844904082860146695 | 序-碎碎叨叨 在家办公的第N周, 也不知道笔者工位上的键盘和显示器有没有想我, 不知道会不会落灰太严重, ...

最新文章

  1. jquery-12 折叠面板如何实现(两种方法)
  2. Hibernate学习4—关联关系一对多映射2
  3. python expect模块_Python尚学堂高淇|第二季0408P119P123with上常见的异常的解决tryexcept...else结构,...
  4. 用纯css来实现一个优惠券
  5. 一加连续点Android版本号,一加6推送国内首个安卓9.0正式版!刘作虎:一加6T出厂就预装...
  6. hnu2021小学期程序设计 电话号码
  7. Hibernate三大类查询总结
  8. linux文件界面画面,Linux对比文件,很好用的图形界面
  9. 下载安装ARM交叉编译器
  10. 我没见过凌晨四点的洛杉矶,但想带你聆听每个都市夜归人的故事
  11. 360“隐私保护器”真相
  12. oppo r5 android 7.1,OPPO R5的手机系统是什么?OPPO R5能升级安卓4.4吗?
  13. 程序员孔乙己!一个愤世嫉俗,脱离低级趣味的人!
  14. 【观察】戴尔易安信ECS:领跑企业级对象存储,背后的底蕴与底气
  15. 魔方四阶玩法[图解]
  16. 最是那一低头地温柔(徐志摩经典爱情语录)
  17. Revit二次开发—参数的读取与写入
  18. 数据库第一范式,第二范式,第三范式详解
  19. Qt编写视频监控系统(移动侦测/遮挡报警/区域入侵/越界侦测/报警输入输出等)
  20. puppeteer生成PDF

热门文章

  1. 物联网入门——零成本学习
  2. ESP32在Arduino框架下使用LVGL(v8.3)
  3. ceph集群状态检查常用命令
  4. 13. 编写一个程序,提示用户输入3组数,每组数包含5个double类型的数(假设用户都正确地响 应,不会输入非数值数据)。该程序应完成下列任务。
  5. win10,vs2015深度学习目标检测YOLOV5+deepsort C++多目标跟踪代码实现,源码注释,拿来即用。
  6. 2021计算机考研历程-深圳大学
  7. java中的静态成员变量_java中什么叫静态成员变量
  8. 网络nan的原因_训练深度学习网络时候,出现Nan是什么原因,怎么才能避免?
  9. 三容水箱系统故障诊断算法研究
  10. 推荐一个为按钮加好看的css网站,大家可以参考一下