什么是幂等性?

对于同一笔业务操作,不管调用多少次,得到的结果都是一样的。普通方式 只适合单机

jvm加锁方式Lock只能在一个jvm中起效,如果多个请求都被同一套系统处理,上面这种使用Lock的方式是没有问题的,不过互联网系统中,多数是采用集群方式部署系统,同一套代码后面会部署多套,如果支付宝同时发来多个通知经过负载均衡转发到不同的机器,上面的锁就不起效了。此时对于多个请求相当于无锁处理了

3. 悲观锁方式,使用数据库中悲观锁实现。悲观锁类似于方式二中的Lock, 只不过是依靠数据库来实现的。数据库中悲观锁使用for update来实现的方式3可以保证接口的幂等性,不过存在缺点,如果业务耗时,并发情况下,后面线程会长期处于等待状态,占用很多线程,让这些线程处于无效等待状态。

4. 乐观锁方式,使用数据库中乐观锁实现。主要还是通过版本号,类似于java的cas操作,那么问题来了,在多线程的情况下,一个线程取得数据了,其它线程会怎么样呢?会不断重试,不会阻塞,可以理解为自旋,阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。

而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。

5. 唯一约束

常见方式:悲观锁,乐观锁,唯一约束

最优排序:乐观锁 > 唯一约束 > 悲观锁

乐观锁CAS的用法,比方说在couchbase中,key是自带版本号的

