​Java面试通关要点汇总集 核心篇

核心篇

数据存储

MySQL 索引使用的注意事项

1.索引不会包含有NULL的列

 只要列中包含有NULL值,都将不会被包含在索引中,复合索引中只要有一列含有NULL值,那么这一列对于此符合索引就是无效的。

2.使用短索引

 对串列进行索引,如果可以就应该指定一个前缀长度。例如,如果有一个char255)的列,如果在前10个或20个字符内,多数值是唯一的,那么就不要对整个列进行索引。短索引不仅可以提高查询速度而且可以节省磁盘空间和I/O操作。

3.索引列排序

 mysql查询只使用一个索引,因此如果where子句中已经使用了索引的话,那么order by中的列是不会使用索引的。因此数据库默认排序可以符合要求的情况下不要使用排序操作,尽量不要包含多个列的排序,如果需要最好给这些列建复合索引。

4.like语句操作

一般情况下不鼓励使用like操作,如果非使用不可,注意正确的使用方式。like %aaa%’不会使用索引,而like aaa%’可以使用索引。

5.不要在列上进行运算

6.不使用NOT IN <>、!=操作,但<,<==>,>=,BETWEEN,IN是可以用到索引的

7.索引要建立在经常进行select操作的字段上。

 这是因为,如果这些列很少用到,那么有无索引并不能明显改变查询速度。相反,由于增加了索引,反而降低了系统的维护速度和增大了空间需求。

8.索引要建立在值比较唯一的字段上。

9.对于那些定义为textimagebit数据类型的列不应该增加索引。因为这些列的数据量要么相当大,要么取值很少。

10.wherejoin中出现的列需要建立索引。

11.where的查询条件里有不等号(where column != ),mysql将无法使用索引。

12.如果where字句的查询条件里使用了函数(如:where DAY(column)=),mysql将无法使用索引。

13.join操作中(需要从多个数据表提取数据时)mysql只有在主键和外键的数据类型相同时才能使用索引,否则及时建立了索引也不会使用。

说说分库与分表设计

垂直分表在日常开发和设计中比较常见,通俗的说法叫做“大表拆小表”

拆分是基于关系型数据库中的“列”(字段)进行的。

通常情况,某个表中的字段比较多,可以新建立一张“扩展表”,将不经常使用或者长度较大的字段拆分出去放到“扩展表”中。

在字段很多的情况下,拆分开确实更便于开发和维护(笔者曾见过某个遗留系统中,一个大表中包含100多列的)。

某种意义上也能避免“跨页”的问题(MySQLMSSQL底层都是通过“数据页”来存储的“跨页”问题可能会造成额外的性能开销,拆分字段的操作建议在数据库设计阶段就做好。

如果是在发展过程中拆分,则需要改写以前的查询语句,会额外带来一定的成本和风险,建议谨慎。

垂直分库在“微服务”盛行的今天已经非常普及了。

基本的思路就是按照业务模块来划分出不同的数据库,而不是像早期一样将所有的数据表都放到同一个数据库中。

系统层面的“服务化”拆分操作,能够解决业务系统层面的耦合和性能瓶颈,有利于系统的扩展维护。而数据库层面的拆分,道理也是相通的。与服务的“治理”和“降级”机制类似,我们也能对不同业务类型的数据进行“分级”管理、维护、监控、扩展等。

众所周知,数据库往往最容易成为应用系统的瓶颈,而数据库本身属于“有状态”的,相对于Web和应用服务器来讲,是比较难实现“横向扩展”的。

数据库的连接资源比较宝贵且单机处理能力也有限,在高并发场景下,垂直分库一定程度上能够突破IO、连接数及单机硬件资源的瓶颈,是大型分布式系统中优化数据库架构的重要手段。

然后,很多人并没有从根本上搞清楚为什么要拆分,也没有掌握拆分的原则和技巧,只是一味的模仿大厂的做法。导致拆分后遇到很多问题(例如:跨库join,分布式事务等)

 

水平分表也称为横向分表

比较容易理解,就是将表中不同的数据行按照一定规律分布到不同的数据库表中(这些表保存在同一个数据库中),这样来降低单表数据量,优化查询性能。

最常见的方式就是通过主键或者时间等字段进行Hash和取模后拆分。

水平分表,能够降低单表的数据量,一定程度上可以缓解查询性能瓶颈。但本质上这些表还保存在同一个库中,所以库级别还是会有IO瓶颈。所以,一般不建议采用这种做法。

 

水平分库

水平分库分表与上面讲到的水平分表的思想相同,唯一不同的就是将这些拆分出来的表保存在不同的数据中。这也是很多大型互联网公司所选择的做法。

某种意义上来讲,有些系统中使用的冷热数据分离”(将一些使用较少的历史数据迁移到其他的数据库中。而在业务功能上,通常默认只提供热点数据的查询),也是类似的实践。

 

在高并发和海量数据的场景下,分库分表能够有效缓解单机和单库的性能瓶颈和压力,突破IO、连接数、硬件资源的瓶颈。当然,投入的硬件成本也会更高。同时,这也会带来一些复杂的技术问题和挑战(例如:跨分片的复杂查询,跨分片事务等)。

以上摘抄自: http://www.infoq.com/cn/articles/key-steps-and-likely-problems-of-split-table

分库与分表带来的分布式困境与应对之策

数据迁移与扩容问题

前面介绍到水平分表策略归纳总结为随机分表和连续分表两种情况。

连续分表有可能存在数据热点的问题,有些表可能会被频繁地查询从而造成较大压力,热数据的表就成为了整个库的瓶颈,而有些表可能存的是历史数据,很少需要被查询到。

连续分表的另外一个好处在于比较容易,不需要考虑迁移旧的数据,只需要添加分表就可以自动扩容。

随机分表的数据相对比较均匀,不容易出现热点和并发访问的瓶颈。但是,分表扩展需要迁移旧的数据

针对于水平分表的设计至关重要,需要评估中短期内业务的增长速度,对当前的数据量进行容量规划,综合成本因素,推算出大概需要多少分片。

对于数据迁移的问题,一般做法是通过程序先读出数据,然后按照指定的分表策略再将数据写入到各个分表中。

 

表关联问题

在单库单表的情况下,联合查询是非常容易的。但是,随着分库与分表的演变,联合查询就遇到跨库关联和跨表关系问题

在设计之初就应该尽量避免联合查询,可以通过程序中进行拼装,或者通过反范式化设计进行规避。

 

分页与排序问题

一般情况下,列表分页时需要按照指定字段进行排序。

在单库单表的情况下,分页和排序也是非常容易的。

但是,随着分库与分表的演变,也会遇到跨库排序和跨表排序问题。

为了最终结果的准确性,需要在不同的分表中将数据进行排序并返回,并将不同分表返回的结果集进行汇总和再次排序,最后再返回给用户。

 

分布式事务问题

随着分库与分表的演变,一定会遇到分布式事务问题,那么如何保证数据的一致性就成为必须面对的问题。

目前,分布式事务并没有很好的解决方案,难以满足数据强一致性

一般情况下,使存储数据尽可能达到用户一致,保证系统经过一段较短的时间的自我恢复和修正,数据最终达到一致。

 

分布式全局唯一ID

在单库单表的情况下,直接使用数据库自增特性来生成主键ID,这样确实比较简单。

在分库分表的环境中,数据分布在不同的分表上,不能再借助数据库自增长特性。

需要使用全局唯一 ID,例如 UUIDGUID等。

 

摘抄自:http://blog.csdn.net/jiangpingjiangping/article/details/78069480

说说 SQL 优化之道

一、一些常见的SQL实践

1)负向条件查询不能使用索引

