传统项目中,使用数据库进行存储数据,数据库有一个弊端。因为数据库持久化主要是面向磁盘,而磁盘的读写速度比较慢。以前的管理项目不存在高并发问题,所以没有瞬间需要读写大量数据的要求,现在互联网中往往存在大数据量的需求。例如商品抢购、抢购高铁票,需要系统在极端的时间内完成成千上万次的读写操作,这种处理量是数据库无法承受的,容易造成数据库系统瘫痪。
redis是一种基于内存的键值(key-value)数据库,可以支持每秒十几万次的读写操作,提供一定的持久化功能,支持集群、分布式、主从同步等配置,还能支持一定的事务能力,在高并发情况下可以保证数据的安全和一致性。
redis的性能优越性:
(1)缓存
现实中,对数据库的操作中,读操作的次数远超写操作,比例1:9到3:7,每次从数据库读数据的时候,数据库就会去磁盘里把对应的数据索引回来,而索引磁盘是一个很缓慢的过程,如果把数据放到运行在内存中的redis服务器上,就可以直接从内存取数据,速度大大提升,所以考虑把常用的数据缓存到redis服务器上。因为内存的价格比磁盘的价格高很多,资源有限,所以往往挑选常用且重要的数据进行缓存,例如:用户登录的信息、银行客户基础信息等等。
1-不常用的数据不缓存
2-写操作多而读操作少的数据不缓存
3-数据太大的不缓存
其中还要考虑到数据安全和一致性,有效请求和无效请求,事务一致性等等
常用命令: set,get,decr,incr,mget 等。
String数据结构是简单的key-value类型,犹如java的Map结构,让redis通过key去找到value,value其实不仅可以是字符串,也可以是整数和浮点数。 常规key-value缓存应用; 常规计数:微博数,粉丝数等。
常用命令: hget,hset,hgetall 等。
hash 是一个 string 类型的 field 和 value 的映射表,是一个键值对应的无序列表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。比如下面我就用 hash 类型存放了我本人的一些信息:
key=JavaUser293847
value={
“id”: 1,
“name”: “SnailClimb”,
“age”: 22,
“location”: “Wuhan, Hubei”
}
常用命令: lpush,rpush,lpop,rpop,lrange等
list 就是链表,每个节点都包含一个字符串,Redis list 的应用场景非常多,也是Redis最重要的数据结构之一,比如微博的关注列表,粉丝列表,消息列表等功能都可以用Redis的 list 结构来实现。
Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。
常用命令: sadd,spop,smembers,sunion 等
set 对外提供的功能与list类似是一个列表的功能,特殊之处在于 set 是可以自动排重的。在它里面的每一个元素都是一个字符串,而且各不相同。
当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。
比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程,具体命令如下:
sinterstore key1 key2 key3 将交集存在key1内
常用命令: zadd,zrange,zrem,zcard等
和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。它是一个有序集合,可以包含字符串、整数、浮点数、分值score,元素的排序根据分值的大小来决定。
举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 Sorted Set 结构进行存储。
数据类型说明表:
基本事务和回滚机制、锁的机制和watch/UNwatch、流水线提高redis的命令性能、发布订阅模式、超时命令和垃圾回收策略、Luau语言(最重要)
和其他大部分的NoSQL不同,redis是存在事务的,提供两个重要的保证:
Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。
在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的可靠性和安全性。在 Redis 中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当 Redis 运行在某种特定的持久化模式下时,事务也具有持久性(Durability)。
redis同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。
redis的事务命令
未完,待续…
redis的事务命令
未完,待续…
使用watch命令可以决定事务是执行还是回滚,
redis的事务命令
未完,待续…
(1)什么是缓存雪崩?
简介:缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
(2)解决方法
(3)什么是缓存穿透?
缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。
一般MySQL 默认的最大连接数在 150 左右,这个可以通过 show variables like ‘%max_connections%’; 命令来查看。最大连接数一个还只是一个指标,cpu,内存,磁盘,网络等无力条件都是其运行指标,这些指标都会限制其并发能力!所以,一般 3000 个并发请求就能打死大部分数据库了。
(4)解决方法
最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。
一般情况下我们都是这样使用缓存的:先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。这种方式很明显会存在缓存和数据库的数据不一致的情况。
你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?
一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况
串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
在事务中redis提供了队列,这是一个可以批量执行任务的队列
redis的事务命令
未完,待续…
当使用银行卡消费的时候,银行会通过微信、短信和邮件来通知用户这笔交易的信息,这是一种发布订阅模式。发布订阅模式需要消息源,采用观察者模式,让订阅者收到消息进行处理
使用SUBSCRIBE命令,注册一个订阅的客户端
未完,待续…
JVM里提供了GC垃圾回收的功能,来保证java程序使用过且不再使用的java对象及时的从内存里释放掉。redis也是基于内存而运行的数据库,也存在着对内存垃圾的回收和管理的问题。
Redis中有个设置时间过期的功能,即对存储在 redis 数据库中的值可以设置一个过期时间。例如短信验证码有效时间为60s,超过时间就会失效。我们 set key 的时候,都可以给一个 expire time,就是过期时间,通过过期时间我们可以指定这个 key 可以存活的时间。
1h后redis如何对这批key进行删除?
但是仅仅通过设置过期时间还是有问题的。我们想一下:如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致redis内存块耗尽了。怎么解决这个问题呢? redis 内存淘汰机制。
redis 内存淘汰机制(MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据?
redis 提供 6种数据淘汰策略:
除了使用命令以外,还可以使用Lua语言操作redis,redis命令的计算能力不算强大,使用Lua语言则弥补了redis的这个不足
很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置。
在redis中存在两种方式的备份:一种是快照(snapshotting,RDB),备份当前瞬间redis在内存中的数据记录;另一种是只追加文件(append-only file,AOF),作用就是当redis执行写命令后,在一定的条件下将执行过的写命令依次保存在redis的文件中,将来就可以依次执行那些保存的命令恢复redis的数据
对于快照备份而言,如果当前redis的数据量大,备份会造成redis卡顿,但是恢复重启的速度较快;对于AOF而言,它只是追加写命令,所以备份一般不会造成卡顿,但是恢复重启要执行更多的命令,备份文件可能也很大。
有时候大量的读操作到达redis服务器,触发众多操作,仅靠一台redis服务器值完全不够用的。当主服务器不能正常工作的时候,也需要从服务器代替原来的主服务器,保证系统可以继续正常工作。因此我们希望可以读写分离,读写分离的前提是读操作远远比写操作频繁的多。
互联网系统一般是以主从架构为基础,思路为:
主从同步的配置分为主机和从机,主机只有一台,从机有多台。
未完,待续…
(1)无论如何先保证主服务器的开启,开启主服务器后,从服务器通过命令或者重启配置项可以同步到主服务器
(2)当从服务器启动时,读取同步的配置,根据配置决定是否使用当前数据响应客户端,然后发送SYNC命令。当主服务器接收到同步命令的时候,就会执行bgsave命令备份数据,但是主服务器并不会拒绝客户端的读写,而是将来自客户端的写命令写入缓冲区,从服务器未收到主服务器备份的快照文件的时候,会根据其配置决定使用现有数据响应客户端或者拒绝
(3)当bgsave命令被主服务器执行完后,开始想从服务器发送备份文件,这个时候从服务器就会丢弃所有现有的数据,开始载入发送的快照文件
(4)当主服务器发送完备份文件后,从服务器就会执行这些写入命令,此时就会把bgsave执行之后的缓存区内的写命令也发送给从服务器,从服务完成备份文件解析,就开始像往常一样,接收命令,等待命令写入
(5)缓冲区的命令发送完成后,当主服务器执行一条写命令后,就同时往主服务器发送同步写入命令,从服务器就和主服务器保持一致了。而此时当从服务器完成主服务器发送的缓冲区命令后,就开始等待主服务器的命令了。
知识在主服务器同步到从服务器的过程中,需要备份文件,所以在配置的时候一把需要预留一些内存空间给主服务器,用来腾出空间执行备份命令。
当主服务器宕机时,需要手动把一台从服务器切换为主服务器,费时费力。更多的时候考虑使用哨兵模式,自动的进行主从切换。
redis提供了哨兵的命令,哨兵是一个独立的进程,会独立进行。原理就哨兵通过发送命令,等待redis服务器响应,从而监控运行的多个redis实例。
哨兵有两个作用:
有时候一个哨兵不够,还可以使用多个哨兵的监控,而各个哨兵之间还会相互监控,成为多个哨兵模式。多个哨兵不仅监控多个redis服务器,而且哨兵之间相互监控,看看哨兵们是不是还活着。
哨兵机制的主从服务器切换过程:
假设主服务器宕机,哨兵1先监测到这个结果,当时系统不会马上进行failover操作,而仅仅是哨兵1主观地认为主机已经不可用,这个现象称为主观下线。当后面的哨兵监测也监测到了主服务器不可用,并且有了一定数量的哨兵认为主服务器不可用,那么哨兵之间就会形成一次投票,投票的结果由一个哨兵发起,进行failover操作,在failover操作的过程中切换成功后,救护通过发布订阅方式,让各个哨兵把自己监控的服务器实现切换主机,这个歌过程称为客观下线。
使用redis可以优化性能,但是存在一个重要的问题:redis的数据和数据库的数据同步的问题,如果在操作的时候出现不一致情况,就会出现脏数据。
比如数据库的事务是完善的严格的,但是redis的事务不是那么严格,如果发生异常回滚的事件,那么redis的数据可能就和数据库不太一致了,所以保存数据的一致性是相当困难的。
我们考虑读写以数据库的最新记录为主,并且同步写入redis,这样数据就能保持一致性了,而对于一些常用的只需要显示的,则以查询redis为主
数据缓存往往会在redis上设置超时时间,当设置redis的数据超时后,redis就没法读书数据了,这个时候就会触发程序读取数据库,然后把读取的数据库数据写入redis,这个时候会给redis重设超时时间,这样就能按一定的时间间隔刷新数据了。
写操作要考虑数据一致,尤其是那么重要的业务数据,所以首先应该考虑从数据库中读取最新的数据,然后对数据进行操作,最后把数据写入redis缓存中。
写入业务数据,先从数据库中读取最新数据。然后进行业务操作,更新业务数据到数据库后,再把数据刷新到redis缓存中,这样就完成了一次写操作。这样的操作就能避免把脏数据写入数据库中。
介绍主要的注解使用
(1)pojo类
要实现Serializable接口,让这个类支持序列化。把对象转换成字节序列的过程称为对象的序列化,当我们需要把对象的状态信息通过网络进行传输,或者需要把对象的状态信息持久化,通知JVM帮我序列化就好,Serializable接口就是Java提供用来高效率的异地共享实例对象的。
(2)spring的缓存管理器
提供CacheManager接口来定义缓存管理器,这样各个不同的缓存就可以实现它来提供管理器的功能了。
@EnableCaching表示Spring IOC容器启动缓存机制,可以加在springboot的启动器类上。
(3)service持久层
@Cacheable和@CachePut都可以保存缓存键值对,而删除缓存key的@CacheEvict则可以用在void的方法上。
@Transactional让程序能够在事务中运行,保证数据的一致性
@Async表示让Spring自动创建另外一条线程去运行它,前提是提供一个任务池给Spring环境
@EnableAsync表明支持异步调用,
上述注解都能标注在类或者方法上,如果放在类上,则对所有的方法都有效;如果放在方法上,则只是对方法有效。在大部分情况下,会放置到方法上。对于查询,我们会考虑使用@Cacheable;对于插入和修改,我们会考虑使用@CachePut;对于删除操作,考虑使用@CacheEvict
无效请求有很多种类,比如通过脚本连续刷新网站首页,会加大无效的访问量,所以需要识别这些无效请求。
首先,一个账号连续发出请求,显然可以认为是无效请求,常用的解决方法是验证。首次无验证码可以让用户减少录入,第二次请求开始加入验证码,验证方式可以是图片验证、等式运算等。
其次,使用短信验证。这类问题的逻辑判断不应该放在web服务器中实现,应该放在负载均衡器上完成,也就是在进入web服务器之间完成,就能避免大量的无效请求。
有时,有些用户可能申请多个账户来迷惑服务器,可以通过提高账户的登记来压制请求,比如支付交易的网站通过银行卡验证、实名制获取相关证件号码。可以有效规避一个人多个账号的频繁请求。
还有,黄牛组织通过多人的账号发送请求,可以考虑使用僵尸账号排除法对可交易的账号进行排除,那些平时没有任何交易的账号,只是在特殊的日子交易。还可以根据那些通过同一IP或者网段频繁请求的,使用IP封禁。
高并发系统需要往往需要分布式的系统分摊请求的压力,需要负载均衡服务,参考Nginx的请求分发
(1)水平分法:
按照业务划分,一个服务管理一种业务,数据库的设计也是根据业务划分来设计。这样也会带来一些麻烦,就是各个业务之间的信息还要通过RCP(远程过程调用协议)来处理才能共同工作,常见的RCP有Dubbo。每一个服务都会暴露一些公共接口给RCP服务,这样所有服务器都可以通过RCP服务获取其他服务器的逻辑来完成功能,但是接口的相互调用也会造成一定的缓慢。
(2)垂直分法:
是一个很大的请求量,按照互不相干的几个同样的系统分摊下去,把一个服务器上的请求量,根据算法合理的分配到多台服务器上,就能实现垂直分法。
(1)分表
本来一张表可以保存的数据,设计成多张表去保存。
(2)优化sql
1)使用更新语句和复杂查询语句的时候,优先使用主键key进行更新和查询,因为主键只会进行行锁定,而使用非主键会进行表锁定,这样在执行sql的时候不仅锁定了更新数据,还锁定了其他表数据,会影响并发。
2)使用连接查询,例如inner join,而不要使用子查询(in语句),
(3)建立索引
(4)读写分离
高并发系统的一个麻烦是并发数据不一致问题。加锁会影响并发,而不加锁就难以保证数据的一致性,这就是高并发和锁的矛盾。锁分为悲观锁和乐观锁。
(1)悲观锁
悲观锁是一种利用数据库内部机制提供的锁的方法,也就是对更新的数据加锁,这样在并发期间一旦有一个事务持有了数据库记录的锁,其他的线程将不能再对数据进行更新了。
在SQL语句里加上for update语句,意味着将持有对数据记录的行更新锁(因为是主键查询),意味着在高并发情况下,当一条事务持有了这个更新锁才能接着往下操作,其他的线程想要更新这个数据就必须得等待,这样就不会出现超发现象引发的数据一致性问题
(2)乐观锁
乐观锁是一种不会阻塞其他线程并发的机制,不会使用数据库的锁进行实现,乐观锁使用的是CAS原理
1)CAS原理概述
对于多个线程共同的资源,先保存一个旧值(Old Value),例如抢红包进入线程后,先查询当前剩余红包数并且把这个旧值保存起来,然后经过逻辑处理后,比较数据库现在的值和旧值是否一致。如果一致就说明数据保持了一致性,可以进行扣减红包的操作,如果值和旧值不一致就说明这个值已经被其他线程修改了,不再进行操作。
但是CAS原理有一个问题,就是ABA问题。
2)ABA问题
例如保持旧值为A,进行逻辑操作的期间,旧值被其他线程修改成了B,又修改成了A,这个时候再进行值与旧值比较的时候,发现值和旧值一致,会认为这个值没有被修改,然后进行更新。
ABA问题是因为业务逻辑存在回退的可能性。可以加入一个逻辑属性,比如加入一个版本号(version),只要修改了一次值,版本号就会递增且不会倒退,这样版本号就不会出现回退的现象,也就可以准确判断出值与旧值是否一致。
3)乐观锁重入机制
上面说的加入乐观锁,就会有很多数据sql执行失败。例如用户抢购时,很多人的订单会因为乐观锁的存在而提交失败,最后货物还有很多没卖出去。这个时候就要考虑提高请求的成功率,如果第一次请求失败,那就再自动请求一次。
有两种方法:1-加入时间戳执行乐观锁重入,2-加入限制重入次数执行乐观锁重入
1-加入时间戳执行乐观锁重入
在一定时间戳内,例如100毫秒,不成功的请求会循环到成功为止,知道超出100毫秒后,不成功的请求才会退出,返回失败
2-加入限制重入次数执行乐观锁重入
有时候时间戳并不是很稳定,也会随着系统的空闲或者繁忙导致重试次数不一。可以限定重入次数为3次,尝试3次请求后如果还是不成功,就退出并返回失败。
redis并不是一个严格的事务,而且事务的功能是有限的,为了增强功能性,还可以使用Lua语言。Lua语言是种原子性的操作,可以保证数据的一致性。依据这个原理可以避免超发现象。