object OriginCasStreamingTransfer {

implicit val conf = ConfigFactory.load

def main(args: Array[String]): Unit = {

val cbSingleUerThreshold = conf.getInt("cb_single_user_threshold.size")

val batchSize = conf.getInt("batch.size") // 消息批次处理

val zkQuorum = conf.getString("test_kafka.zkQuorum")

val brokers = conf.getString("test_kafka.brokers")

val topics = conf.getString("test_kafka.topics")

val group = conf.getString("test_kafka.group.id")

val seconds: Long = 120 // spark streaming mini batch time interval 防止pending batch过多造成流任务失败

val numThreads = 16

val topicMap = topics.split(",").map((_, numThreads)).toMap

val kafkaParams = Map(

("metadata.broker.list", brokers),

("zookeeper.connect", zkQuorum),

("auto.offset.reset", "smallest"),

("zookeeper.connection.timeout.ms", "30000"),

("group.id", group)

)

val sparkConf = new SparkConf()

.setIfMissing("spark.master", "local[*]")

.setAppName("CasStreamingTransfer")

.set("spark.default.parallelism", "1000")

.set("spark.executor.memory", "8g")

val spark = SparkSession

.builder()

.appName("CasStreamingTransfer")

.config(sparkConf)

.getOrCreate()

val ssc = new StreamingContext(spark.sparkContext, Seconds(seconds))

val stream = KafkaUtils.createStream[String, String, StringDecoder, StringDecoder](

ssc, kafkaParams, topicMap,

StorageLevel.MEMORY_AND_DISK_SER) // key value key解码格式 value解码格式

stream.foreachRDD{each_rdd =>

each_rdd.repartition(1000).foreachPartition{ each_partition =>

while (each_partition.hasNext){

var batchCount = 0

val batchRecord = ListBuffer[String]()

while (batchCount < batchSize && each_partition.hasNext){

val item = each_partition.next()

batchRecord += item._1

batchCount += 1

}

val errorReadResult = collection.mutable.Set[String]()

val readResult = CbManager.getInstance().readCasAsync("test_playback_record_couchbase", batchRecord.asJava, errorReadResult.asJava)

// println("readResult: " + readResult)

val writeResult = collection.mutable.Map[CbCasEntity, Array[Byte]]()

for ((key, value)

val uid = key.getId // 取出的用户uid

// println("uid: " + uid)

val cas = key.getCas // 取出的用户的couchbase当中的cas值

val parseResult = PlayRecord.UserPlayRecords.parseFrom(value) // 解析couchbase当中的value字段

// 解析byte[], 拆分出旧,新的播放记录

val transferCount = calTransferCount(parseResult, cbSingleUerThreshold) // 计算需要从cb转移到hikv旧的播放记录量

val oldCbCutEntity = cutOldPlayRecordsFromCb(transferCount, parseResult) // 截取couchbase中比较旧的播放记录

val newCbUserPlayRecords = cutNewPlayRecordsFromCb(oldCbCutEntity.getTimeRecordBreakCount, oldCbCutEntity.getRecordEntryCount, parseResult) // 截取couchbase中比较新的播放记录

// println("transferCount: " + transferCount)

try{

val playRecordData = PlayRecordDataDao.getInstance().get(uid).toCompletableFuture.get // 阻塞读取

// println(playRecordData.getPlayRecord)

if (playRecordData != null){

// 如果hikv里面已经存在播放记录了,需要将cb中旧的播放记录移到原有hikv后面, 并将cb中旧的播放记录用切分好的新的播放记录覆盖

// 日志****** couchbase中超过阈值的单个用户的uid, 第一条记录,记录总共条数,最后一条记录

val parseResultLastTimedRecordIndex = parseResult.getTimedRecordCount - 1

if (parseResultLastTimedRecordIndex != -1){

val parseResultLastTimedRecord = parseResult.getTimedRecord(parseResultLastTimedRecordIndex)

val parseResultLastRecordEntryIndex = parseResultLastTimedRecord.getRecordCount - 1

if (parseResultLastRecordEntryIndex != -1){

val parseResultLastRecordEntry = parseResultLastTimedRecord.getRecord(parseResultLastRecordEntryIndex)

println("0000 couchbase beyond threshold all records uid: " + uid)

println("0000 couchbase beyond threshold all records first record: " + parseResult.getTimedRecord(0).getRecord(0))

println("0000 couchbase beyond threshold all records total count: " + calTotalCount(parseResult))

println("0000 couchbase beyond threshold all records last record: " + parseResultLastRecordEntry)

}

}

// 日志******

// 日志****** couchbase中超过阈值的单个用户的uid,截取的较旧的播放记录的第一条记录,较旧记录的总共条数,最后一条记录

val cbUserPlayRecordsLastTimedRecordIndex = oldCbCutEntity.getCbUserPlayRecords.getTimedRecordCount - 1

if (cbUserPlayRecordsLastTimedRecordIndex != -1){

val cbUserPlayRecordsLastTimedRecord = oldCbCutEntity.getCbUserPlayRecords.getTimedRecord(cbUserPlayRecordsLastTimedRecordIndex)

val cbUserPlayRecordsLastRecordEntryIndex = cbUserPlayRecordsLastTimedRecord.getRecordCount - 1

if (cbUserPlayRecordsLastRecordEntryIndex != -1){

val cbUserPlayRecordsLastRecordEntry = cbUserPlayRecordsLastTimedRecord.getRecord(cbUserPlayRecordsLastRecordEntryIndex)

println("1111 couchbase beyond threshold cut old records uid: " + uid)

println("1111 couchbase beyond threshold cut old records first record: " + oldCbCutEntity.getCbUserPlayRecords.getTimedRecord(0).getRecord(0))

println("1111 couchbase beyond threshold cut old records break record: " + oldCbCutEntity.getBreakAlbumId)

println("1111 couchbase beyond threshold cut old records total count: " + calTotalCount(oldCbCutEntity.getCbUserPlayRecords.build()))

println("1111 couchbase beyond threshold cut old records last record: " + cbUserPlayRecordsLastRecordEntry)

}

}

// 日志******

// 日志****** couchbase中超过阈值的单个用户的uid, 截取的较新的播放记录的第一条记录,较新记录的总共条数,最后一条记录

val newCbUserPlayRecordsLastTimedRecordIndex = newCbUserPlayRecords.getTimedRecordCount - 1

if (newCbUserPlayRecordsLastTimedRecordIndex != -1){

val newCbUserPlayRecordsLastTimedRecord = newCbUserPlayRecords.getTimedRecord(newCbUserPlayRecordsLastTimedRecordIndex)

val newCbUserPlayRecordsLastRecordEntryIndex = newCbUserPlayRecordsLastTimedRecord.getRecordCount - 1

if (newCbUserPlayRecordsLastRecordEntryIndex != -1){

val newCbUserPlayRecordsLastRecordEntry = newCbUserPlayRecordsLastTimedRecord.getRecord(newCbUserPlayRecordsLastRecordEntryIndex)

println("2222 couchbase beyond threshold cut new records uid: " + uid)

println("2222 couchbase beyond threshold cut new records first record: " + newCbUserPlayRecords.getTimedRecord(0).getRecord(0))

println("2222 couchbase beyond threshold cut new records total count: " + calTotalCount(newCbUserPlayRecords.build()))

println("2222 couchbase beyond threshold cut new records last record: " + newCbUserPlayRecordsLastRecordEntry)

}

}

// 日志******

// hikv中已经存在的记录,之前插入的

val playRecord = playRecordData.getPlayRecord

// 日志****** hikv中已经存在的记录,之前插入的

val playRecordLastTimedRecordIndex = playRecord.getTimedRecordCount - 1

if (playRecordLastTimedRecordIndex != -1){

val playRecordLastTimedRecord = playRecord.getTimedRecord(playRecordLastTimedRecordIndex)

val playRecordLastRecordEntryIndex = playRecordLastTimedRecord.getRecordCount - 1

if (playRecordLastRecordEntryIndex != -1){

val playRecordLastRecordEntry = playRecordLastTimedRecord.getRecord(playRecordLastRecordEntryIndex)

println("3333 hikv already exist uid: " + uid)

println("3333 hikv already exist first record: " + playRecord.getTimedRecord(0).getRecord(0))

println("3333 hikv already exist total count: " + calTotalCount(playRecord.build()))

println("3333 hikv already exist last record: " + playRecordLastRecordEntry)

}

}

// 日志******

val beforePlayRecordCount = calTotalCount(playRecord.build())

// 将couchbase旧的播放记录加入到hikv后面形成新的记录 4444 = 3333 + 1111

val newPlayRecord = playRecord.addAllTimedRecord(oldCbCutEntity.getCbUserPlayRecords.getTimedRecordList)

// 日志******

val newPlayRecordLastTimedRecordIndex = newPlayRecord.getTimedRecordCount - 1

if (newPlayRecordLastTimedRecordIndex != -1){

val newPlayRecordLastTimedRecord = newPlayRecord.getTimedRecord(newPlayRecordLastTimedRecordIndex)

val newPlayRecordLastRecordEntryIndex = newPlayRecordLastTimedRecord.getRecordCount - 1

if (newPlayRecordLastRecordEntryIndex != -1){

val newPlayRecordLastRecordEntry = newPlayRecordLastTimedRecord.getRecord(newPlayRecordLastRecordEntryIndex)

println("4444 hikv ideal uid: " + uid)

println("4444 hikv ideal first record: " + newPlayRecord.getTimedRecord(0).getRecord(0))

println("4444 hikv ideal total count: " + calTotalCount(newPlayRecord.build()))

println("4444 hikv ideal last record: " + newPlayRecordLastRecordEntry)

}

}

// 日志******

// 将hikv中新增的旧的播放记录重新写入到hikv

PlayRecordDataDao.getInstance().set(new PlayRecordData(uid, newPlayRecord)).toCompletableFuture.get // 阻塞写入

// hikv写入成功后,再写入couchbase

/**

* cb hikv

* 成功 失败 不存在 这种情况会导致播放记录减少,因为截取的新的播放记录写入了cb, 但现在先写入hikv, 再写入cb,自然避免了这种情况

* 失败 成功 存在 这种情况会有重复的播放记录,hikv里面有旧的播放记录,cb里面有旧的播放记录和新的播放记录

* 成功 成功 存在 理想情况

* 失败 失败 存在 没有转移成功,下次kafka里面还会发送这个uid过来

*/

writeResult.put(new CbCasEntity(uid, cas), newCbUserPlayRecords.build().toByteArray)

}else{

// 如果hikv里面本来就没有播放记录, 切分,将旧的播放记录放到hikv中,将新的播放记录覆盖到cb中

// 日志****** couchbase中超过阈值的单个用户的uid, 第一条记录,记录总共条数,最后一条记录

val parseResultLastTimedRecordIndex = parseResult.getTimedRecordCount - 1

if (parseResultLastTimedRecordIndex != -1){

val parseResultLastTimedRecord = parseResult.getTimedRecord(parseResultLastTimedRecordIndex)

val parseResultLastRecordEntryIndex = parseResultLastTimedRecord.getRecordCount - 1

if (parseResultLastRecordEntryIndex != -1){

val parseResultLastRecordEntry = parseResultLastTimedRecord.getRecord(parseResultLastRecordEntryIndex)

println("5555 couchbase beyond threshold all records uid: " + uid)

println("5555 couchbase beyond threshold all records first record: " + parseResult.getTimedRecord(0).getRecord(0))

println("5555 couchbase beyond threshold all records total count: " + calTotalCount(parseResult))

println("5555 couchbase beyond threshold all records last record: " + parseResultLastRecordEntry)

}

}

// 日志******

// 日志****** couchbase中超过阈值的单个用户的uid,截取的较旧的播放记录的第一条记录,较旧记录的总共条数,最后一条记录

val cbUserPlayRecordsLastTimedRecordIndex = oldCbCutEntity.getCbUserPlayRecords.getTimedRecordCount - 1

if (cbUserPlayRecordsLastTimedRecordIndex != -1){

val cbUserPlayRecordsLastTimedRecord = oldCbCutEntity.getCbUserPlayRecords.getTimedRecord(cbUserPlayRecordsLastTimedRecordIndex)

val cbUserPlayRecordsLastRecordEntryIndex = cbUserPlayRecordsLastTimedRecord.getRecordCount - 1

if (cbUserPlayRecordsLastRecordEntryIndex != -1){

val cbUserPlayRecordsLastRecordEntry = cbUserPlayRecordsLastTimedRecord.getRecord(cbUserPlayRecordsLastRecordEntryIndex)

println("6666 couchbase beyond threshold cut old records uid: " + uid)

println("6666 couchbase beyond threshold cut old records first record: " + oldCbCutEntity.getCbUserPlayRecords.getTimedRecord(0).getRecord(0))

println("6666 couchbase beyond threshold cut old records total count: " + calTotalCount(oldCbCutEntity.getCbUserPlayRecords.build()))

println("6666 couchbase beyond threshold cut old records last record: " + cbUserPlayRecordsLastRecordEntry)

}

}

// 日志******

// 日志****** couchbase中超过阈值的单个用户的uid, 截取的较新的播放记录的第一条记录,较新记录的总共条数,最后一条记录

val newCbUserPlayRecordsLastTimedRecordIndex = newCbUserPlayRecords.getTimedRecordCount - 1

if (newCbUserPlayRecordsLastTimedRecordIndex != -1){

val newCbUserPlayRecordsLastTimedRecord = newCbUserPlayRecords.getTimedRecord(newCbUserPlayRecordsLastTimedRecordIndex)

val newCbUserPlayRecordsLastRecordEntryIndex = newCbUserPlayRecordsLastTimedRecord.getRecordCount - 1

if (newCbUserPlayRecordsLastRecordEntryIndex != -1){

val newCbUserPlayRecordsLastRecordEntry = newCbUserPlayRecordsLastTimedRecord.getRecord(newCbUserPlayRecordsLastRecordEntryIndex)

println("7777 couchbase beyond threshold cut new records uid: " + uid)

println("7777 couchbase beyond threshold cut new records first record: " + newCbUserPlayRecords.getTimedRecord(0).getRecord(0))

println("7777 couchbase beyond threshold cut new records total count: " + calTotalCount(newCbUserPlayRecords.build()))

println("7777 couchbase beyond threshold cut new records last record: " + newCbUserPlayRecordsLastRecordEntry)

}

}

// 日志******

PlayRecordDataDao.getInstance().set(new PlayRecordData(uid, oldCbCutEntity.getCbUserPlayRecords)).toCompletableFuture.get

writeResult.put(new CbCasEntity(uid, cas), newCbUserPlayRecords.build().toByteArray)

}

}catch {

case e: Exception => e.printStackTrace()

}

}

val errorWriteResult = collection.mutable.Set[String]()

CbManager.getInstance().writeCasAsync("test_playback_record_couchbase", writeResult.asJava, 186, 16070400, errorWriteResult.asJava)

println("writeResult: " + writeResult.keySet.map(item => item.getId)) // 这些key说明hikv确实是插入成功了, 可以查看写入cb的key有哪些

val writeSuccess = writeResult.keySet.filterNot(item => errorReadResult.contains(item.getId))

println("writeSuccess: " + writeSuccess.map(item => item.getId))

}

}

}

ssc.start()

ssc.awaitTermination()

}

def calTotalCount(parseResult: PlayRecord.UserPlayRecords): Int = {

/*

计算从couchbase解析出来的每个用户的播放记录长度

*/

var recordTotalCount = 0

for (timeRecordItem

recordTotalCount += timeRecordItem.getRecordCount

}

recordTotalCount

}

def calTransferCount(parseResult: PlayRecord.UserPlayRecords, cbSingleUerThreshold: Int): Int = {

/*

计算超过阈值的单个用户从couchbase转移到hikv的播放记录的长度

e.g. : 10000(couchbase中存在的量) - 1000(单个用户的阈值数) = 9000

*/

var recordTotalCount = 0

for (timeRecordItem

recordTotalCount += timeRecordItem.getRecordCount

}

recordTotalCount - cbSingleUerThreshold

}

def cutOldPlayRecordsFromCb(transferCount: Int, parseResult: PlayRecord.UserPlayRecords): CbCutEntity = {

/*

将couchbase中单个用户的播放记录进行切分,得到旧的播放记录

*/

var breakCount = 0

var recordEntryCount = 0 // 看break的那个recordEntry是到达第几个以后退出

var timeRecordBreakCount = 0 // 之前转移到hikv的最后的timeRecord的点

var breakFlag = false

var breakAlbumId = 0L

var userPlayRecords = PlayRecord.UserPlayRecords.newBuilder()

breakable{for (timedRecordItem

var timeRecord = PlayRecord.TimedRecord.newBuilder()

timeRecord.setRecordTime(timedRecordItem.getRecordTime)

var innerRecordEntryCount = 0 // 每一次都初始化为0

breakable {for (recordEntryItem

var recordEntry = PlayRecord.RecordEntry.newBuilder()

recordEntry.setAlbumId(recordEntryItem.getAlbumId)

recordEntry.setMaxTvId(recordEntryItem.getMaxTvId)

recordEntry.setMaxPlayPosition(recordEntryItem.getMaxPlayPosition)

if (breakCount == transferCount){

recordEntryCount = innerRecordEntryCount

breakAlbumId = recordEntryItem.getAlbumId

breakFlag = true

break

}

timeRecord.addRecord(recordEntry)

breakCount += 1

innerRecordEntryCount += 1

}}

userPlayRecords.addTimedRecord(timeRecord)

if (breakFlag){

break

}

timeRecordBreakCount += 1

}}

new CbCutEntity(timeRecordBreakCount, recordEntryCount, breakAlbumId, userPlayRecords)

}

def cutNewPlayRecordsFromCb(timeRecordBreakCount: Int, recordEntryCount: Int, parseResult: PlayRecord.UserPlayRecords): PlayRecord.UserPlayRecords.Builder = {

var cbUserPlayRecords = PlayRecord.UserPlayRecords.newBuilder()

for (trIndex

var timedRecordItem = parseResult.getTimedRecord(trIndex)

var cbTimeRecord = PlayRecord.TimedRecord.newBuilder()

cbTimeRecord.setRecordTime(timedRecordItem.getRecordTime)

if (trIndex == timeRecordBreakCount) {

for (rIndex

var recordEntryItem = timedRecordItem.getRecord(rIndex)

var cbRecordEntry = PlayRecord.RecordEntry.newBuilder()

cbRecordEntry.setAlbumId(recordEntryItem.getAlbumId)

cbRecordEntry.setMaxTvId(recordEntryItem.getMaxTvId)

cbRecordEntry.setMaxPlayPosition(recordEntryItem.getMaxPlayPosition)

cbTimeRecord.addRecord(cbRecordEntry)

}

}else for (recordEntryItem

var cbRecordEntry = PlayRecord.RecordEntry.newBuilder()

cbRecordEntry.setAlbumId(recordEntryItem.getAlbumId)

cbRecordEntry.setMaxTvId(recordEntryItem.getMaxTvId)

cbRecordEntry.setMaxPlayPosition(recordEntryItem.getMaxPlayPosition)

cbTimeRecord.addRecord(cbRecordEntry)

}

cbUserPlayRecords.addTimedRecord(cbTimeRecord)

}

cbUserPlayRecords

}

def compareOldNew(beforePlayRecordCount: Int, newPlayRecord: PlayRecord.UserPlayRecords.Builder): Boolean = {

var newRecordTotalCount = 0

for (timedRecordItem

newRecordTotalCount += timedRecordItem.getRecordCount

}

newRecordTotalCount > beforePlayRecordCount

}

}

