美文网首页嘟嘟程序猿
Java面试题(四):数据库

Java面试题(四):数据库

作者: 雪飘千里 | 来源:发表于2019-04-04 02:34 被阅读87次

    十七、MySql

    164. 数据库的三范式是什么?

    什么是范式:简言之就是,数据库设计对数据的存储性能,还有开发人员对数据的操作都有莫大的关系。所以建立科学的,规范的的数据库是需要满足一些

    规范的来优化数据数据存储方式。在关系型数据库中这些规范就可以称为范式。

    什么是三大范式:

    第一范式:当关系模式R的所有属性都不能在分解为更基本的数据单位时,称R是满足第一范式的,简记为1NF。满足第一范式是关系模式规范化的最低要求,否则,将有很多基本操作在这样的关系模式中实现不了。

    • 每一列属性都是不可再分的属性值,确保每一列的原子性
    • 两列的属性相近或相似或一样,尽量合并属性一样的列,确保不产生冗余数据。

    第二范式:如果关系模式R满足第一范式,并且R得所有非主属性都完全依赖于R的每一个候选关键属性,称R满足第二范式,简记为2NF。

    • 每一行的数据只能与其中一列相关,即一行数据只做一件事。只要数据列中出现数据重复,就要把表拆分开来。

    第三范式:设R是一个满足第一范式条件的关系模式,X是R的任意属性集,如果X非传递依赖于R的任意一个候选关键字,称R满足第三范式,简记为3NF.

    • 数据不能存在传递关系,即没个属性都跟主键有直接关系而不是间接关系。像:a-->b-->c 属性之间含有这样的关系,是不符合第三范式的

    三大范式只是一般设计数据库的基本理念,可以建立冗余较小、结构合理的数据库。如果有特殊情况,当然要特殊对待,数据库设计最重要的是看需求跟性能,需求>性能>表结构。所以不能一味的去追求范式建立数据库。

    165. 一张自增表里面总共有 7 条数据,删除了最后 2 条数据,重启 mysql 数据库,又插入了一条数据,此时 id 是几?

    6
    一般情况下,我们创建的表的类型是InnoDB,如果新增一条记录(不重启mysql的情况下),这条记录的id是8;但是如果重启(上文中提到的)MySQL的话,这条记录的ID是5。因为InnoDB表只把自增主键的最大ID记录到内存中,所以重启数据库或者对表OPTIMIZE操作,都会使最大ID丢失。
    但是,如果我们使用表的类型是MylSAM,那么这条记录的ID就是8。因为MylSAM表会把自增主键的最大ID记录到数据文件里面,重启MYSQL后,自增主键的最大ID也不会丢失。

    166. 如何获取当前数据库版本?

    select version()
    

    167. 说一下 ACID 是什么?

    数据库事务
    一个事务一般是指多个操作的集合,比如插入数据库分两段插入,第二次插入错误,第一次插入操作也需要回退

    • 1.Atomicity 原子性
      原子性,指的是整个事务是一个独立的单元,是不可分割的工作单位,要么操作成功,要么操作不成功,事务必须要保持和系统处于一致的状态,只有使据库中所有的操作执行成功,才算整个事务成功;事务中任何一个SQL语句执行失败,那么已经执行成功的SQL语句也必须撤销,数据库状态应该退回到执行事务前的状态。

    • 2.Consistency 一致性
      指数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。例如对银行转帐事务,不管事务成功还是失败,应该保证事务结束后ACCOUNTS表中Tom和Jack的存款总额为2000元。

    • 3.Isolation 隔离性
      指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。

    • 4.Durability 持久性
      指的是只要事务成功结束,它对数据库所做的更新就必须永久保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

    168. char 和 varchar 的区别是什么?

    • char:定长,效率高,一般用于固定长度的表单提交数据存储 ;例如:身份证号,手机号,电话,密码等

    • varchar:不定长,效率偏低

    • 存数据时的区别
      char定义的是固定长度,长度范围为0-255,存储时,如果字符数没有达到定义的位数,会在后面用空格补全存入数据库中,在上例中,name实际存储在数据中的数据为'zejin ';
      varchar是变长长度,长度范围为0-65535,存储时,如果字符没有达到定义的位数,也不会在后面补空格,在上例subject字段中,实际存储在数据中的数据为'zejin',当然还有一或两个字节来描述该字节长度

    • 取数据时的区别
      数据库取char的数据时,会把后面的空格全部丢弃掉,譬如上例中的description字段取出来时只剩zejin
      而数据库在取varchar数据时,尾部空格会保留,譬如subject字段

    169. float 和 double 的区别是什么?

    double精度高,有效数字16位,float精度7位。但double消耗内存是float的两倍,double的运算速度比float慢得多;

    float数值类型用于表示单精度浮点数值,而double数值类型用于表示双精度浮点数值,float和double都是浮点型,而decimal是定点型;

    MySQL 浮点型和定点型可以用类型名称后加(M,D)来表示,M表示该值的总共长度,D表示小数点后面的长度,M和D又称为精度和标度,如float(7,4)的 可显示为-999.9999,MySQL保存值时进行四舍五入,如果插入999.00009,则结果为999.0001。

    FLOAT和DOUBLE在不指 定精度时,默认会按照实际的精度来显示,而DECIMAL在不指定精度时,默认整数为10,小数为0。

    170. mysql 的内连接、左连接、右连接有什么区别?

    • 1.内连接,显示两个表中有联系的所有数据;

    • 2.左链接,以左表为参照,显示所有数据;

    • 3.右链接,以右表为参照显示数据;

    171. mysql 索引是怎么实现的?

    索引是一个排序的列表,在这个列表中存储着索引的值和包含这个值的数据所在行的物理地址,在数据十分庞大的时候,索引可以大大加快查询的速度,这是因为使用索引后可以不用扫描全表来定位某行的数据,而是先通过索引表找到该行数据对应的物理地址然后访问相应的数据

    • 索引的优缺点
      优势:可以快速检索,减少I/O次数,加快检索速度;根据索引分组和排序,可以加快分组和排序;

      劣势:索引本身也是表,因此会占用存储空间,一般来说,索引表占用的空间的数据表的1.5倍;索引表的维护和创建需要时间成本,这个成本随着数据量增大而增大;构建索引会降低数据表的修改操作(删除,添加,修改)的效率,因为在修改数据表的同时还需要修改索引表;

    • 索引的分类
      常见的索引类型有:主键索引、唯一索引、普通索引、全文索引、组合索引

      1、主键索引:即主索引,根据主键pk_clolum(length)建立索引,不允许重复,不允许空值;

      2、唯一索引:用来建立索引的列的值必须是唯一的,允许空值

      3、普通索引:用表中的普通列构建的索引,没有任何限制

      4、全文索引:用大文本对象的列构建的索引(下一部分会讲解)

      5、组合索引:用多个列组合构建的索引,这多个列中的值不允许有空值

    172.索引的使用策略

    什么时候要使用索引?

    主键自动建立唯一索引;
    经常作为查询条件在WHERE或者ORDER BY 语句中出现的列要建立索引;
    作为排序的列要建立索引;
    查询中与其他表关联的字段,外键关系建立索引
    高并发条件下倾向组合索引;
    用于聚合函数的列可以建立索引,例如使用了max(column_1)或者count(column_1)时的column_1就需要建立索引
    什么时候不要使用索引?

    经常增删改的列不要建立索引;
    有大量重复的列不建立索引;
    表记录太少不要建立索引。只有当数据库里已经有了足够多的测试数据时,它的性能测试结果才有实际参考价值。如果在测试数据库里只有几百条数据记录,它们往往在执行完第一条查询命令之后就被全部加载到内存里,这将使后续的查询命令都执行得非常快--不管有没有使用索引。只有当数据库里的记录超过了1000条、数据总量也超过了MySQL服务器上的内存总量时,数据库的性能测试结果才有意义。
    索引失效的情况:

    在组合索引中不能有列的值为NULL,如果有,那么这一列对组合索引就是无效的。
    在一个SELECT语句中,索引只能使用一次,如果在WHERE中使用了,那么在ORDER BY中就不要用了。
    LIKE操作中,'%aaa%'不会使用索引,也就是索引会失效,但是‘aaa%’可以使用索引。
    在索引的列上使用表达式或者函数会使索引失效,例如:select * from users where YEAR(adddate)<2007,将在每个行上进行运算,这将导致索引失效而进行全表扫描,因此我们可以改成:select * from users where adddate<’2007-01-01′。其它通配符同样,也就是说,在查询条件中使用正则表达式时,只有在搜索模板的第一个字符不是通配符的情况下才能使用索引。
    在查询条件中使用不等于,包括<符号、>符号和!=会导致索引失效。特别的是如果对主键索引使用!=则不会使索引失效,如果对主键索引或者整数类型的索引使用<符号或者>符号不会使索引失效。
    在查询条件中使用IS NULL或者IS NOT NULL会导致索引失效。
    字符串不加单引号会导致索引失效。更准确的说是类型不一致会导致失效,比如字段email是字符串类型的,使用WHERE email=99999 则会导致失败,应该改为WHERE email='99999'。
    在查询条件中使用OR连接多个条件会导致索引失效,除非OR链接的每个条件都加上索引,这时应该改为两次查询,然后用UNION ALL连接起来。
    如果排序的字段使用了索引,那么select的字段也要是索引字段,否则索引失效。特别的是如果排序的是主键索引则select * 也不会导致索引失效。
    尽量不要包括多列排序,如果一定要,最好为这队列构建组合索引;

    172. 怎么验证 mysql 的索引是否满足需求?

    在select语句前加上explain就可以用来查看sql的执行计划,
    explain显示了MySQL如何使用索引来处理select语句以及连接表

    possible_keys:显示可能应用在这张表中的索引。如果为空,没有可能的索引。可以为相关的域从WHERE语句中选择一个合适的语句

    key: 实际使用的索引。如果为NULL,则没有使用索引。很少的情况下,MYSQL会选择优化不足的索引。这种情况下,可以在SELECT语句中使用USE INDEX(indexname)来强制使用一个索引或者用IGNORE INDEX(indexname)来强制MYSQL忽略索引

    173. 说一下数据库的事务隔离?

    image.png
    • 未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据

    • 提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)

    • 可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读

    • 串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞

    ① 脏读: 脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
    ② 不可重复读:是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
    ④ 幻读:第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。

    174. 说一下 mysql 常用的引擎?

    • a.Innodb引擎
      Innodb引擎提供了对数据库ACID事务的支持。并且还提供了行级锁和外键的约束。它的设计的目标就是处理大数据容量的数据库系统。它本身实际上是基于Mysql后台的完整的系统。Mysql运行的时候,Innodb会在内存中建立缓冲池,用于缓冲数据和索引。但是,该引擎是不支持全文搜索的。同时,启动也比较的慢,它是不会保存表的行数的。当进行Select count(*) from table指令的时候,需要进行扫描全表。所以 当需要使用数据库的事务时,该引擎就是首选。由于锁的粒度小,写操作是不会锁定全表的。所以在并发度较高的场景下使用会提升效率的。

    • b.MyIASM引擎
      是MySql的默认引擎,但不提供事务的支持,也不支持行级锁和外键。因此当执行Insert插入和Update更新语句时,即执行写操作的时候需要锁定这个表。所以会导致效率会降低。不过和Innodb不同的是,MyIASM引擎是保存了表的行数,于是当进行Select count(*) from table语句时,可以直接的读取已经保存的值而不需要进行扫描全表。所以, 如果表的读操作远远多于写操作时,并且不需要事务的支持的。可以将MyIASM作为数据库引擎的首先。

    175. 说一下 mysql 的行锁和表锁?

    • 表级锁:每次操作锁住整张表。开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;
    • 行级锁:每次操作锁住一行数据。开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高;

    176. 说一下乐观锁和悲观锁?

    • 乐观锁
      乐观锁不是数据库自带的,需要我们自己去实现。乐观锁是指操作数据库时(更新操作),想法很乐观,认为这次的操作不会导致冲突,在操作数据时,并不进行任何其他的特殊处理(也就是不加锁),而在进行更新后,再去判断是否有冲突了。

      通常实现是这样的:在表中的数据进行操作时(更新),先给数据表加一个版本(version)字段,每操作一次,将那条记录的版本号加1。也就是先查询出那条记录,获取出version字段,如果要对那条记录进行操作(更新),则先判断此刻version的值是否与刚刚查询出来时的version的值相等,如果相等,则说明这段期间,没有其他程序对其进行操作,则可以执行更新,将version字段的值加1;如果更新时发现此刻的version值与刚刚获取出来的version的值不相等,则说明这段期间已经有其他程序对其进行操作了,则不进行更新操作。

    • 悲观锁
      与乐观锁相对应的就是悲观锁了。悲观锁就是在操作数据时,认为此操作会出现数据冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作,这点跟java中的synchronized很相似,所以悲观锁需要耗费较多的时间。另外与乐观锁相对应的,悲观锁是由数据库自己实现了的,要用的时候,我们直接调用数据库的相关语句就可以了。

    说到这里,由悲观锁涉及到的另外两个锁概念就出来了,它们就是共享锁与排它锁。共享锁和排它锁是悲观锁的不同的实现,它俩都属于悲观锁的范畴。

    • 应用场景
      案例:
      某商品,用户购买后库存数应-1,而某两个或多个用户同时购买,此时三个执行程序均同时读得库存为n,之后进行了一些操作,最后将均执行update table set 库存数=n-1,那么,很显然这是错误的。

    解决:

    • 1.使用悲观锁(其实说白了也就是排他锁)
      |--程序A在查询库存数时使用排他锁(select * from table where id=10 for update)
      |--然后进行后续的操作,包括更新库存数,最后提交事务。
      |--程序B在查询库存数时,如果A还未释放排他锁,它将等待。
      |--程序C同B……

    • 2.使用乐观锁(靠表设计和代码来实现)
      |--一般是在该商品表添加version版本字段或者timestamp时间戳字段
      |--程序A查询后,执行更新变成了:
      update table set num=num-1 where id=10 and version=23
      这样,保证了修改的数据是和它查询出来的数据是一致的,而其他执行程序未进行修改。当然,如果更新失败,表示在更新操作之前,有其他执行程序已经更新了该库存数,那么就可以尝试重试来保证更新成功。为了尽可能避免更新失败,可以合理调整重试次
      数(阿里巴巴开发手册规定重试次数不低于三次)。

    总结:对于以上,可以看得出来乐观锁和悲观锁的区别。
    1.悲观锁使用了排他锁(写锁),当程序独占锁时,其他程序就连查询都是不允许的,导致吞吐较低。如果在查询较多的情况下,可使用乐观锁。
    2.乐观锁更新有可能会失败,甚至是更新几次都失败,这是有风险的。所以如果写入较频繁,对吞吐要求不高,可使用悲观锁。
    也就是一句话:读频繁用乐观锁,写频繁用悲观锁。

    177.共享锁与排他锁

    首先说明:数据库的增删改操作默认都会加排他锁,而查询不会加任何锁。

    • 共享锁:对某一资源加共享锁,自身可以读该资源,其他人也可以读该资源(也可以再继续加共享锁,即 共享锁可多个共存),但无法修改。要想修改就必须等所有共享锁都释放完之后。语法为:
      select * from table lock in share mode
    • 排他锁:对某一资源加排他锁,自身可以进行增删改查,其他人无法进行任何操作。语法为:
      select * from table for update --增删改自动加了排他锁

    178. mysql 问题排查都有哪些手段?

    EXPLAIN 你的 SELECT 查询

    179. 如何做 mysql 的性能优化?

    缓存
    索引

    十八、Redis

    179. redis 是什么?都有哪些使用场景?

    Redis是一个开源的key—value型数据库,支持string、list、set、zset和hash类型数据。对这些数据的操作都是原子性的,Redis为了保证效率会定期持久化数据。

    • 适用场景:
      数据高并发的读写
      海量数据的读写
      对扩展性要求高的数据

    • 不适场景:
      需要事务支持(非关系型数据库)
      基于sql结构化查询储存,关系复杂

    redis 最适合的场景

    Redis最适合所有数据in-momory的场景,虽然Redis也提供持久化功能,但实际更多的是一个disk-backed的功能,跟传统意义上的持久化有比较大的差别,那么可能大家就会有疑问,似乎Redis更像一个加强版的Memcached,那么何时使用Memcached,何时使用Redis呢?

    如果简单地比较Redis与Memcached的区别,大多数都会得到以下观点:
    1 、Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
    2 、Redis支持数据的备份,即master-slave模式的数据备份。
    3 、Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。

    (1)、会话缓存(Session Cache)

    最常用的一种使用Redis的情景是会话缓存(session cache)。用Redis缓存会话比其他存储(如Memcached)的优势在于:Redis提供持久化。当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗?

    幸运的是,随着 Redis 这些年的改进,很容易找到怎么恰当的使用Redis来缓存会话的文档。甚至广为人知的商业平台Magento也提供Redis的插件。

    (2)、全页缓存(FPC)

    除基本的会话token之外,Redis还提供很简便的FPC平台。回到一致性问题,即使重启了Redis实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似PHP本地FPC。

    再次以Magento为例,Magento提供一个插件来使用Redis作为全页缓存后端

    此外,对WordPress的用户来说,Pantheon有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。

    (3)、队列

    Reids在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得Redis能作为一个很好的消息队列平台来使用。Redis作为队列使用的操作,就类似于本地程序语言(如Python)对 list 的 push/pop 操作。

    如果你快速的在Google中搜索“Redis queues”,你马上就能找到大量的开源项目,这些项目的目的就是利用Redis创建非常好的后端工具,以满足各种队列需求。例如,Celery有一个后台就是使用Redis作为broker,你可以从这里去查看。

    (4),排行榜/计数器

    Redis在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis只是正好提供了这两种数据结构。所以,我们要从排序集合中获取到排名最靠前的10个用户–我们称之为“user_scores”,我们只需要像下面一样执行即可:

    当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执行:

    ZRANGE user_scores 0 10 WITHSCORES

    Agora Games就是一个很好的例子,用Ruby实现的,它的排行榜就是使用Redis来存储数据的,你可以在这里看到。

    (5)、发布/订阅

    最后(但肯定不是最不重要的)是Redis的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用Redis的发布/订阅功能来建立聊天系统!(不,这是真的,你可以去核实)。

    180. redis 优势?

    (1) 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)

    (2) 支持丰富数据类型,支持string,list,set,sorted set,hash

    (3) 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行

    (4) 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除

    181. redis 和 memecache 有什么区别?

    • 1)、存储方式
      Memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。
      Redis有部份存在硬盘上,这样能保证数据的持久性。

    • 2)、数据支持类型
      Memcache对数据类型支持相对简单。
      Redis有复杂的数据类型。

    • 3)、使用底层模型不同
      它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。
      Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。

    • 4)、value大小
      redis最大可以达到1GB,而memcache只有1MB

    redis相比memcached有哪些优势

    • (1) memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型

    • (2) redis的速度比memcached快很多

    • (3) redis可以持久化其数据

    182. redis 为什么是单线程的?

    • Redis为什么这么快,尤其是其采用单线程
      因为它的所有的数据都在内存中,所有的运算都是内存级别的运算,而且单线程避免了多线程的切换性能损耗问题;
      而且正因为Redis是单线程,所以要小心使用Redis指令,对于那些耗时的指令(比如keys),一定要谨慎使用,一步小心就可能会导致Redis卡顿;

    • Redis单线程处理多个并发客户端连接:IO多路复用
      Redis的IO多路复用:redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放到文件事件分派器,事件分派器将事件分发给事件处理器。
      Nginx也是采用IO多路复用原理解决C10k问题

    183. 什么是缓存穿透?怎么解决?

    缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,通常出于容错的考虑,如果从存储层查不到数据则不写入缓存层。
    缓存穿透的原因
    缓存穿透的问题,肯定是再大并发情况下。依此为前提,我们分析缓存穿透的原因如下:

    • 1、恶意攻击,猜测你的key命名方式,然后估计使用一个你缓存中不会有的key进行访问。

    • 2、第一次数据访问,这时缓存中还没有数据,则并发场景下,所有的请求都会压到数据库。

    • 3、数据库的数据也是空,这样即使访问了数据库,也是获取不到数据,那么缓存中肯定也没有对应的数据。这样也会导致穿透。

    缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。缓存穿透问题可能会使后端存储负载加大,由于很多后端存储不具备高并发性,甚至可能造成后端存储宕掉。

    解决的步骤如下:

    • 1、再web服务器启动时,提前将有可能被频繁并发访问的数据写入缓存。—这样就规避大量的请求在第3步出现排队阻塞。

    • 2、规范key的命名,并且统一缓存查询和写入的入口。这样,在入口处,对key的规范进行检测。–这样保存恶意的key被拦截。

    • 3、Synchronized双重检测机制,这时我们就需要使用同步(Synchronized)机制,在同步代码块前查询一下缓存是否存在对应的key,然后同步代码块里面再次查询缓存里是否有要查询的key。 这样“双重检测”的目的,还是避免并发场景下导致的没有意义的数据库的访问(也是一种严格避免穿透的方案)。
      这一步会导致排队,但是第一步中我们说过,为了避免大量的排队,可以提前将可以预知的大量请求提前写入缓存。

    • 4、不管数据库中是否有数据,都在缓存中保存对应的key,值为空就行。–这样是为了避免数据库中没有这个数据,导致的平凡穿透缓存对数据库进行访问。

    • 5、第4步中的空值如果太多,也会导致内存耗尽。导致不必要的内存消耗。这样就要定期的清理空值的key。避免内存被恶意占满。导致正常的功能不能缓存数据。

    image.png

    184. redis 支持的数据类型有哪些?

    REDIS_STRING 字符串对象
    REDIS_LIST 列表对象
    REDIS_HASH 哈希对象
    REDIS_SET 集合对象
    REDIS_ZSET 有序集合对象

    185. redis 支持的 java 客户端都有哪些?

    Redis的Java客户端很多,官方推荐的有三种:Jedis、Redisson和lettuce。
    在这里对Jedis和Redisson进行对比介绍

    • Jedis:

    轻量,简洁,便于集成和改造
    支持连接池
    支持pipelining、事务、LUA Scripting、Redis Sentinel、Redis Cluster
    不支持读写分离,需要自己实现
    文档差(真的很差,几乎没有……)

    • Redisson:

    基于Netty实现,采用非阻塞IO,性能高
    支持异步请求
    支持连接池
    支持pipelining、LUA Scripting、Redis Sentinel、Redis Cluster
    不支持事务,官方建议以LUA Scripting代替事务
    支持在Redis Cluster架构下使用pipelining
    支持读写分离,支持读负载均衡,在主从复制和Redis Cluster架构下都可以使用
    内建Tomcat Session Manager,为Tomcat 6/7/8提供了会话共享功能
    可以与Spring Session集成,实现基于Redis的会话共享
    文档较丰富,有中文文档
    对于Jedis和Redisson的选择,同样应遵循前述的原理,尽管Jedis比起Redisson有各种各样的不足,但也应该在需要使用Redisson的高级特性时再选用Redisson,避免造成不必要的程序复杂度提升

    186. jedis 和 redisson 有哪些区别?

      1. 概况对比
        Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持;Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。2.2. 编程模型Jedis中的方法调用是比较底层的暴露的Redis的API,也即Jedis中的Java方法基本和Redis的API保持着一致,了解Redis的API,也就能熟练的使用Jedis。而Redisson中的方法则是进行比较高的抽象,每个方法调用可能进行了一个或多个Redis方法调用
      1. 可伸缩性
        Jedis使用阻塞的I/O,且其方法调用都是同步的,程序流需要等到sockets处理完I/O才能执行,不支持异步。Jedis客户端实例不是线程安全的,所以需要通过连接池来使用Jedis。Redisson使用非阻塞的I/O和基于Netty框架的事件驱动的通信层,其方法调用是异步的。Redisson的API是线程安全的,所以可以操作单个Redisson连接来完成各种操作。
      1. 数据结构
        Jedis仅支持基本的数据类型如:String、Hash、List、Set、Sorted Set。Redisson不仅提供了一系列的分布式Java常用对象,基本可以与Java的基本数据结构通用,还提供了许多分布式服务,其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service)。在分布式开发中,Redisson可提供更便捷的方法。

    总结
    功能需求不多,上Jedis即可
    功能需求复杂使用Redisson,虽然臃肿但是功能齐全

    187. 怎么保证缓存和数据库数据的一致性?

      1. 首先尝试从缓存读取,读到数据则直接返回;如果读不到,就读数据库,并将数据会写到缓存,并返回。
      1. 需要更新数据时,先更新数据库,然后把缓存里对应的数据失效掉(删掉)。

    188. redis 持久化有几种方式?

    redis提供两种方式进行持久化,一种是RDB持久化(原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化),另外一种是AOF(append only file)持久化(原理是将Reids的操作日志以追加的方式写入文件)。那么这两种持久化方式有什么区别呢,改如何选择呢

    • 区别
      RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储;

    AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录;

    • 优缺点
      RDB存在哪些优势呢?
      1). 一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这对于文件备份而言是非常完美的。比如,你可能打算每个小时归档一次最近24小时的数据,同时还要每天归档一次最近30天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。

      2). 对于灾难恢复而言,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。

      3). 性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。

      4). 相比于AOF机制,如果数据集很大,RDB的启动效率会更高。

    RDB又存在哪些劣势呢?

    1). 如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。

    2). 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。

    AOF的优势有哪些呢?
    1). 该机制可以带来更高的数据安全性,即数据持久性。Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。至于无同步,无需多言,我想大家都能正确的理解它。

    2). 由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据一致性的问题。

    3). 如果日志过大,Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。

    4). AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。

    AOF的劣势有哪些呢?

    1). 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

    2). 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。

    二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。rdb这个就更有些 eventually consistent的意思了。

    189.redis 怎么实现分布式锁?

    分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。这里介绍第二种方式,基于Redis实现分布式锁
    为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

    • 互斥性。在任意时刻,只有一个客户端能持有锁。
    • 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
    • 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
    • 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了
    public class RedisTool {
        private static final String LOCK_SUCCESS = "OK";
    //意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
        private static final String SET_IF_NOT_EXIST = "NX";
    //意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
        private static final String SET_WITH_EXPIRE_TIME = "PX";
        /**
         * 尝试获取分布式锁
         * @param jedis Redis客户端
         * @param lockKey 锁
         * @param requestId 请求标识
         * @param expireTime 超期时间
         * @return 是否获取成功
         */
        public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
            String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
    
            if (LOCK_SUCCESS.equals(result)) {
                return true;
            }
            return false;
        }
    }
    

    从上可以看出,我们的加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端

    public class RedisTool {
        private static final Long RELEASE_SUCCESS = 1L;
        /**
         * 释放分布式锁
         * @param jedis Redis客户端
         * @param lockKey 锁
         * @param requestId 请求标识
         * @return 是否释放成功
         */
        public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
          //Lua脚本代码
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    //作用,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。
            Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
    
            if (RELEASE_SUCCESS.equals(result)) {
                return true;
            }
            return false;
        }
    }
    

    为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的,
    就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。
    注:如果Redis是多机部署的,那么可以尝试使用Redisson实现分布式锁

    190. redis 分布式锁有什么缺陷?

    单机故障

    image.png

    191. redis 如何做内存优化?

    192. redis 淘汰策略有哪些?

    • voltile-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(驱逐):禁止驱逐数据

    193. redis 常见的性能问题有哪些?该如何解决?

    (1) Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件

    (2) 如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次

    (3) 为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内

    (4) 尽量避免在压力很大的主库上增加从库

    (5) 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3…

    这样的结构方便解决单点故障问题,实现Slave对Master的替换。如果Master挂了,可以立刻启用Slave1做Master,其他不变。

    194. 缓存雪崩 和 缓存击穿

    • 缓存雪崩,是指在某一个时间段,缓存集中过期失效。
      其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,那么那个时候数据库能顶住压力,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮
    • 缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

      在做电商项目的时候,把这货就成为“爆款”。
      其实,大多数情况下这种爆款很难对数据库服务器造成压垮性的压力。达到这个级别的公司没有几家的。所以,务实主义的小编,对主打商品都是早早的做好了准备,让缓存永不过期。即便某些商品自己发酵成了爆款,也是直接设为永不过期就好了。
      大道至简,mutex key互斥锁真心用不上。

    相关文章

      网友评论

        本文标题:Java面试题(四):数据库

        本文链接:https://www.haomeiwen.com/subject/pubwbqtx.html