select from order where status!=0 and stauts!=1

not in/not exists都不是好习惯

可以优化为in查询:

select from order where status in(2,3)

 

2)前导模糊查询不能使用索引

select from order where desc like '%XX'

而非前导模糊查询则可以:

select from order where desc like 'XX%'

 

3)数据区分度不大的字段不宜使用索引

select from user where sex=1

原因:性别只有男,女,每次过滤掉的数据很少,不宜使用索引。

经验上,能过滤80%数据时就可以使用索引。

对于订单状态,如果状态值很少,不宜使用索引,如果状态值很多,能够过滤大量数据,则应该建立索引。

 

4)在属性上进行计算不能命中索引

select from order where YEAR(date) < = '2017'

即使date上建立了索引,也会全表扫描,可优化为值计算:

select from order where date < = CURDATE()

或者:

select from order where date < = '2017-01-01'

 

二、并非周知的SQL实践

5)如果业务大部分是单条查询,使用Hash索引性能更好,例如用户中心

select from user where uid=?

select from user where login_name=?

原因:B-Tree索引的时间复杂度是O(log(n))Hash索引的时间复杂度是O(1)

 

6)允许为null的列,查询有潜在大坑

单列索引不存null值,复合索引不存全为null的值,如果列允许为null,可能会得到“不符合预期”的结果集

select from user where name != 'shenjian'

如果name允许为null,索引不存储null值,结果集中不会包含这些记录。

所以,请使用not null约束以及默认值

 

7)复合索引最左前缀,并不是值SQL语句的where顺序要和复合索引一致

用户中心建立了(login_name, passwd)的复合索引

select from user where login_name=? and passwd=?

select from user where passwd=? and login_name=?

都能够命中索引

select from user where login_name=?

也能命中索引,满足复合索引最左前缀

select from user where passwd=?

不能命中索引,不满足复合索引最左前缀

 

8)使用ENUM而不是字符串

ENUM保存的是TINYINT,别在枚举中搞一些“中国”“北京”“技术部”这样的字符串,字符串空间又大,效率又低。

 

三、小众但有用的SQL实践

9)如果明确知道只有一条结果返回,limit 1能够提高效率

select from user where login_name=?

可以优化为:

select from user where login_name=? limit 1

原因:你知道只有一条结果,但数据库并不知道,明确告诉它,让它主动停止游标移动

 

10)把计算放到业务层而不是数据库层,除了节省数据的CPU,还有意想不到的查询缓存优化效果

select from order where date < = CURDATE()

这不是一个好的SQL实践,应该优化为:

$curDate = date('Y-m-d');

$res = mysqlquery(

'select from order where date < = $curDate');

原因:

释放了数据库的CPU

多次调用,传入的SQL相同,才可以利用查询缓存

 

