这个教程是谷歌出来的,树莓派4B是官方推出的最新版,在树莓派3B上面的安装成功的教程不适用于树莓派4B,会遇到各种难以解决的报错问题,所以找一个好教程非常重要。教程地址:https://qengineering.eu/install-opencv-4.1-on-raspberry-pi-4.html
。不科学上网也是可以打开的,这里我就不翻译了。推荐把SD卡的内存升级一下,16G的内存卡就会很勉强,需要删除一些不经常使用的软件,为了避免因内存不足出现的错误,建议直接将内存卡升级到32G或64G。
在安装OpenCV的依赖过程中,我有两个依赖安装失败了,
1 | sudo apt-get install libgtk2.0-dev libcanberra-gtk* |
搜了一下也没有找出合适的解决方案,就没有管这个了,但是似乎不影响后面的编译。
在命令行中输入python3
,然后输入import cv2
,如果没有报错就说明成功安装,输入cv2.__version__
查看cv2的安装版本,成功退出即可。python3是非常容易调用OpenCV的,但是如果想要用C++来调用,相对而言就复杂多了。
OpenCV3和OpenCV4版本不兼容,所以如果你直接把OpenCV3运行在OpenCV4上是会报错的。我就吃了这个亏,刚开始也不是特别熟悉OpenCV,所以走一些弯路是难免的。编译C++文件有两种方式,cmake和g++,这里推荐使用cmake,更稳定。在移植代码的时候不容易出错。
参考网址:https://www.aiuai.cn/aifarm792.html
新建opencv_test.cpp
文件
1 |
|
新建CMakeLists.txt
文件:
1 | // opencv4 需要 c++ 11 支持 |
当前路径下存放待显示的图片test.jpg
,cmake文件中需要更改自己的OpenCV4的安装目录。执行
1 | cmake . |
显示图片。
打开摄像头并显示成功。同样是采用cmake的方式编译,套路和上面一样。
1 |
|
https://qengineering.eu/install-opencv-4.1-on-raspberry-pi-4.html
,但是里面有两个依赖安装失败,尾部含有*
号,libcanberra-gtk*
和gcc-arm*
,但是似乎不影响后面的编译运行。CMakeLists
中第一行的代码出错;于是我就开始谷歌找错的过程,希望能够解决。均无果。愈加烦躁。各种努力,仍旧报错。从MySQL的基础架构学起,比如,有一个非常简单的表T,表T只有一个ID字段,执行下面的查询语句:
1 | mysql> select * from T where ID=10; |
这条查询语句在MySQL内部是如何执行的呢?
大体来说,MySQL可以分为Server层和存储引擎层两部分。
Server层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖MySQL的大多数核心服务功能,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器和视图等。
而存储引擎层负责数据的存储和提取。其架构是插件式的,支持InnoDB,MyISAM,Memory等多个存储引擎。不同的存储引擎共用一个Server层,即从存储器到执行器的部分。
第一步,先连接到这个数据库,这时候站在第一位的是连接器。连接器负责跟客户端建立连接、获取权限、维持和管理连接。连接命令如下:
1 | mysql -h$ip -P$port -u$user -p |
输完命令之后,需要在交互对话里面输入密码。虽然密码也可以直接跟在-p后面写在命令行中,但这样可能会导致密码泄露。如果你连接的是生产服务器,强烈建议你不要这样做。
连接命令中的mysql是客户端工具,用来跟服务器建立连接。在完成经典的TCP握手后,连接器就开始认证身份,这时候用的是输入的是用户名和密码。
这意味着,一个用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响到已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置。
连接完成后,如果你没有后续的动作,这个连接就处于空闲状态,你可以在show processlist
命令中看到它。客户端如果太长时间没动静,连接器就会自动将它断开。这个时间默认是wait_timeout控制的,默认值是8小时。如果在连接被断开后,客户端再次发送请求的话,就会收到一个错误提醒:Lost connection to MySQL server during query。这时候如果你要继续,就需要重连,然后再执行请求了。
数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接。短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。建立的连接的过程通常是比较复杂的,因此建议在使用中尽量减少建立连接的动作,即尽量使用长连接。
但是全部使用长连接后,可能会出现MySQL占用内存涨的特别快的情况,这是因为MySQL在执行过程中临时使用的内存是管理在连接对象里面的。这些资源会在连接断开的时候才释放,所以如果长连接累计下来,可能导致内存占用太大,被系统强行杀掉(OOM),从现象上看就是MySQL异常重启了。
怎么解决这个问题呢?有两种解决方案:
连接建立完成后,就可以执行select语句了。执行逻辑就会来到第二步:查询缓存。
MySQL拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句则其结果可能会以key-value对的形式,被直接缓存在内存中。key是查询的语句,value是查询的结果。如果你的查询能够直接在这个缓存中找到key,那么这个value就会直接返回给客户端。
如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。你可以看到,如果查询命中缓存,MySQL不需要执行后面的复杂操作,就可以直接返回结果,这个效率会很高。
但是大多数情况下建议不要使用查询缓存?因为查询缓存往往弊大于利。
查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。
同时,MySQL 8.0版本将查询缓存整块删掉了,即8.0后彻底没有这个功能了。
如果没有命中查询缓存,就要开始真正执行语句了。首先,MySQL需要知道你要做什么,因此需要对SQL语句做解析。分析器先做“词法分析”,识别字符串是什么,代表什么;然后做“语法分析”,语法分析器根据语法规则,判断SQL语句是否满足MySQL语法。
一般语法错误会提示第一个出现错误的位置,因此要关注的是后面的内容。
经过了分析器,MySQL就知道你要做什么了。在开始执行之前,还要先经过优化器的处理。优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。
MySQL通过分析器知道了你要做什么,通过优化器知道了该怎么做,然后进入执行器阶段,开始执行语句。开始执行的时候,要先判断一下你对表T有没有执行查询的权限,如果没有,就会返回没有权限的错误。如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。
在数据库的慢查询日志中看到一个rows_examined
的字段,表示这个语句执行过程中扫描了多少行。这个值就是在执行器每次调用引擎获取数据行的时候累加的。
在有些场景下,执行器调用一次,在引擎内部扫描了多行,因此引擎扫描行数跟rows_examined并不是完全相同的。
]]>DNS服务:
当一个用户在浏览器中输入www.baidu.com
时,DNS解析主要有以下几个步骤:
浏览器检查缓存中有没有这个域名对应的解析过的IP,如果缓存命中,解析结束;未命中,进入下个阶段。(浏览器的缓存大小和缓存时间都是有限制的)
查找操作系统缓存中是否有这个域名对应的DNS解析结果。缓存命中,解析结束。未命中,进入下个阶段(在Windows中可以通过C:Windows\System32\drivers\etc\hosts
查看);
如果本地缓存无法完成,那么就会把这个域名发送到LDNS(本地域名服务器),如果在学校接入互联网,那么LNDS就在学校;如果在小区接入互联网,那么这个LDNS就是接入互联网的应用提供商,即电信或联通。
这个域名服务器的性能很好,缓存时间受域名的失效时间限制,不受缓存空间的限制。大约80%的域名解析都到这里就完成了。故LDNS承担了主要的域名解析工作;
如果LDNS仍然没有命中,就直接到Root Server域名服务器请求解析;
根域名服务器返回给本地域名服务器一个所查询的顶级域名服务器。顶级域名服务器包括.com, .cn, .org
等。全球有十三台根域名服务器。
本地域名服务器再向上一步返回的顶级域名服务器发送请求;
接受请求的顶级域名服务器查找并返回此域名对应的权威域名服务器的地址。
权威域名服务服务器会查询存储的域名和IP的映射关系表,在正常情况下根据域名得到IP地址,连同一个TTL值返回给LDNS域名服务器。
LDNS缓存这个域名和IP,缓存的时间由TTL控制。
把解析的结果返回给用户,用户根据TTL值缓存在本地系统缓存中,域名解析结束。
本地域名服务器也能够缓存顶级域名服务器的地址,因而允许本地DNS绕过查询链中的根域名服务器。
主机向LDNS查询是递归查询,LDNS向根域名服务器查询是迭代查询。
互联网的域名结构:
只要域名解析服务器获得域名-IP映射,即缓存这一映射。一段时间过后,缓存条目失效。本地域名服务器一般会缓存顶级域名服务器的映射,因此根域名服务器不经常别访问。有效时间(两天)
DNS可以使用UDP或者TCP进行传输,使用的端口号都为53.大多数情况下DNS使用UDP进行传输,这就要求域名解析器和域名服务器都必须自己处理超时和重传从而保证可靠性。在两种情况下会使用TCP进行传输:
实现DNS分布式服务器的所有DNS服务器共同存储了资源记录,RR提供了主机名到IP地址的映射。
资源记录(RR)包含了四个字段:name,value,type,ttl
TTL是该记录的生存时间,它决定了资源记录应当从缓存中删除的时间。Name和Value的值取决于Type:
概要如下表所示:
类型 | 功能 |
---|---|
A | 将域名指向一个 IPv4 地址 |
NS | 将子域名指向其它 DNS 服务器解析 |
CNAME | 将域名指向另一个域名 |
MX | 将域名指向邮件服务器地址 |
进程间通信的方法包括管道(PIPE)、消息队列、信号、共享内存以及套接字(Socket)。
管道通常指无名管道,是UNIX系统IPC最古老的形式。
特点:
单个进程的管道几乎没有任何用处。所以,通常调用pipe的进程接着调用fork,这样就创建了父进程与子进程之间的IPC通道。
如果数据从父进程流向子进程,则关闭父进程的读端(fd[0])与子进程的写端(fd[1]);反之,可以使数据流从子进程流向父进程。
FIFO,也称为命名管道,是一种文件类型。
特点:
FIFO的通信方式类似于在进程中使用文件来传输数据,只不过FIFO类型文件同时具有管道的特性。在数据读出时,FIFO同时清除数据,并且“先进先出”。
消息队列,是消息的链接表,存放在内核中。一个消息队列有一个标志符(即队列ID)来标识。
特点:
信号量是一个计数器,信号量用于实现进程间的同步与互斥,而不是存储进程间通信数据。
信号量可以用来控制多个进程对共享资源的访问。若此信号量的值为正,则进程可以使用该资源;进程将信号量减一,表示其使用了一个资源单元。若此信号量的值为0,则进程进入休眠状态,直至信号量位于0.若一个进程不再使用由一个信号量控制的共享资源时,该信号值增1.如果有进程正在休眠等待此信号量,则唤醒它们。
特点:
共享内存,指两个或多个进程共享一个给定的存储区。
特点:
套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器的进程通信。
]]>HTTP无状态是指:HTTP协议对事物处理是没有记忆能力的,也就是说服务器不知道客户端是什么状态。当我们向服务器发送请求后,服务器解析此请求,然后返回对应的响应,服务器负责完成整个过程。这个过程是独立的的,服务器不会记录前后状态的变化,也就是缺失状态记录。这就是说如果后续处理需要前面的信息,就必须重传,这导致需要额外传递一些前面的重复请求,才能获取后续响应,然而这种效果显然太浪费资源了。
于是两种用于保持HTTP连接状态的技术就出现了,即Session和Cookies。Session在服务端,也就是网站的服务器,用来保存用户的会话信息;Cookie在客户端,有了Cookies,浏览器在下次访问网站是会自动附带上它发送给服务器,服务器通过识别Cookies鉴别出是哪个用户,判断是否是登录状态,然后返回对应的响应。
Cookies保存了登录的凭证,有了它,只需要在下次请求中携带Cookies发送请求就不用重新输入用户名,密码等信息重新登录了。所以在爬虫中,一般会将登录成功后获取的Cookies放在请求头中直接请求,而不必重新模拟登录。
Session指有始有终的一系列动作/消息。比如,打电话时,从拿起电话拨号到挂断电话这一过程可以称为一个Session。
而Web中,Session对象用来存储特定用户Session所需的属性和配置信息。这样,当用户在各个Web网页之间跳转时,存储在Session对象的变量不会丢失,而是在整个Session中一直存在下去。当用户请求来自应用程序的web网页时,如果该用户还没有Session,则web服务器会自动创建一个Session对象。而当Session过期或被放弃的时候,服务器就会终止该Session。
Cookies指某些网站为了辨别用户身份,进行Session跟踪而存储在本地终端上的数据。
当客户端第一次请求服务器时,服务器会返回一个请求头中带有Set-Cookie的字段相应给客户端,用来标记是哪个用户,客户端会把Cookies保存起来,Cookies携带了SessionID信息,服务器检查该Cookies即可查找到对应的Session是什么,然后再判断Session来以此辨别用户状态。
成功登录某个网站时,服务器会告诉客户端设置哪些Cookies信息,在后续访问客户端会把Cookies发送给服务器,服务器再找到对应的Session加以判断。如果Session中某些设置登录状态是有效的,就证明用户处于登录状态,此次返回登录之后才可以查看网页内容,浏览器进行解析,用户就可以看到内容了。
传说中会话Cookie是把Cookie放在浏览器内存中,浏览器关闭后该Cookie失效,持久Cookie则会保持到客户端的硬盘中,下次还可以继续使用,长久保持用户登录状态。
传说是假的,过期时间是由Cookie的Max Age或Expire决定的。持久化Cookie是把有效时间设置的比较长,这样下次访问时仍然携带之前的cookie,就可以直接保持登录状态。
对于Session来说,除非程序员通知服务器删除Session,否则服务器会一直保留。
但是在我们关闭浏览器后,浏览器不会主动关闭之前通知服务器它将要关闭,所以服务器不会有机会知道浏览器已经关闭。
大部分Session机制使用会话Cookis来保存SessionID信息,而关闭浏览器后,Cookies消失了,再次连接服务器时,就无法找到原来的Session了。如果服务器设置的Cookies被浏览器保存到硬盘上,或者用某种手段改写浏览器发出HTTP请求头把原来的Cookies发送给服务器,则当再次打开服务器是,仍然能够找到原来的SessionID,仍然可以保存登录状态。
由于关闭浏览器不会使Session被删除,这就需要服务器为Session设置一个失效时间,当距离客户端上一次使用Session的时间超过这个失效时间时,服务器就认为客户端停止了活动,会把Session删除以节省存储空间。
因为有大量的并发访问,为了预防死锁,一般应用中推荐一次封锁法,就是在方法的开始阶段,已经预先知道会用到哪些数据,然后全部锁住,在方法运行之后,再全部解锁。这种方式可以有效的避免循环死锁,但在数据库中却不使用,因为在事务开始阶段,数据库并不知道会用到哪些数据。
数据库遵循的是两段锁协议,将事务分成两个阶段,加锁和解锁阶段(因此叫两段锁):
事务 | 加锁/解锁处理 |
---|---|
begin; | |
insert into test… | 加insert对应的锁 |
update test set… | 加update对应的锁 |
delete from test… | 加delete对应的锁 |
commit | 事务提交时,同时释放insert、update、delete对应的锁 |
这种方式虽然无法避免死锁,但是两段锁协议可以保证事务的并发调度是串行化(串行化很重要,尤其是在数据恢复和备份的时候)
在数据库操作中,为了有效保证并发读取数据的正确性,提出的事务隔离级别。我们的数据库锁,也是为了构建这些隔离级别存在的。
MySQL中常见的有表锁和行锁,也有新加入的元数据锁(meta data lock MDL)等,表锁是对一整张表加锁虽然可分为读锁和写锁,但毕竟是锁住整张表,会导致并发能力下降,一般是做ddl处理时使用。
行锁则是锁住数据行,这种加锁方法比较复杂,但是由于只锁住有限的数据,对于其他数据不加限制,所以并发能力强,MySQL一般都是用行锁来处理数据,这里主要讨论行锁。
在RC级别中,数据的读取都是不加锁的,但是数据的写入、修改是需要加锁的。
为了防止并发过程中的修改冲突,事务A中MySQL给teacher_id=1的数据行加锁,并一直不commit(释放锁),那么事务B也就一直拿不到该行锁,wait直到超时。
但是我们注意到,teacher_id是有索引的,如果是没有索引的class_name呢,那么MySQL会给整张表的所有数据行加行锁,这听起来有点不可思议,但是当SQL运行的过程中,MySQL并不知道哪些数据行是class_name=”初三一班“,如果一个条件无法通过索引快速过滤,存储引擎层面就会将所有记录加锁后返回,再由MySQL_Server层进行过滤。
但在实际使用过程中,MySQL做了一些改进,在MySQL_Server过滤条件,发现不满足后,会调用unlock_row方法,把不满足条件的记录释放锁(违背了二段锁的约束),这样做,保证了最后只有持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。可见即使MySQL,为了效率也是会违反规范的。
这种情况同样适用于MySQL的默认隔离级别RR。所以对一个数据量很大的表做批量修改的时候,如果无法使用相应的索引,MySQL Server过滤数据的时候特别慢,就会出现虽然没有修改某些行的数据,但是它们还是被锁住了的现象。
这是MySQL中InnoDB默认的隔离级别。我们姑且分为”读“和”写“两个模块来讲解。
读就是可重读,可重读这个概念是一事务的多个实例在并发读取数据时,会看到相同的数据行,有点抽象。
很多人搞不清楚不可重复读和幻读的区别,确实这两者有些相似。但不可重复读重点在于update和delete,而幻读的重点在于insert。
如果使用锁机制来实现这两种隔离级别,在可重复读中,该SQL第一次读取到数据后,就将这些数据加锁,其他事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还有可以insert数据提交,这是事务A就会发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效避免幻读、不可重复读、脏读等,但会极大的降低数据库的并发能力。
所以说不可重复度和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题。
上文说的是,是使用悲观锁机制来处理这两种问题,但是MySQL、ORACLE、PostgreSQL等成熟的数据库,处于性能考虑,都是使用了以乐观锁为理论基础的MVCC(多版本并发控制)来避免这种问题。
正如其名,它指的是对外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其他事务无法修改这些数据。修改删除数据时也要加锁,其他事务无法处读取这些数据。
相对悲观锁而言,乐观锁采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据的锁机制实现,以保证操作最大程度的独占性。但随着而来就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。
而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本的记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个字段“version”来实现。读取此数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行对比,如果提交的版本号大于数据表当前版本号,则予以更新,否则认为是过期数据。
在InnoDB中,会在每行数据后添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增。在可重读Repeatable reads事务隔离级别下:
通过MVCC,虽然每行记录都需要额外的存储空间,更多的行检查工作以及一些额外的维护工作,但可以减少锁的使用,大多数读操作都不用加锁,读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行,也只锁住必要行。
在数据库方面的教科书学到,PR是可重复读的,但无法解决幻读,而只有在Serializable级别才能解决幻读。经测试,在MySQL中是不存在这种情况的,在事务C提交后,事务A还是不会读到这条数据。可见在RR级别中,是解决了幻读的问题的。
事务的隔离级别其实都是对于读数据的定义,但到了这里,就被拆成了读和写两个模块来讲解。这主要是因为MySQL中的读,和事务隔离级别中的读,是不一样的。
在RR级别中,通过MVCC机制,虽然让数据变得可重复读,但我们读到的可能是历史数据,是不及时的数据,不是数据库当前的数据!这在一些对于数据的时效性特别敏感的业务中,就很可能出问题。
对于这种读取历史数据的方式,我们叫它快照读,而读取数据库当前版本数据的方式,叫当前读。很显然,在MVCC中:
快照读:即SELECT
SELECT * fron table…
当前读:特殊的读操作,插入/更新/删除操作,属于当前读,处理的都是当前的数据,需要加锁。
SELECT * from table where ?lock in share modes;
SELECT * from table where ? for update;
insert;
update;
delete;
事务的隔离级别实际上都是定义了当前读的级别,MySQL为了减少锁处理(包括等待其他锁)的时间,提升并发能力,引入了快照读的概念,使得select不用加锁。而update、insert这些“当前读”,就需要另外的模块来解决了。
事务的隔离级别虽然只定义了读数据的要求,实际上这也可以说是写数据的要求。上文的“读”,实际上讲的快照读,而这里说的“写”就是当前读了。
为了解决当前读中的幻读问题,MySQL事务使用了Next-Key锁。
Next-key锁是行锁和GAP(间隙锁)的合并,行锁上文已经介绍了,接下来GAP间隙锁。行锁可以防止不同事务版本的数据修改提交时造成数据冲突的情况。但如何避免别的事务插入数据就成了问题。我们可以看看RR级别和RC级别的对比:
RR级别中,事务A在update后加锁,事务B无法插入新数据,这样事务A在update前后读的数据保持一致,避免了幻读,这个锁,就是GAP锁。
不仅用行锁锁住了相应的数据行,同时也在两边的区间,加入了gap锁,这样事务B就无法在这两个区间insert进新数据。
受限于这种实现方式,InnoDB很多时候会锁住不需要锁的区间。
如果使用的是没有索引的字段,那么会给全表加入gap锁。同时,它不能像上文中行锁一样经过MySQL Server过滤自动解除不满足条件的锁,因为没用索引,则这些字段也就没有排序,也就没有区间。除非该事务提交,否则其他事务无法插入任何数据。
行锁防止别的事务修改或删除,GAP锁防止别的事务新增,行锁和GAP锁结合形成的Next-KEY锁共同解决了RR级别在写数据时的幻读问题。
这个级别很简单,读加共享锁,写加排他锁,读写互斥。使用的悲观的理论,实现简单,数据更加安全,但是并发能力非常差。如果你的业务并发特别少或者没有并发,同时又要求数据及时可靠的话,可以使用这种模式。
注意,不要看到select就说不会加锁了,在Serializable这个级别,还是会加锁的。
HTTP有如下安全性问题:
HTTPS不是新协议,而是让HTTP先和SSL(Secure Socket Layer)通信,再由SSL和TCP通信,也就是说HTTPS使用了隧道进行通信。
通过使用SSL,HTTPS具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)
HTTPS采用混合的加密机制,使用非对称秘钥加密用于传输对称秘钥来保证传输过程的安全性,之后使用对称秘钥加密进行通信来保证通信过程的效率。
除了加/解密之外,还可以用加密系统对报文进行签名,以说明是谁编写的报文,同时证明报文未被篡改过。这种技术称为数字签名。
使用数字签名有以下两个好处:
下面的例子说明了A是如何向节点B发送一条报文,并对其进行签名的。
公钥存在伪装的问题,通过使用数字证书来对公钥进行认证。
数字证书认证机构(CA)是客户端与服务器双方都可信赖的第三方机构。
服务器的运营人员向CA提出公开秘钥的申请,CA在判明申请者的身份之后,会对已申请的公开密钥做数字签名,具体过程如下:CA用自己的私钥对服务器申请的公钥已以及相关信息加密,生成数字证书,然后将这个公开秘钥和数字签名发送给服务器。进行HTTPS通信时就多了一个步骤:
由上图可知,HTTPS的通信过程主要分为以下几个步骤:
Map这样的
Key Value
在软件开发中是非常经典的结构,常用于在内存中存放数据。
众所周知HashMap底层是基于数组+链表
组成的,不过jdk1.7和1.8中实现稍有不同,为了便于理解,以下源码分析以JDK1.7为主
内部包含了一个Entry类型的数组table。
1 | transient Entry[] table; |
Entry存储着键值对。它包含了四个字段,从next字段我们可以看出来Entry是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap使用拉链法来解决冲突,同一个链表存放哈希值和散列桶取模运算结果相同的Entry。
下面是1.7中的实现。
1 | static class Entry<K,V> implements Map.Entry<K,V> { |
1 | HashMap<String, String> map = new HashMap<>(); |
这里应该注意到链表的插入是以头插法进行的。例如上面的<K3,V3>不是插在<K2,V2>后面,而是插入在链表头部。
查找需要分成两步进行:
1 | static final int DEFAULT_INITIAL_CAPACITY = 1<<4; |
给定的默认容量为16,负载因子为0.75.Map在使用过程中不断的往里面存放数据,当数量达到了16*0.75=12
时,就需要将当前16的容量进行扩容。而扩容这个操作设计rehash、复制数据等操作,所以非常消耗性能。
因此通常建议能提前预估HashMap的大小,尽量的减少扩容带来的性能损耗。
知晓了基本结构,下面来看看重要的put和get操作。
1 | public V put(K key, V value){ |
HashMap允许插入键为null的键值对,但是因为无法调用null的hashCode()方法,也就无法确定该键值对的桶下标,只能通过强制指定一个桶下标来存放。HashMap使用第0个桶存放键为null的键值对。
使用链表的头插法,也就是新的键值对插在链表的头部,而不是链表的尾部。
1 | void addEntry(int hash, K key, V value, int bucketIndex){ |
1 | public V get(Object key){ |
首先根据key计算出hashCode,然后定位到具体的桶中;
判断该位置是否为链表;
不是链表就根据key,key的hashCode是否相等来返回值;
为链表则需要遍历直到key及hashCode相等时就返回值
啥都没取到就直接返回null。
1 | final int hash(Object k){ |
另x=1<<4,即x为2的四次方,它具有以下性质:
1 | x : 0001 0000 |
令一个数y与x-1做与运算,可以去除y位级表示的第四位以上数:
1 | y : 1011 0010 |
这个性质和y对x取模效果是一样的。我们知道,位运算的代价比求模运算小得多,因此在这种计算时用位运算的话能带来更高的性能。
确定桶下标的最后一步是将key的hash值对桶个数取模:hash%capacity,如果能保证capacity为2的n次方,那么就可以将这个操作转换为位运算。
1 | static int indexFor(int h, int length){ |
设HashMap的table长度为M,需要存储的键值对数量为N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为N/M,因此平均查找次数的复杂度为O(N/M).
为了让查找的成本降低,应该尽可能使得N/M尽可能小,因此需要保证M尽可能大,也就是说table要尽可能大。HashMap采用动态扩容来根据当前的N值来调整M值,使得空间效率和时间效率都能得到保证。
和扩容相关的参数有:capacity,size,threshold和load_factor。
参数 | 含义 |
---|---|
capacity | table的容量大小,默认为16。capacity必须保证为2的n次方 |
size | 键值对的数量 |
threshold | size的临界值,当size大于等于threshold就必须进行扩容操作 |
load_factor | 装载因子,table能够使用的比例,threshold=capacity*load_factor |
当需要扩容时,令capacity为原来的两倍。扩容使用resize()实现,需要注意的是,扩容操作同样需要把oldTable的所有键值对重新插入到newTable中,因此这一步是非常费时的。
1 | void resize(int newCapacity){ |
在进行扩容时,需要把键值对重新放到对应的桶上。HashMap使用了一个特殊的机制。可以降低重新计算桶下标的操作。假设源数组长度为capacity为16,扩容之后new capacity为32.
1 | capacity : 0001 0000 |
对于一个key,
HashMap构造函数允许用户传入的容量不是2的n次方,因为它可以自动地将传入的容量转换为2的n次方。先考虑如何求一个数的掩码,对于10010000,它的掩码为11111111,可以使用如下方法得到:
1 | mask |= mask >> 1 1101 1000 |
mask+1是大于原始数字的最小的2的n次方。
1 | num 1001 0000 |
一下是HashMap中计算数组容量的代码:
1 | static final int tableSizeFor(int cap){ |
1.7有一个很明显需要优化的点,当Hash冲突严重时,在桶上形成的链表会变得越来越长,这样在查询时的效率就会越来越低;时间复杂度为O(N). 因此1.8中重点优化了这个查询效率
和1.7大体上都差不多,有几个重要的区别:
Node的核心组成和HashEntry一样,存放的都是key value hashCode next等数据。
其put操作要比1.7复杂一些:
get方法:
从这两个核心方法可以看出1.8对大链表做了优化,修改为红黑树之后查询效率直接提高到了O(logn)
.
但是HashMap原有的问题也都存在,比如在并发场景下使用容易出现死循环。
1 | final HashMap<String, String> map = new HashMap<String, String>(); |
在HashMap扩容时会调用resize()方法,这里的并发操作容易在一个桶上形成环形链表;这样当获取一个key时,计算出的index正好是环形链表的下标就会出现死循环。
HashMap有以下几种遍历方式:
1 | Iterator<Map.Entry<String, String>> entryIterator = map.entryset().iterator(); |
强烈建议使用第一种EntrySet进行遍历。第一种可以把key value同时取出,第二种还得需要通过key去一次value,效率较低。
总结:无论是 1.7 还是 1.8 其实都能看出 JDK 没有对它做任何的同步操作,所以并发会出问题,甚至 1.7 中出现死循环导致系统不可用(1.8 已经修复死循环问题)。
下面就可以引入ConcurrentHashMap了。
1 | static final class HashEntry<K,V> { |
ConcurrentHashMap和HashMap实现上类似,核心数据如value,以及链表都是由volatile修饰,保证了获取时的可见性。最主要的差别是ConcurrentHashMap采用了分段锁(Segment),每个分段锁维护者几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是Segment的个数)。一个线程占用所访问一个Segment时,不会影响到其它的Segment。
Segment继承自ReentrantLock。
1 | static final class Segment<K, V> extends ReentrantLock implements Serializable{ |
默认的并发级别为16,也就是说默认创建16个Segment。
在ConcurrentHashMap中,线程对映射表做读操作时,一般情况下不需要加锁就可以完成。对容器做结构性修改的操作才需要加锁。以put操作为例:
首先根据key计算出对应的hash值:
1 | public V put(K key, V value){ |
然后,根据hash值找到对应的Segment对象。
1 | //使用 key 的散列码来得到 segments 数组中对应的 Segment |
最后,在这个Segment中执行具体的put操作:
1 | V put(K key, int hash, V value, boolean onlyIfAbsent){ |
注意: 这里的加锁操作是真的(键的hash值对应的)某个具体的Segment,锁定的是该Segment而不是整个ConcurrentHashMap。因为插入键值对操作只是在Segment包含的某个桶中完成,不需要锁定整个ConcurrentHashMap。此时其他写线程对另外15个Segment的加锁并不会因为当前线程对这个Segment的加锁而阻塞。同时,所有读线程几乎不会因为本线程的加锁而阻塞(除非读线程刚好读到这个 Segment 中某个 HashEntry 的 value 域的值为 null,此时需要加锁后重新读取该值
)。
对比HashTable和由同步器包装的HashMap每次只能有一个线程执行读或写操作,ConCurrentHashMap在并发访问性能上有了质的提高。在理想状态下,ConCurrentHashMap可以支持16个线程执行并发写操作(如果并发级别设置为16),及任意数量线程的读操作。
1 | public V get(Object key){ |
get逻辑比较简单,只需要将key通过Hash之后定位到具体的Segment,再通过一次Hash定位到具体的元素上。由于HashEntry中value属性是用volatile关键词修饰的,保证了内存可见性,所以每次获取到的都是最新值。
ConCurrentHashMap的get方法是非常高效的,因为整个过程都不需要加锁。
1.7已经解决了并发问题,并且能支持N个Segment的并发度,但是依然存在HashMap在1.7版本中的问题,即查询遍历链表效率太低。因此1.8做了一些数据结构上的调整。
底层的数据结构:
同时抛弃了原有的Segment分段锁,而采用了CAS+synchronized来保证并发安全性。
1.8在1.7的数据结构上做了大的改动,采用红黑树之后可以保证查询效率O(logn),甚至取消了ReentrantLock改为了synchronized,这样可以看出在新版的JDK中对synchronized优化是很到位的。
[1] HashMap? ConcurrentHashMap? 相信看完这篇没人能难住你!
[2] 技术面试必备基础知识 )
]]>为了不断优化推荐效果,今日头条每天要存储和处理海量数据。假设有这样一种场景:我们对用户按照它们的注册时间先后来标号,对于一类文章,每个用户都有不同的喜好值,我们会想知道某一段时间内注册的用户(标号相连的一批用户)中,有多少用户对这类文章喜好值为k。因为一些特殊的原因,不会出现一个查询的用户区间完全覆盖另一个查询的用户区间(不存在L1<=L2<=R2<=R1)。
输入: 第1行为n代表用户的个数 第2行为n个整数,第i个代表用户标号为i的用户对某类文章的喜好度 第3行为一个正整数q代表查询的组数 第4行到第(3+q)行,每行包含3个整数l,r,k代表一组查询,即标号为l<=i<=r的用户中对这类文章喜好值为k的用户的个数。 数据范围n <= 300000,q<=300000 k是整型
输出:一共q行,每行一个整数代表喜好值为k的用户的个数
这题的题目很长,容易把人弄晕。给了一个数组,其中索引i对应的值代表员工编号为i的喜好值val;然后给一个查询条件,员工编号的范围[l, r],以及喜好值k,判断在这个范围内有多少喜好值为k的员工。我们输出打印员工的人数即可。
看到这题,就觉得简直不能更简单了好么!用一个数组把员工的喜好值存起来,再用一个for循环遍历索引为[left,right]中的喜好值即可。无奈理想很丰满,现实很骨感,一提交运行就给你一个50%的通过率,原因是超时。可能是员工的人数过多,但喜好值呢,是一个比较固定的范围。
因此想办法改进,很直观的方法是用一个map把喜好和员工编号对应起来,每次查询找到喜好值为k的所有员工,员工的编号是有序的,二分查找所有在范围内的员工即可。二分查找时,找到大于等于员工编号左边界的最小值,和小于等于员工编号的最大值,根据二者的差值求出员工数目。
代码如下
1 | import java.util.*; |
作为一个手串艺人,有金主向你订购了一条包含n个杂色串珠的手串——每个串珠要么无色,要么涂了若干种颜色。为了使手串的色彩看起来不那么单调,金主要求,手串上的任意一种颜色(不包含无色),在任意连续的m个串珠里至多出现一次(注意这里手串是一个环形)。手串上的颜色一共有c种。现在按顺时针序告诉你n个串珠的手串上,每个串珠用所包含的颜色分别有哪些。请你判断该手串上有多少种颜色不符合要求。即询问有多少种颜色在任意连续m个串珠中出现了至少两次.
第一行输入n,m,c三个数,用空格隔开。(1 <= n <= 10000, 1 <= m <= 1000, 1 <= c <= 50) 接下来n行每行的第一个数num_i(0 <= num_i <= c)表示第i颗珠子有多少种颜色。接下来依次读入num_i个数字,每个数字x表示第i颗柱子上包含第x种颜色(1 <= x <= c)
一个非负整数,表示该手链上有多少种颜色不符需求。
这个题目也很绕,搞得人看不明白就对了。我一度觉得自己智商有问题。给一个手串,手串上有n个珠子,总共有c种颜色,每个珠子可能由0种或多种颜色组成,规则为连续m个珠子中出现了至少两次则这种颜色不符合要求。
我们把题目转换一下,有一个数组,索引i指的是颜色i,每个里面放的是所有出现了的珠子位置,然后判断每个颜色里面的珠子是否是符合要求的。
注意:这里要输出的是有多少种颜色不符合需求,因此每种颜色只要出现了一组不符合要求的位置就可以退出了。
代码如下:
1 | import java.util.ArrayList; |
索引是存储引擎用于快速找到记录的一种数据结构。索引对于良好的性能非常关键,尤其是当表中的数据量越来越大时。
数据库查询是数据库最主要的功能之一.
如果想要理解MySQL中索引是如何工作的,最简单的方法就是去看看一本书的“目录”部分:如果想在一本书中找到某个索引,一般会先看书的“目录”,找到对应的页码。
目前大部分数据库系统及文件系统都采用B-Tree或者B+Tree作为索引结构。这是有一定原因的。首先介绍其数据结构。
为了描述B-Tree,首先定义一条数据记录为一个二元组[key,data],key为记录的键值,对应不同的数据记录,key是互不相同的。data为数据记录出key外的数据。那么B-Tree是满足下列条件的数据结构:
d为大于1的一个正整数,称为B-Tree的度。
h为一个正整数,称为B-Tree的高度。
每个非叶子节点由n-1个key和n个指针组成,其中d<=n<=2d.
每个叶子节点最少包含一个key和两个指针,最多包含2n-1个key和2d个指针,叶节点的指针均为null。
所有叶节点具有相同的深度,等于树高h。
key和指针互相间隔,节点两端是指针。
一个节点中的key从左到右非递减排列[非严格递增]。
所有节点组成树结构。
每个指针要么为null,要么指向另外一个节点。
如果某个指针在节点node最左边且不为null,则其指向节点的所有key小于v(key1),其中v(key1)为node的第一个key的值。(隐隐有点像二叉搜索树,左边比根节点小。右边比根节点大)
如果某个指针在节点node的最右边且不为null,则其指向节点的所有key大于v(keym),其中v(keym)为node的最后一个key的值。
如果某个指针在节点node的左右相邻key分别是keyi和keyi+1且不为null,则其指向节点的所有key小于v(keyi+1)且大于v(keyi);
图为一个d=2的B-Tree示意图,d为2指的是每个指针有两个数据,20,49是大于15且小于56的。
MySQL普遍使用B+Tree实现其索引结构。与B-Tree相比,B+tree有以下不同点:
下面是一个简单B+Tree示意:
由于并不是所有节点都具有相同的域,因此B+Tree中叶节点和内节点一般大小不同。这点与B-Tree中不同节点存放的key和指针可能数量不一致,但是每个节点的域和上限是一致的,所以在实现中B-Tree往往对每个节点申请同等大小的空间。
一般来说,B+Tree比B-Tree更适合外存储索引结构,具体原因与外存储器原理及计算机存取原理有关。
一般在数据库系统或文件系统中使用的B+Tree结构都在经典B+Tree的基础上进行了优化,增加了顺序访问指针。
如图所示,在B+Tree中的每个叶子节点增加一个指向相邻叶子节点的指针,这样就形成了带有顺序访问指针的B+Tree。做这个优化的目的是为了提高区间访问的性能,例如图4要查询key从18到49顶点所有数据记录,当找到18后,只需顺着节点和指针顺序遍历就可以一次性访问到所有数据节点,极大提高了区间查询效率。
红黑树等数据结构也可以用来实现索引,但是文件系统及数据库系统普遍采用B-/+Tree作为索引结构,这是为什么呢?
一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储在磁盘上。这样的话,索引查找过程中中就要产生磁盘IO消耗,相对于内存存取,IO存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是查找过程中磁盘IO操作次数。
根据B-Tree的定义,可知检索一次最多需要访问h个节点,数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次IO就可以完全载入了。为了达到这个目的,在实际实现中还需要如下技巧:
综上所述,用B-Tree作为索引结构效率是非常高的。
而红黑树这种结构,h明显要深得多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用数据的局部性,所以红黑树的IO渐进复杂度为O(h),效率明显比B-Tree差很多。
除此之外,B+Tree更适合外存索引,原因和内节点出度d有关。由上面分析可以看到,d越大索引的性能越好,而出度的上限取决于节点内key和data的大小。
由于B+Tree内节点去掉了data域,因此可以拥有更大的出度,拥有更好的性能。
不同存储引起对索引的实现方式是不同的,这里主要介绍MyISAM和InnoDB两个存储引擎的索引实现方式。
MyISAM引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址。下图是MyISAM索引的原理图:
这里假设表一共有三列,假设我们以col1为主键,则图8是一个MyISAM表的主索引示意。可以看出,MyISAM的索引文件仅仅保存数据记录的地址。在MyISAM中,主索引和辅助索引在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复。如果我们在col2上建立一个辅助索引,则此索引的结构如下图所示:
注意看辅助索引的和主索引的差别,辅助索引叶子节点存放的key是辅助字段的值,而主索引叶子节点存放的key是主键的值。
因此MyISAM中索引检索的算法首先按照B+Tree搜索算法搜索索引,如果指定的key存在,则取出其data域的值,然后以data域的值为地址,读取相应的数据记录。
MyISAM的索引方式也叫做非聚集索引。
虽然InnoDB也使用B+Tree作为索引结构,但实现方式却与MyISAM截然不同。
第一个重大区别就是InnoDB的数据文件本身就是索引文件。从上文知道,MyISAM索引文件和数据文件是分离的,索引文件仅保存数据文件的地址。而在InnoDB中,表数据文件本身就是按B+tree组织的一个索引结构。这颗树的叶节点data域保存了完整的数据记录,这个索引的key是数据表的主键。
图10是InnoDB主索引(同时也是数据文件)的示意图,可以看到叶节点包含了完整的数据记录。这种索引叫做聚集索引。因为InnoDB本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键。如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整型。
第二个与MyISAM索引不同的是InnoDB的辅助索引data域存储相应记录主键的值而不是地址。换句话说,InnoDB的所有辅助索引都引用主键作为data域。如下图所示:
聚集索引这种实现方式使得按主键搜索十分高效,但是辅助索引搜索需要检索两遍索引:通过辅助索引获得主键,然后用主键到主索引中检索获得记录。
了解不同存储引擎的索引实现对于正确使用和优化索引都非常有帮助,例如知道了InnoDB的索引实现后,就很容易明白为什么不建议使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。再例如,用非单调的字段作为主键在InnoDB中不是一个好主意,因为InnoDB数据本间本身是一颗B+Tree,非单调的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。
MySQL的优化主要分为结构优化和查询优化。本章讨论的高性能索引策略主要属于结构化优化范围。
高效使用索引的首要条件是知道什么样的查询会使用到索引,这个问题和B+Tree中的“最左前缀原理”有关。主键有三个emp_no, title, from_date
.
很明显,当按照索引中的所有列进行精确匹配(这里精确匹配指“=”或“IN”匹配)时,索引可以被用到。理论上索引对顺序是敏感的,但是由于MySQL的查询优化器会自动调整where子句的条件顺序以使用适合的索引。
当查询条件精确匹配索引的左边连续一个或几个列时,索引可以被用到,但是只用到一部分,即条件所组成的最左前缀。
此时索引使用情况和情况二相同,因为title未提供,所以查询只用到了索引的第一列,而后面的from_date虽然也在索引中,但是由于title不存在而无法和左前缀连接,因此需要对结果进行扫描过滤from_date(这里由于emp_no唯一,所以不存在扫描)。如果想让from_date也使用索引而不是where过滤,可以增加一个辅助索引<emp_no, from_date>,此时上面的查询会使用这个索引。除此之外,还可以使用一种称之为“隔离列”的优化方法,将emp_no与from_date之间的“坑”填上。
“填坑”后性能提升了一点。如果经过emp_no筛选后余下很多数据,则后者性能优势会更加明显。当然,如果title的值很多,用填坑就不合适了,必须建立辅助索引。
由于不是最左前缀,索引这样的查询显然用不到索引。
EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND title LIKE 'Senior%';
如果通配符%不出现在开头,则可以用到索引,但根据具体情况不同可能只会用其中一个前缀。
范围列可以用到索引(必须是最左前缀),但是范围列后面的列无法用到索引。同时,索引最多用于一个范围列,因此如果查询条件中有两个范围列则无法全用到索引。
很不幸,如果查询条件中含有函数或表达式,则MySQL不会为这列使用索引(虽然某些在数学意义上可以使用)。由于查询条件是一个表达式,MySQL无法为其使用索引。看来MySQL还没有智能到自动优化常量表达式的程度,因此在写查询语句时尽量避免表达式出现在查询中,而是先手工私下代数运算,转换为无表达式的查询语句。
既然索引可以加快查询速率,是不是只要是查询语句就建上索引?答案是否定的,因为索引虽然加快了查询速度,但索引也是有代价的:索引文件本身要消耗存储空间,同时索引会加重插入、删除和修改记录时的负担,另外,MySQL在运行中也要消耗资源维护索引,因此索引并不是越多越好。一般两种情况下不建议建索引:
第一种情况是表记录比较少,例如一两千条甚至只有几百条记录的表,没必要建索引,让查询做全表扫描好了。至于多少条记录才算多,这个人有个人的看法,记录数不超过2000可以考虑不建索引,超过2000条才可以酌情考虑索引。
另一种不建议建索引的情况是索引的选择性较低。所谓索引的选择性是指不重复的索引值与表记录数的比值。显然选择性的取值范围为(0,1],选择性越高的索引价值越大,这是由B+Tree的性质决定。
有一种与索引选择性有关的索引优化策略叫做前缀索引,就是用列的前缀代替整个列作为索引key,当前缀长度合适时,可以做到既使得前缀索引的选择性接近全列索引,同时应为索引key变短而减少了索引文件的大小和维护开销。
前缀索引兼顾索引大小和查询速度,但是其缺点是不能用于ORDER BY和GROUP BY操作,也不能用于Covering index(即当索引本身包含查询所需全部数据时,不再访问数据文件本身)
在使用InnoDB存储引擎时,如果没有特别的需要,请永远使用一个与业务无关的自增字段作为主键。
上文讨论过InnoDB的索引实现,InnoDB使用聚集索引,数据记录本身被存于主索引的叶子节点上。这就要求同一个叶子节点内(大小为一个内存页或磁盘页)的各条数据记录按主键顺序存放,因此每当有一条新的记录插入时,MySQL会根据其主机那将其插入到适当的节点和位置,如果页面到达装载因子(InnoDB默认为15/16),则开辟一个新的页(节点)。
如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。
这样就会形成一个紧凑的索引结构,近似顺序填满。由于每次插入时也不需要移动已有数据,因此效率很高,也不会增加很多开销在维护索引上。
如果使用非自增主键(如身份证或学号等)由于每次插入主键的值近似于随机,因此每次新记录都要被插到现有索引页的中间某个位置。
因此MySQL不得不为了将新记录插到合适位置而移动数据,甚至目标页面可能被回写到磁盘上而从缓存中清掉,此时又要从磁盘中读回来。这增加了很多开销,同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面。
因此,只要可以,请尽量在InnoDB上采用自增字段作为主键。
确保一个类只有一个实例,并提供该实例的全局访问点。Singleton通常被用来代表那些本质上唯一的系统组件,比如窗口管理器或者文件系统。
使用一个私有构造函数,一个私有静态变量以及一个公有静态函数来实现。私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。
1 | public class Singleton { |
这种实现方式虽然不是最好的实现方式,但是是最常用的单例的实现方式。因为类一开始即被装载,所以不用担心线程安全的问题。但是缺点就是如果不使用这个类,就会存在内存浪费的问题。
加锁操作只需要对实例化部分的代码进行,只有当instance没有被实例化时,才需要进行加锁。
1 | public class Singleton { |
为什么在锁的内部还有再加一层if判断呢,如果只有一个if语句,在instance==null的情况下,如果两个线程都进入了if语句块中,虽然在if语句块中有加锁操作,但两个线程都会执行实例化instance= new Singleton()
这条语句,只是时间问题。那么就会进行两次实例化。破坏了单例模式。因此需要两个if语句:第一个语句用来避免instance已经被实例化后的加锁操作,第二个if语句进行了加锁,只有一个线程进入,不会出现多次实例化的情况。
当Singleton类被加载时,静态内部类SingletonHolder没有被加载进内存。只有当调用getInstance()方法时从而触发SingletonHolder.instance时SingletonHolder才会被加载。此时初始化INSTANCE实例,并且JVM确保INSTANCE只能被实例化一次。
这种方式不仅具有延迟初始化的好处,而且由JVM提供了对线程安全的支持。
1 | public class Singleton { |
JVM装载内部类并不是程序启动就装载,而且装载内部类是线程安全的。所以这个单例模式真正意义上实现了懒加载与线程安全且节省了内存。
实现单例模式只需编写一个包含单个元素的枚举类型:
1 | public enum Singleton { |
简洁,且无偿地提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。可以防止反射攻击,防止反序列化重新创建新的对象。
[1] Effective Java 中文版
[2] 技术面试必备基础知识
[3] CodeSheep单例模式
]]>主要是点来点去的。
点击IDEA菜单栏的tools
->Deplment
,输入服务器的账号和密码,测试一下,查看是否连接成功。这里连接道康服务器,之前已经输入过账号和密码,连接成功。
tools
->start SSH session
,终端进入道康服务器。
输入指令ps aux | grep visualcensus
过滤查找之前运行的进程号。
杀死之前的那个进程kill 1215
,1215为上面查找的进程id。
当然要配合数据库,但是因为男神操作太快,这里就没能记录下来。
右侧边状态栏,点击Maven
,在Lifecycle
中点击package
,将整个项目打包。等待打包完成。
直接将打包好的额target包下面的.jar
文件用鼠标拖到Remote Host
中的visualcensus中,在命令行输入nohup java -jar visualcensusserver-0.0.1-SNAPSHOT.jar > /dev/null 2>&1 &
y运行java工程。
难度:中等 思路:递归
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例:
1 | 给定 1->2->3->4, 你应该返回 2->1->4->3. |
链表中节点的交换是传统问题,两个指针解决问题,唯一要注意的是不能在交换过程中把链表给断开了,否则岂不是得不偿失?暴力法本可以解决一切,但是优雅的方法是递归,把相邻节点的交换搞定,然后递归把所有的节点串起来,简直是美滋滋啊。
1 | class Solution: |
复杂度分析
O(N)
]]>难度: hard模式 思路:优先级队列
合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。
示例:
1 | 输入: |
首先是hard模式,说明不能轻易的解决这个问题。认真思考了一个小时,觉得已经理清楚思路,然而苦于不知该如何实现自己的思路。试了几种方法,反而是原地打转,于是决定参考一下答案。答案实现了我的思路,原来使用了一种之前没有用过的数据结构,优先级队列(PrivirotyQueue
),这一篇专栏简单介绍了优先级队列,对于理解这题,简直是正中靶心。
思路如下:
用三个指针指向k个链表的头部,比较指针中的值,将最小的值加入到结果中,然后指针后移,直到所有的指针为空。由于是列表,无法直接得到所有链表的头部,所以只能通过优先级队列来做。遍历列表,将头指针和头指针的val组成一个元组入队列,出队列时头指针的val作为评判其优先级的标准。注意事项,当队列中的元组优先级(即头指针的val)相同时,将会把指针这个对象作为比较优先级的评判标准,但是对象在python中是不能直接比较的,会引起代码崩溃,元组之间的元素用逗号隔开,不能有空格,否则代码也会崩溃。
1 | from queue import PriorityQueue as PQ |
复杂度分析
O(N log k) :比较大小的时间花费将被减少到O(log k),因为优先级队列的内部实现机制用到了堆,但是找到最小值的节点仅仅花费 O(1), 在最终的链表中有K个节点。
]]>难度:easy 思路:硬算
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
1 | 输入:1->2->4, 1->3->4 |
有序链表的合并是归并排序中的经典算法,这题采用常规思路即可解决问题,唯一要注意的点是当指针p、q中任一指针为空时后续应该如何操作。
1 | def mergeTwoLists(self, l1, l2,): |
时间复杂度分析:
O(M+N) M为l1链表的长度,N为l2链表的长度
]]>难度:简单 思路:栈
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串,判断字符串是否有效。
有效字符串需满足:
注意空字符串可被认为是有效字符串。
示例:
1 | 输入: "{[]}" |
这是一题难度为简单的题,但我仍旧很久没有思路,就是没有想到利用栈的特性,本题是一道经典的栈的特性的题目,想到栈,就成功了一大半。
1 | def isValid(self, s): |
复杂度分析:
O(N) N为字符串的长度
]]>难度:中等 思路:窗口法
给定一个链表,删除链表的倒数第n
个节点,并且返回链表的头结点。
示例:
给定一个链表:1->2->3->4->5, 和 n = 2.
当删除了倒数第二个节点后,链表变为 1->2->3->5.
说明:给定的n保证是有效的。
进阶:你能尝试一趟扫描实现吗?
本题和链表有关,链表是最基本的数据结构,链表中的元素可存储在内存的任何地方,链表的每个元素都存储了下一个元素的地址,从而使一系列随机的内存地址串在一起。链表的优势在于插入和删除元素,可以快速完成而不需要移动其他元素。链表由一个个节点组成,节点有两部分,节点的值和下一个节点的地址,尾节点的下一个节点的地址为空。在Python中即None
,想要通过一趟扫描删除倒数第n个节点,最有效的方法就是窗口法,指针p,q
之间间隔n个节点,然后指针p、q
同步往尾部移动,当q
到达尾节点时,p
后的节点即我们需要删除的目标节点。将p内节点的地址指向下下个节点,删除目标节点,任务完成。
函数传进来的参数和传出去的值都是头结点,在删除节点时,有一个问题需要考虑,怎么保证下下个节点是存在的,如果恰好要删除的就是头节点,是否在程序中考虑到了?其实我刚开始也没考虑,就把示例传进去有正确结果返回高兴的屁颠屁颠以为自己做完了,但上传上去之后被特殊情况搞得晕头转向,仔细分析了很长时间才把head、p、q
之间的关系理清楚。
1 | def removeNthFromEnd(self, head, n): |
复杂度分析:
O(N) N为链表的节点个数
]]>难度:中等 思路:头尾逼近法
给定一个包含n
个整数的数组nums
和一个目标值target
,判断nums中是否存在四个元素a,b,c,和d,使得a+b+c+d
的值与target相等?找出所有满足条件且不重复的四元组。
注意:答案中不可以包含重复的四元组。
示例:
给定数组nums=[1, 0, -1, 0, -2, 2], 和target=0。
满足要求的四元组集合为:
[[-1 ,0, 0, 1],
[-2, -1, 1, 2],
[-2 ,0, 0, 2]]
在LeetCode的题目顺序有一个由浅入深的过渡,这题的解决方法完全可以参照三个数的求和问题。让我们来回顾一个三个数的求和问题,三个数的求和与四个数的求和表面上是一模一样的,无非是把三个数替换为四个数,所以在解决方式上内核机制也是相同的。
数的求和问题,最简单粗暴的方法就是把给定的数组for循环遍历几次,找到所有符合要求的数,但是因为时间开销过大,提交的时候不被通过,所以只能找其他的渠道。第一步,将所给的数按照从小到大排序,Python有内置的排序函数,这里就直接调用了。
在多个数求和问题中,使用的方法为两头法,具体实现方法如下。首先固定第一个数,从数组的头部开始,求出目标数target与第一个数的差值diff;然后固定第二个数,求出之前的差值diff与当前数的差值作为新目标goal;然后使用头尾法,如果头尾的和小于goal,则头往后移,如果头尾的和大于goal,则尾往前移(经过排序后,尾部的数大头部的数小),如果头尾的和正好等于goal,那么我们找到了目标数,把四个数存到结果中,同时往中间移动头尾的坐标,头尾位置下标相等时此次遍历结束;然后依次移动前两个固定的数。
这题确定了算法,还是有很多细节需要推敲,而这些细节有些时候比算法本身更花时间。在题目描述中,它特地提出让我们注意解决重复元素的问题,解决重复问题有两个思路:一是处理返回的数组,将其中重复的元素去掉;二是从源头上解决问题,在生成结果数组时,一旦发现重复就不再添加入返回数组。这题真正的难点就在这里了,怎么样才能最有效的去除重复元素呢?这里只提供一个巧妙的思路,重复的元素从何而来,归根结底是因为数组中本身就有重复的元素,在求和的时候,当我们移动到下一个元素的时候,检测当前元素与上一个元素是否相等,这里为什么是与前一个元素作比较而不是与后一个元素作比较,这是有原因的,如果与后一个元素作比较的话,那么就会漏掉一种情况,这两个数相等但是他们的和就是我们想找的,如果与后一个元素作比较的话这个数还没有参与计算就会被跳过。结果会错误。(注意,当我们访问一个元素的时候要确保这个元素存在否则就会报错),如果相等就跳过此次循环。(需要确保每个不重复元素都被访问到)
首先要对输入的数组进行长度判断,避免输入空数组时后续进行不存在位置的的访问导致程序崩溃;其次是重复元素的问题,在元素移动过程中,为了不产生重复的四个数组合,固定的第一个数往后移动时,需要考虑当前数与上一个数是否相等,如果相等,则使用continue
语句跳过此次循环。固定的第一个数也存在一模一样的问题。其次是首尾移动时,当检测到首尾之和相等时,头部和尾部的位置都往中间移,这时头尾分别与后一个数作比较,如果相等的话,头尾坐标继续往中间靠。(这里检测到头尾之和与目标相等时,不存在一个数还没用到就被丢弃,所以可以与后一个数作比较)
1 | def fourSum(self, nums, target): |
复杂度分析:
耗时还是非常多的,排序+O(N^3^)。
]]>难度:中等 思路:递归
给定一个仅包含数字2-9
的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意1
不对应任何字母。
示例:
输入:“23”
输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”]
拿到这个题目,分析我们要解决的问题,题目给出了数字到字母的映射,故首先要把映射关系一一对应出来,在Python中很容易想到字典,把映射关系先存起来,唯一值得推敲的是这里的值的类型是存成字符串"abc"
还是数组['a','b','c']
呢?看到电话按键我们很容易想到字符串,但研究我们最终的输出结果类型,存为数组类型更方便后边的计算,如下:
1 | dict_map = {'2':['a','b','c'], '3':['d','e','f'], |
第二步来到了怎么解决这个题目,这个题目看起来很简单,实际上也很简单但就需要那么灵光一现,首先把它的结构图画出来,本质上是一个全组合问题,通过for循环遍历给出的数字所对应的字母,然后内部嵌套for循环遍历下一个数字对应的字母,就这样层层嵌套,唯一的难点是我们无法提前预知需要嵌套多少层for循环。
灵光乍现了,递归函数能够很好的解决这种循环问题。通过认真分析,发现这题用递归是最棒的方法。首先来复习一下递归:
递归将问题分解为越来越小的子问题,直到问题的规模小到可以被直接解决。每个递归函数都有两个部分:基线条件和递归条件 。递归条件指的是函数调用自己,而基线条件是指函数不再调用自己,从而避免无限循环。
如果你还对递归算法有疑问的话,试着写一个关于某个数阶乘的递归算法,fact(n) return n*(n-1)*(n-2)...
规定0!=1
,最合适的基线条件什么呢?
分析此题,基线条件可以是待访问的数字个数为1时,返回这个键的映射值;递归条件为访问当前访问数字的第一个数字,然后for循环遍历这个数字对应的字母,在for循环内部,将数字的第一位去掉然后调用函数本身。然后for循环遍历返回值。
完整代码如下:
1 | class Solution: |
写在前面:因项目需要,故在网上淘了一个红外遥控模块,在树莓派上学习一下红外遥控。
38K通用红外遥控器,采用NEC编码格式,传输距离大于八米,比较适合日常开发,使用方便。
红外接收模块引脚说明:S为OUT引脚,中间为VCC,-为GND。根据模块上的标识来接。
插到树莓派上,S接树莓派的12引脚(物理引脚编码),其BCM编码为18;VCC接3.3V;GND接树莓派任意GND引脚即可。模块实拍如下图所示:
LIRC(Linux Infrared remote control)是一个Linux系统下开源的软件包,用来从远程通用红外设备上接收和发送红外信号。可以解码和发送红外信号。
通过SSH连接树莓派,安装lirc:
1 | sudo apt-get install lirc |
因为lirc版本更新的原因,不同的版本修改的配置文件不同,输入lircd -v
查看lirc的版本,我是0.9.4.c,修改/etc/lirc/lirc_options.conf
文件,用vi
进入修改:
1 | [lircd] |
/boot/config.txt
用vi
进入文件内部,找到dtoverlay
并修改如下:
1 | dtoverlay=lirc-rpi,gpio_in_pin = 18 |
这里18对应树莓派BCM编码的gpio接口。
1 | sudo /etc/init.d/lircd restart |
至此,lirc
软件配置完成
命令行输入以下命令,关闭lirc
:
1 | sudo kill $(pidof lircd) |
1 | mode2 -d/dev/lirc0 |
用红外遥控器,对着接收器按下任意按键,屏幕会打印类似下面的内容,说明红外接收功能正常。
1 | space 562 |
在这里我卡壳了,接收不到任何内容,反复检查了前面的步骤,确定无误后继续谷歌,找到了这篇博客^1,之前输出随意找了一个IO口,但是都没有用,把OUT连接到GPIO pin12上,然后执行
1 | sudo dmesg | grep -i lirc |
发现有内容了,怀疑是红外遥控的输出引脚有指定。
首先查看有哪些按键名并记录,输入:
1 | sudo irrecord --list-namaspace |
我用的几个键名是:
按键 | 按键名 |
---|---|
1 | KEY_1 |
2 | KEY_2 |
3 | KEY_3 |
4 | KEY_4 |
5 | KEY_5 |
6 | KEY_6 |
7 | KEY_7 |
8 | KEY_8 |
9 | KEY_9 |
0 | KEY_0 |
* | KEY_STAR |
# | KEY_PUND |
↑ | KEY_UP |
↓ | KEY_DOWN |
← | KEY_LEFT |
→ | KEY_RIGHT |
OK | KEY_OK |
执行红外线编码录制命令:
1 | sudo irrecord -d /dev/lirc0 ~/lircd.conf |
刚开始需要输入文件名称,最终会根据此名称保存对应的文件名,我的文件名为pi-key
,然后会有一堆英文提示出来,继续回车,会让你按按键,每个按键保证屏幕上输出一个.
,一直按保证所有的按键都被按到,全部按过一遍之后就不停的按最后按的那个按键
然后就会弹出第二轮按键录入,这时也是不停按,要有耐心,循环按,使劲按,直到弹出需要你输入下一个按键的名字为止。如下:
这时依次录入按键名字,然后按下对应的按键,输入按键名字不能输删除,如果输错了也不要紧,按回车重新输入即可。将所有的按键都录入,这一步就结束了。成功之后会在~/目录下生成pi.lircd.conf
这个文件,把这个文件放到/etc/lirc/lircd/lircd.conf.d/
这个目录里即可,命令行输入:
1 | sudo cp ~/pi.lircd.conf /etc/lirc/lircd.conf.d/ |
完成后重启树莓派。
运行编写的Python代码,终端会显示按键的键值。
Python代码如下:
1 | #!/usr/bin/python |
按下遥控按键,终端会显示接收到的按键的键值:
1、网址:https://iaiai.iteye.com/blog/2411532
]]>