乐观锁实现接口幂等性_什么是幂等性,如何实现,以及乐观锁在项目中的实际用法...相关推荐

  1. 乐观锁实现接口幂等性_深入理解幂等性

    什么是幂等性 HTTP/1.1中对幂等性的定义是:一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外).也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同. ...

  2. springboot幂等性_如何使用 SpringBoot + Redis 优雅的解决接口幂等性问题

    前言: 在实际的开发项目中,一个对外暴露的接口往往会面临,瞬间大量的重复的请求提交,如果想过滤掉重复请求造成对业务的伤害,那就需要实现幂等! 我们来解释一下幂等的概念: 任意多次执行所产生的影响均与一 ...

  3. springboot幂等性_请问,springboot项目支付接口设计,如何保证支付的幂等性,并能给前端反馈友好的提示?...

    什么是幂等性? 幂等是一个数学与计算机学概念,在数学中某一元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同." 在计算机中编程中,一个幂等操作的特点是其任意多次执行所产生的影响 ...

  4. **Java有哪些悲观锁的实现_「Java并发编程」何谓悲观锁与乐观锁,Java编程你会吗...

    何谓悲观锁与乐观锁 悲观锁 乐观锁 两种锁的使用场景 乐观锁常见的两种实现方式 1. 版本号机制 2. CAS算法 乐观锁的缺点 1 ABA 问题 2 循环时间长开销大 3 只能保证一个共享变量的原子 ...

  5. lock是悲观锁还是乐观锁_图文并茂的带你彻底理解悲观锁与乐观锁

    点击上方蓝色字体,选择"设置星标" 优质文章,第一时间送达 文章转自:Hollis 原创:安静的boy 这是一篇介绍悲观锁和乐观锁的入门文章.旨在让那些不了解悲观锁和乐观锁的小白们 ...

  6. mysql锁的应用场景_浅谈Mysql共享锁、排他锁、悲观锁、乐观锁及其使用场景

    Mysql共享锁.排他锁.悲观锁.乐观锁及其使用场景 一.相关名词 |--表级锁(锁定整个表) |--页级锁(锁定一页) |--行级锁(锁定一行) |--共享锁(S锁,MyISAM 叫做读锁) |-- ...

  7. mysql乐观锁 秒杀_使用数据库乐观锁解决高并发秒杀问题,以及如何模拟高并发的场景,CyclicBarrier和CountDownLatch类的用法...

    数据库:mysql 数据库的乐观锁:一般通过数据表加version来实现,相对于悲观锁的话,更能省数据库性能,废话不多说,直接看代码 第一步: 建立数据库表: CREATE TABLE `skill_ ...

  8. java8 同步队列_秋招之路8:JAVA锁体系和AQS抽象队列同步器

    整个的体系图 悲观锁,乐观锁 是一个广义概念:体现的是看待线程同步的不同角度. 悲观锁 认为在自己使用数据的时候一定有别的线程来修改数据,在获取数据的时候会先加锁,确保数据不被别的线程修改. 实现:关 ...

  9. usb接口驱动_乾坤合一~Linux设备驱动之USB主机和设备驱动

    这一章从主机侧角度看到的USB 主机控制器驱动和设备驱动从主机侧的角度而言,需要编写的USB 驱动程序包括主机控制器驱动和设备驱动两类,USB 主机控制器驱动程序控制插入其中的USB 设备,而USB ...

最新文章

  1. openwrt安装oracle,Openwrt安装软件的方法-tomcat 随笔小记-install ubuntu 12.04 in virtualbox_169IT.COM...
  2. android adb常用指令
  3. 如果C++程序要调用已经被编译后的C函数,该怎么办?
  4. python 播放视频 ftp_利用Python快速搭建HTTPFTP服务器
  5. 【渝粤教育】电大中专幼儿园组织与管理 (10)作业 题库
  6. AJAX Tookits -- ConfirmButton
  7. linux go missing git command,安装beego出现go: missing Git command
  8. npm audit fix
  9. 机器学习之保存与加载.pickle模型文件
  10. 拓端tecdat|R语言中回归模型预测的不同类型置信区间应用比较分析
  11. AD元件库安装与使用
  12. STM32的ADC多通道采集的实现
  13. Win10电脑浏览器连不上网
  14. [JZOJ5629]【NOI2018模拟4.4】Map
  15. 2020年日历_2020年农历阳历表,2020年日历表,2020年黄历
  16. Python开发【项目】:生产环境下实时统计网站访问日志信息
  17. Java课程设计-图书借阅管理系统
  18. 人工智能-用matlab实现数字识别
  19. zigbee 终端设备如何离开当前网络
  20. SkeyeVSS综合安防监控Onvif、RTSP、GB28181视频云无插件直播点播解决方案之监控视频云端录像无插件回放

热门文章

  1. 1.Blockly Loop循环
  2. RAID0---RAID6
  3. Arch Linux 的安装配置
  4. linux上升级tesseract,linux 安装Tesseract-OCR
  5. nagios监控php-fpm,Nginx平台安装Nagios监控服务
  6. C/C++|Error: C++ requires a type specifier for all declarations 的解决
  7. 虎牙直播数据采集,为数据分析做储备,Python爬虫120例之第24例
  8. Android流媒体开发之-获取直播节目预告
  9. Java程序设计:五子棋(三)——悔棋
  10. mac电脑上localhost找不到