11)强制类型转换会全表扫描

select from user where phone=13800001234

你以为会命中phone索引么?大错特错了?

 

末了,再加一条,不要使用select *(潜台词,文章的SQL都不合格 ==),只返回需要的列,能够大大的节省数据传输量,与数据库的内存使用量哟。

 

整理自:https://cloud.tencent.com/developer/article/1054203

MySQL 遇到的死锁问题

死锁(Deadlock

所谓死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

blob.png

产生死锁的四个必要条件:

1) 互斥条件:

一个资源每次只能被一个进程使用。

—— 只有一副钥匙

2 请求与保持条件:

一个进程因请求资源而阻塞时,对已获得的资源保持不放。

—— 拿着红钥匙的人在没有归还红钥匙的情况下,又提出要蓝钥匙

3 不剥夺条件:

进程已获得的资源,在未使用完之前不能被剥夺,只能在使用完时由自己释放。

—— 除非归还了钥匙,不然一直占用着钥匙

4 循环等待条件:

若干进程之间形成一种头尾相接的循环等待资源关系资源的环形链

——拿着红钥匙的人在等蓝钥匙,同时那个拿着蓝钥匙的人在等红钥匙

 

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

常见的避免死锁的方法

方法一 . 破坏互斥条件

只有一副钥匙,这是形成死锁的最关键的原因。

显然,如果我们能在两个线程跑之前,能给每个线程单独拷贝一份钥匙的副本,就能有效的避免死锁了。

当然,这种方法试用范围并不广。因为有时如果系统拷贝那副钥匙的成本极高,而线程又很多的话,这种方法就不适用了。

 

方法 . 破坏请求和保持条件

任何一个线程“贪心”,都可能会导致死锁。大致就是说有了一把钥匙还没还就要另一把。

这里我们可以通过规定在任何情况下,一个线程获取一把钥匙之后,必须归还了钥匙之后才能请求另一把钥匙,就可以有效解决这个问题。

 

方法三 . 破坏不剥夺条件

除非线程自己还钥匙,否则线程会一直占有钥匙,是形成不可剥夺条件的原因。

这里,我们可以通过设置一个最长占用时间的阈值来解决这个问题——如果过了10分钟仍然没有进入下一个步骤,则归还已有的钥匙。

这样的话,两个线程都能取到所需的钥匙继续下去了。

 

方法 . 破坏环路等待条件

会出现死锁的两两组合,一定都是一个线程先取了红钥匙而另一个线程先取了蓝钥匙,从而导致了可能形成了“环路等待”。

所以我们可以强制规定线程取钥匙的顺序只能是 “先取蓝钥匙再取红钥匙”的话,就能避免死锁了。

 

下列方法有助于最大限度地降低死锁:

1)按同一顺序访问对象。

2)避免事务中的用户交互。

3)保持事务简短并在一个批处理中。

4)使用低隔离级别。

5)使用绑定连接。

 

整理自: http://onwise.xyz/2017/04/20/mysql-%E6%AD%BB%E9%94%81%E9%97%AE%E9%A2%98%E5%8F%8A%E8%A7%A3%E5%86%B3/

存储引擎的 InnoDB MyISAM

   1.InnoDB不支持FULLTEXT类型的索引。

  ◆2.InnoDB 中不保存表的具体行数,也就是说,执行select count() from table时,InnoDB要扫描一遍整个表来计算有多少行,但是MyISAM只要简单的读出保存好的行数即可。注意的是,当count()语句包含 where条件时,两种表的操作是一样的。

  ◆3.对于AUTO_INCREMENT类型的字段,InnoDB中必须包含只有该字段的索引,但是在MyISAM表中,可以和其他字段一起建立联合索引。

  ◆4.DELETE FROM table时,InnoDB不会重新建立表,而是一行一行的删除。

  ◆5.LOAD TABLE FROM MASTER操作对InnoDB是不起作用的,解决方法是首先把InnoDB表改成MyISAM表,导入数据后再改成InnoDB表,但是对于使用的额外的InnoDB特性(例如外键)的表不适用。

另外,InnoDB表的行锁也不是绝对的,假如在执行一个SQL语句时MySQL不能确定要扫描的范围,InnoDB表同样会锁全表,例如update table set num=1 where name like %aaa%

数据库索引的原理

数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。

为什么要用 B-tree

一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。

这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。

聚集索引与非聚集索引的区别

1).聚集索引:

物理存储按照索引排序;聚集索引是一种索引组织形式,索引的键值逻辑顺序决定了表数据行的物理存储顺序

2) .非聚集索引:

物理存储不按照索引排序;非聚集索引则就是普通索引了,仅仅只是对数据列创建相应的索引,不影响整个表的物理存储顺序.

 

1).聚集索引一个表只能有一个,而非聚集索引一个表可以存在多个

2).聚集索引存储记录是物理上连续存在,而非聚集索引是逻辑上的连续,物理存储并不连续

索引是通过二叉树的数据结构来描述的

我们可以这么理解聚簇索引:索引的叶节点就是数据节点。

而非聚簇索引的叶节点仍然是索引节点,只不过有一个指针指向对应的数据块。

limit 20000 加载很慢怎么解决

Mysql的性能低是因为数据库要去扫描N + M条记录,然后又要放弃之前N条记录,开销很大

解决思略:

1前端加缓存,或者其他方式,减少落到库的查询操作

例如某些系统中数据在搜索引擎中有备份的,可以用es等进行搜索

2使用延迟关联

即先通用limit得到需要数据的索引字段,然后再通过原表和索引字段关联获得需要数据

select a.* from a,(select id from table_1 where is_deleted='N' limit 100000,20) b where a.id = b.id

3、从业务上实现,不分页如此多

例如只能分页前100页,后面的不允许再查了

4不使用limit N,M,而是使用limit N

即将offset转化为where条件。

Mysql数据库中的查询语句有关limit语句的优化。

一般limit是用在分页的程序的分页上的,当应用数据量够小时,也许感觉不到limit语句的任何问题,但当查询数据量达到一定程度时,limit的性能就会急剧下降。 这个是通过大量实例得出来的结论。

对同一张表在不同的地方取10条数据:

1offset比较小时  

代码示例: select * from user limit 10,10;

这条sql语句多次运行,时间保持在0.0004-0.0005之间。  

代码示例:

select * from user where uid >=( select uid from user order by uid limit 10,1 ) limit 10;

     这条sql语句多次运行,时间保持在0.0005-0.0006之间,主要是0.0006

结论:偏移offset较小时,直接使用limit较优。

2offset大时  

代码示例: select * from user limit 10000,10;

这条sql语句多次运行,时间保持在0.0187左右  

代码示例:

 select * from user where uid >=( select uid from user order by uid limit 10000,1 ) limit 10;

这条sql语句多次运行,时间保持在0.0061左右,只有前者的1/3

可以预计offset越大,后者越优。这个显然是子查询的原因。

 

通过以上对比,得出mysql limit查询语句优化经验:

使用limit语句时,当数据量偏移量较小时可以直接使用limit,当数据量偏移量较大时,可以适当的使用子查询来做相关的性能优化

选择合适的分布式主键方案

1. 数据库自增长序列或字段

2. UUID

3. 使用UUID to Int64的方法

4. Redis生成ID

5. Twittersnowflake算法

6. 利用zookeeper生成唯一ID

7. MongoDBObjectId

选择合适的数据存储方案

关系型数据库 MySQL

MySQL 是一个最流行的关系型数据库,在互联网产品中应用比较广泛。

一般情况下,MySQL 数据库是选择的第一方案,基本上有 80% ~ 90% 的场景都是基于 MySQL 数据库的。

因为,需要关系型数据库进行管理,

此外,业务存在许多事务性的操作,需要保证事务的强一致性。

同时,可能还存在一些复杂的 SQL 的查询。

值得注意的是,前期尽量减少表的联合查询,便于后期数据量增大的情况下,做数据库的分库分表。

 

内存数据库 Redis

随着数据量的增长,MySQL 已经满足不了大型互联网类应用的需求。

因此,Redis 基于内存存储数据,可以极大的提高查询性能,对产品在架构上很好的补充。

例如,为了提高服务端接口的访问速度,尽可能将读频率高的热点数据存放在 Redis 中。

这个是非常典型的以空间换时间的策略,使用更多的内存换取 CPU 资源,通过增加系统的内存消耗,来加快程序的运行速度。

在某些场景下,可以充分的利用 Redis 的特性,大大提高效率。

这些场景包括:

缓存,会话缓存,时效性,访问频率,计数器,社交列表,记录用户判定信息,交集、并集和差集,热门列表与排行榜,最新动态等。

使用 Redis 做缓存的时候,需要考虑:

数据不一致与脏读、缓存更新机制、缓存可用性、缓存服务降级、缓存穿透、缓存预热等缓存使用问题。

文档数据库 MongoDB

MongoDB 是对传统关系型数据库的补充

它非常适合高伸缩性的场景,它是可扩展性的表结构。

基于这点,可以将预期范围内,表结构可能会不断扩展的 MySQL 表结构,通过 MongoDB 来存储,这就可以保证表结构的扩展性。

此外,日志系统数据量特别大,如果用 MongoDB 数据库存储这些数据,利用分片集群支持海量数据,同时使用聚集分析和 MapReduce 的能力,是个很好的选择。

MongoDB 还适合存储大尺寸的数据,GridFS 存储方案就是基于 MongoDB 的分布式文件存储系统。

列族数据库 HBase

HBase 适合海量数据的存储与高性能实时查询

它是运行于 HDFS 文件系统之上,并且作为 MapReduce 分布式处理的目标数据库,以支撑离线分析型应用。

在数据仓库、数据集市、商业智能等领域发挥了越来越多的作用,在数以千计的企业中支撑着大量的大数据分析场景的应用。

全文搜索引擎 ElasticSearch

在一般情况下,关系型数据库的模糊查询,都是通过 like 的方式进行查询。

其中,like value%” 可以使用索引,但是对于 like %value%” 这样的方式,这在数据量小的表,不存在性能问题,但是对于海量数据,全表扫描是非常可怕的事情。

ElasticSearch 作为一个建立在全文搜索引擎 Apache Lucene 基础上的实时的分布式搜索和分析引擎,适用于处理实时搜索应用场景。

此外,使用 ElasticSearch 全文搜索引擎,还可以支持多词条查询、匹配度与权重、自动联想、拼写纠错等高级功能。因此,可以使用 ElasticSearch 作为关系型数据库全文搜索的功能补充,将要进行全文搜索的数据缓存一份到 ElasticSearch 上,达到处理复杂的业务与提高查询速度的目的。

ElasticSearch 不仅仅适用于搜索场景,还非常适合日志处理与分析的场景。

著名的 ELK 日志处理方案,由 ElasticSearchLogstash Kibana 三个组件组成,包括了日志收集、聚合、多维度查询、可视化显示等。

 

摘抄自:http://blog.720ui.com/2017/db_better_db_use/

MongoDB _id生成规则

MongoDB的文档必须有一个_id键。目的是为了确认在集合里的每个文档都能被唯一标识。

 

ObjectId _id 的默认类型。

ObjectId 采用12字节的存储空间,每个字节两位16进制数字,是一个24位的字符串。

12位生成规则:

 

[0,1,2,3] [4,5,6]  [7,8] [9,10,11]

时间戳 |机器码 |PID |计数器

 

前四位是时间戳,可以提供秒级别的唯一性。

接下来三位是所在主机的唯一标识符,通常是机器主机名的散列值。

接下来两位是产生ObjectIdPID,确保同一台机器上并发产生的ObjectId是唯一的。

前九位保证了同一秒钟不同机器的不同进程产生的ObjectId时唯一的。

最后三位是自增计数器,确保相同进程同一秒钟产生的ObjectId是唯一的。

 

https://github.com/qianjiahao/MongoDB/wiki/MongoDB%E4%B9%8B_id%E7%94%9F%E6%88%90%E8%A7%84%E5%88%99

 

聊聊 MongoDB 使用场景

MongoDB是对传统关系型数据库的补充,但是 MongoDB 不支持事务,因此对事务性有要求的程序不建议使用 MongoDB。此外,MongoDB 不支持表联合查询,而这个是关系型数据库擅长的事情。

高伸缩性的场景

MongoDB 非常适合高伸缩性的场景,它是可扩展性的表结构。基于这点,可以将预期范围内,表结构可能会不断扩展的 MySQL 表结构,通过 MongoDB 来存储,这就可以保证表结构的扩展性。

日志系统的场景

日志系统数据量特别大,如果用 MongoDB 数据库存储这些数据,利用分片集群支持海量数据,同时使用聚集分析和 MapReduce 的能力,是个很好的选择。

分布式文件存储

MongoDB 还适合存储大尺寸的数据,之前介绍的 GridFS 存储方案,就是基于 MongoDB 的分布式文件存储系统。

摘抄自: http://blog.720ui.com/2017/mongodb_core_use/

 

聊聊 ElasticSearch 使用场景

全文搜索

这个是用的最多的。加上分词插件、拼音插件什么的可以做成强大的全文搜索引擎。

数据库

挺奇葩的用法,因为ES存数相同数据,更费空间,不过确实不错,因为他的强大统计分析汇总能力,再加上分布式P2P扩展能力,现在硬件又那么便宜,所以就有人拿来当数据库了。

在线统计分析引擎

日志系统。logstash,不用解释了吧。可以实时动态分析数据,很是爽。

倒排索引

倒排索引(英语:Inverted index),也常被称为反向索引、置入档案或反向档案

是一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射。

它是文档检索系统中最常用的数据结构。

有两种不同的反向索引形式:

一条记录的水平反向索引(或者反向档案索引)包含每个引用单词的文档的列表。

一个单词的水平反向索引(或者完全反向索引)又包含每个单词在一个文档中的位置。

说说反模式设计

简单的来说,反模式是指在对经常面对的问题经常使用的低效,不良,或者有待优化的设计模式/方法。甚至,反模式也可以是一种错误的开发思想/理念。

在这里我举一个最简单的例子:

在面向对象设计/编程中,有一条很重要的原则, 单一责任原则(Single responsibility principle)。其中心思想就是对于一个模块,或者一个类来说,这个模块或者这个类应该只对系统/软件的一个功能负责,而且该责任应该被该类完全封装起来。当开发人员需要修改系统的某个功能,这个模块/类是最主要的修改地方。

相对应的一个反模式就是上帝类(God Class),通常来说,这个类里面控制了很多其他的类,同时也依赖其他很多类。整个类不光负责自己的主要单一功能,而且还负责了其他很多功能,包括一些辅助功能。

很多维护老程序的开发人员们可能都遇过这种类,一个类里有几千行的代码,有很多功能,但是责任不明确单一。单元测试程序也变复杂无比。维护/修改这个类的时间要远远超出其他类的时间。

很多时候,形成这种情况并不是开发人员故意的。很多情况下主要是由于随着系统的年限,需求的变化,项目的资源压力,项目组人员流动,系统结构的变化而导致某些原先小型的,符合单一原则类慢慢的变的臃肿起来。

最后当这个类变成了维护的噩梦(特别是原先熟悉的开发人员离职后),重构该类就变成了一个不容易的工程。

 

缓存使用

Redis 有哪些类型

Redis中有五种数据类型

String ———-字符串

Hash ————-字典

List  ————-列表

Set  ————-集合

Sorted Set —–有序集合

Redis 内部结构

Redis 内部使用一个 redisObject 对象来表示所有的 key value

blob.png

 

redisObject 核心对象

type

代表一个 value 对象具体是何种数据类型。

encoding

是不同数据类型在 redis 内部的存储方式

比如:

type = string 代表 value 存储的是一个普通字符串

那么对应的 encoding 可以是 raw 或者是 int

如果是 int 则代表实际 redis 内部是按数值型类存储和表示这个字符串的

当然前提是这个字符串本身可以用数值表示,比如:"123" "456"这样的字符串。

vm

只有打开了 Redis 的虚拟内存功能,此字段才会真正的分配内存,该功能默认是关闭状态的。

Redis 使用 redisObject 来表示所有的 key/value 数据是比较浪费内存的,当然这些内存管理成本的付出主要也是为了给 Redis 不同数据类型提供一个统一的管理接口,实际作者也提供了多种方法帮助我们尽量节省内存使用。

 

Key(键值)

过期删除

过期数据的清除从来不容易,为每一条key设置一个timer,到点立刻删除的消耗太大,每秒遍历所有数据消耗也大,Redis使用了一种相对务实的做法:

client主动访问key会先对key进行超时判断,过时的key会立刻删除。

如果clien永远都不再get那条key呢?

它会在Master的后台,每秒10次的执行如下操作: 随机选取100key校验是否过期,如果有25个以上的key过期了,立刻额外随机选取下100key(不计算在10次之内)

可见,如果过期的key不多,它最多每秒回收200条左右,如果有超过25%key过期了,它就会做得更多,但只要key不被主动get,它占用的内存什么时候最终被清理掉只有天知道。

常用操作

Key的长度限制Key的最大长度不能超过1024字节,在实际开发时不要超过这个长度,但是Key的命名不能太短,要能唯一标识缓存的对,作者建议按照在关系型数据库中的库表唯一标识字段的方式来命令Key的值,用分号分割不同数据域,用点号作为单词连接。

 

Key的查询Keys,返回匹配的key,支持通配符如 keys a* keys a?c,但不建议在生产环境大数据量下使用。

 

Key对应的Value进行的排序Sort命令对集合按数字或字母顺序排序后返回或另存为list,还可以关联到外部key等。因为复杂度是最高的O(N+Mlog(M))*(N是集合大小,M 为返回元素的数量),有时会安排到slave上执行。

 

Key的超时操作Expire(指定失效的秒数)/ExpireAt(指定失效的时间戳)/Persist(持久化)/TTL(返回还可存活的秒数),关于Key超时的操作。默认以秒为单位,也有p字头的以毫秒为单位的版本

 

String(字符串类型的Value

 

可以是String,也可是是任意的byte[]类型的数组,如图片等。String redis 内部存储默认就是一个字符串,被 redisObject 所引用,当遇到 incr,decr 等操作时会转成数值型进行计算,此时 redisObject encoding 字段为int

 

1. 大小限制:最大为512Mb,基本可以存储任意图片啦。

 

2. 常用命令的时间复杂度为O(1),读写一样的快。

 

3. String代表的数字进行增减操作(没有指定的Key则设置为0值,然后在进行操作):Incr/IncrBy/IncrByFloat/Decr/DecrBy(原子性),

** 可以用来做计数器,做自增序列,也可以用于限流,令牌桶计数等**

key不存在时会创建并贴心的设原值为0IncrByFloat专门针对float

 

4. 设置Value的安全性:SetNx命令仅当key不存在时才Set(原子性操作)。

可以用来选举Master或做分布式锁:所有Client不断尝试使用SetNx master myName抢注Master,成功的那位不断使用Expire刷新它的过期时间。

如果Master倒掉了key就会失效,剩下的节点又会发生新一轮抢夺。SetExSet + Expire 的简便写法,p字头版本以毫秒为单位。

 

5. 获取:GetSet(原子性), 设置新值,返回旧值。

比如一个按小时计算的计数器,可以用GetSet获取计数并重置为0

这种指令在服务端做起来是举手之劳,客户端便方便很多。

MGet/MSet/MSetNx, 一次get/set多个key

 

6. 其他操作:Append/SetRange/GetRange/StrLen,对文本进行扩展、替换、截取和求长度

只对特定数据格式如字段定长的有用,json就没什么用。

 

7. BitMap的用法:GetBit/SetBit/BitOp,与或非/BitCount

BitMap的玩法,比如统计今天的独立访问用户数时,每个注册用户都有一个offset,他今天进来的话就把他那个位设为1,用BitCount就可以得出今天的总人数。

 

HashHashMap,哈希映射表)

 

Redis Hash 实际是内部存储的 Value 为一个 HashMap,并提供了直接存取这个 Map 成员的接口。

Hash将对象的各个属性存入Map里,可以只读取/更新对象的某些属性。另外不同的模块可以只更新自己关心的属性而不会互相并发覆盖冲突。

不同程序通过 key(用户 ID+ field(属性标签)就可以并发操作各自关心的属性数据

 

常用操作

 

O(1)操作:hgethset等等

O(n)操作:hgetallRedis 可以直接取到全部的属性数据,但是如果内部 Map 的成员很多,那么涉及到遍历整个内部 Map 的操作,由于 Redis 单线程模型的缘故,这个遍历操作可能会比较耗时,而另其它客户端的请求完全不响应,这点需要格外注意。

List(双向链表)

Redis list 的应用场景非常多,也是 Redis 最重要的数据结构之一

比如 twitter 的关注列表,粉丝列表等都可以用 Redis list 结构来实现

还提供了生产者消费者阻塞模式(B开头的命令),常用于任务队列,消息队列等。

实现方式

 

Redis list 的实现为一个双向链表,即可以支持反向查找和遍历

更方便操作,不过带来了部分额外的内存开销,Redis 内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。

 

常用操作

 

复合操作RPopLPush/ BRPopLPush

弹出来返回给client的同时,把自己又推入另一个list,是原子操作。 

按值进行的操作LRem(按值删除元素)LInsert(插在某个值的元素的前后),复杂度是O(N)NList长度,因为List的值不唯一,所以要遍历全部元素,而Set只要O(log(N))

按下标进行操作(下标从0开始,队列从左到右算,下标为负数时则从右到左,-1为右端第一个元素)

时间复杂度为O(N)

LSet :按下标设置元素值。(NList的长度)

LIndex:按下标返回元素。(Nindex的值)

LTrim:限制List的大小,保留指定范围的元素。(N是移除元素的个数)

LRange:返回列表内指定范围下标的元素,常用于分页。(N = start+range

set(HashSet

Set就是HashSet,可以将重复的元素随便放入而Set会自动去重,底层实现也是HashMap,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。

set 的内部实现是一个 value 永远为 null HashMap,实际就是通过计算 hash 的方式来快速排重的,这也是 set 能提供判断一个成员是否在集合内的原因。

常用操作

 

增删改查:

SAdd/SRem/SIsMember/SCard/SMove/SMembers等等。除了SMembers都是O(1)

 

集合操作:

SInter/SInterStore/SUnion/SUnionStore/SDiff/SDiffStore,各种集合操作。

交集运算可以用来显示在线好友(在线用户 交集 好友列表),共同关注(两个用户的关注列表的交集)O(N),并集和差集的N是集合大小之和,交集的N是小的那个集合的大小的2倍。

Sorted Set(插入有序Set集合)

set 不是自动有序的,而** sorted set 可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序**。当你需要一个有序的并且不重复的集合列表,那么可以选择 sorted set 数据结构,比如 twitter public timeline 可以以发表时间作为 score 来存储,这样获取时就是自动按时间排好序的。

实现方式

内部使用 HashMap 和跳跃表(SkipList)来保证数据的存储和有序

 

Sorted Set的实现是HashMap(element->score, 用于实现ZScore及判断element是否在集合内),和SkipList(score->element,score排序)的混合体。SkipList有点像平衡二叉树那样,不同范围的score被分成一层一层,每层是一个按score排序的链表。

 

常用操作

ZAdd/ZRemO(log(N))ZRangeByScore/ZRemRangeByScoreO(log(N)+M)NSet大小,M是结果/操作元素的个数。复杂度的log取对数很关键,可以使,1000万大小的Set,复杂度也只是几十不到。但是,如果一次命中很多元素M很大则复杂度很高。

ZRange/ZRevRange,按排序结果的范围返回元素列表,可以为正数与倒数。

 

ZRangeByScore/ZRevRangeByScore,按score的范围返回元素,可以为正数与倒数。

 

ZRemRangeByRank/ZRemRangeByScore,按排序/score的范围限删除元素。

 

ZCount,统计按score的范围的元素个数。

 

ZRank/ZRevRank ,显示某个元素的正/倒序的排名。

 

ZScore/ZIncrby,显示元素的Score/增加元素的Score

 

ZAdd(Add)/ZRem(Remove)/ZCard(Count)ZInsertStore(交集)/ZUnionStore(并集),与Set相比,少了IsMember和差集运算。

Redis使用与内存优化

上面的一些实现上的分析可以看出 redis 实际上的内存管理成本非常高,即占用了过多的内存,属于用空间换时间。作者对这点也非常清楚,所以提供了一系列的参数和手段来控制和节省内存

建议不要开启VM(虚拟内存)选项

VM 选项是作为 Redis 存储超出物理内存数据的一种数据在内存与磁盘换入换出的一个持久化策略,将严重地拖垮系统的运行速度,所以要关闭 VM 功能,请检查你的 redis.conf 文件中 vm-enabled no

设置最大内存选项

最好设置下 redis.conf 中的 maxmemory 选项,该选项是告诉 Redis 当使用了多少物理内存后就开始拒绝后续的写入请求,该参数能很好的保护好你的 Redis 不会因为使用了过多的物理内存而导致 swap,最终严重影响性能甚至崩溃。

设置内存饱和回收策略

volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰

volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰

volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰

allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰

allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰

no-enviction(驱逐):禁止驱逐数据

控制内存使用的参数

Redis 为不同数据类型分别提供了一组参数来控制内存使用

Hash

redis.conf 配置文件中下面2

**hash-max-zipmap-entries 64 **

含义是当 value 这个 Map 内部不超过多少个成员时会采用线性紧凑格式存储,默认是64,即 value 内部有64个以下的成员就是使用线性紧凑存储zipmap,超过该值自动转成真正的 HashMap(ht)

hash-max-zipmap-value 512

hash-max-zipmap-value 含义是当 value 这个 Map 内部的每个成员值长度不超过

多少字节就会采用线性紧凑存储zipmap来节省空间。

以上2个条件任意一个条件超过设置值都会转换成真正的 HashMap,也就不会再节省内存了,但是也不是越大越好(空间和查改效率需要根据实际情况来权衡)

List

list-max-ziplist-entries 512

list 数据类型多少节点以下会采用去指针的紧凑存储格式ziplist

list-max-ziplist-value 64

list 数据类型节点值大小小于多少字节会采用紧凑存储格式ziplist

Set

set-max-intset-entries 512

set 数据类型内部数据如果全部是数值型,且包含多少节点以下会采用紧凑格式存储

Redis内部的优化

Redis 内部实现没有对内存分配方面做过多的优化,在一定程度上会存在内存碎片,不过大多数情况下这个不会成为 Redis 的性能瓶颈。

 

Redis 缓存了一定范围的常量数字作为资源共享,在很多数据类型是数值型则能极大减少内存开销,默认为1-10000,可以重新编译配置修改源代码中的一行宏定义 REDIS_SHARED_INTEGERS

总结

根据业务需要选择合适的数据类型,并为不同的应用场景设置相应的紧凑存储参数。

当业务场景不需要数据持久化时,关闭所有的持久化方式可以获得最佳的性能以及最大的内存使用量。

如果需要使用持久化,根据是否可以容忍重启丢失部分数据在快照方式与语句追加方式之间选择其一,不要使用虚拟内存以及 diskstore 方式。

不要让你的 Redis 所在机器物理内存使用超过实际内存总量的3/5

Redis 的持久化使用了 Buffer IO ,所谓 Buffer IO 是指 Redis 对持久化文件的写入和读取操作都会使用物理内存的 Page Cache,而当 Redis 的持久化文件过大操作系统会进行Swap,这时你的系统就会有内存还有余量但是系统不稳定或者崩溃的风险。

作者:zhanglbjames

链接:https://www.jianshu.com/p/f09480c05e42

聊聊 Redis 使用场景

随着数据量的增长,MySQL 已经满足不了大型互联网类应用的需求。因此,Redis 基于内存存储数据,可以极大的提高查询性能,对产品在架构上很好的补充。在某些场景下,可以充分的利用 Redis 的特性,大大提高效率。

缓存

对于热点数据,缓存以后可能读取数十万次,因此,对于热点数据,缓存的价值非常大。例如,分类栏目更新频率不高,但是绝大多数的页面都需要访问这个数据,因此读取频率相当高,可以考虑基于 Redis 实现缓存。

会话缓存

此外,还可以考虑使用 Redis 进行会话缓存。例如,将 web session 存放在 Redis 中。

时效性

例如验证码只有60秒有效期,超过时间无法使用,或者基于 Oauth2 Token 只能在 5 分钟内使用一次,超过时间也无法使用。

访问频率

出于减轻服务器的压力或防止恶意的洪水攻击的考虑,需要控制访问频率,例如限制 IP 在一段时间的最大访问量。

计数器

数据统计的需求非常普遍,通过原子递增保持计数。例如,应用数、资源数、点赞数、收藏数、分享数等。

社交列表

社交属性相关的列表信息,例如,用户点赞列表、用户分享列表、用户收藏列表、用户关注列表、用户粉丝列表等,使用 Hash 类型数据结构是个不错的选择。

记录用户判定信息

记录用户判定信息的需求也非常普遍,可以知道一个用户是否进行了某个操作。例如,用户是否点赞、用户是否收藏、用户是否分享等。

交集、并集和差集

在某些场景中,例如社交场景,通过交集、并集和差集运算,可以非常方便地实现共同好友,共同关注,共同偏好等社交关系。

热门列表与排行榜

按照得分进行排序,例如,展示最热、点击率最高、活跃度最高等条件的排名列表。

最新动态

按照时间顺序排列的最新动态,也是一个很好的应用,可以使用 Sorted Set 类型的分数权重存储 Unix 时间戳进行排序。

消息队列

Redis 能作为一个很好的消息队列来使用,依赖 List 类型利用 LPUSH 命令将数据添加到链表头部,通过 BRPOP 命令将元素从链表尾部取出。同时,市面上成熟的消息队列产品有很多,例如 RabbitMQ。因此,更加建议使用 RabbitMQ 作为消息中间件。

摘抄自:http://blog.720ui.com/2017/redis_core_use/

 

用作消息队列中防止数据丢失的解决方法

如果消费者把jobPop走了又没处理完就死机了怎么办?

 

消息生产者保证不丢失

加多一个sorted se t,分发的时候同时发到listsorted set,以分发时间为score,用户把job做完了之后要用ZREM消掉sorted set里的job,并且定时从sorted set中取出超时没有完成的任务,重新放回list。 如果发生重复可以在sorted set中在查询确认一遍,或者将消息的消费接口设计成幂等性。

 

消息消费者保证不丢失

为每个worker多加一个的list,弹出任务时改用RPopLPush

发表评论

电子邮件地址不会被公开。 必填项已用*标注