淘先锋技术网

首页 1 2 3 4 5 6 7

Redis实践应用

一、为什么需要用Redis

1、Redis优势

  传统的互联网系统,数据都存储在数据库中,数据库又把数据存放在磁盘上,从磁盘读取数据速度慢,数据库的并发性低,互联网系统需求变化快,系统响应速度快,所以在java应用和数据库之间加上了中间件Redis,使用Redis系统性能高,集成简单,容易理解,比使用数据库高出3 ~ 6倍。(Redis官方性能12W/S,数据库一般2 ~ 4W/S)

2、Redis和MySQL数据库的区别

Redis和数据库本质上是一样的,都是“数据存取”。

  • MySQL以磁盘存放为主
  • Redis以内存存放为主

  磁盘和内存之间的读取速度相差100~200倍,数据库往往是程序的性能瓶颈,这并不代表数据库的性能特别差。

3、使用MySQL数据库

  MySQL数据库使用InnoDB引擎,本身就用到内存,磁盘的数据会加载到内存,所有的读写操作都在内存中完成MySQL数据库磁盘中的数据量远大于数据库服务器内存容量,所以MySQL不会把全部的数据加载到内存,当内存放满了,而查询的数据还在磁盘中时,MySQL会进行一个淘汰替换过程,MySQL服务器会将内存中不常用的数据淘汰掉,把要查询的数据加载到内存中,它不是无限制的把数据加载到内存中,而是先把需要的数据放进去。

4、使用Redis

  Redis是当做缓存使用的,数据放在数据库的同时加载到Redis的内存中,当用户请求查询数据时,先在Redis中查询,查询到,返回结果;查询不到,去数据库中查询,并将从数据库中查询出来的数据加载到Redis缓存中,Redis是一个独立的服务器,需要单独启动,可以把Redis简单理解为JVM外置的增强版HashMap。
在这里插入图片描述

5、Redis中的数据是什么时候加载的?

  • 读取数据时,Redis中没有,将从数据库中查询出来的数据加载到Redis缓存中;
  • 定时任务——定时把数据加载到Redis缓存。

6、缓存集成方式

  • 直连数据库:使用mybatis或者JDBC;
public User findUserById(String userId){
	User user = userDao.findUserById(userId);
	return user;
)
  • 使用缓存:
    Redis原生客户端
public User findUserById(String userId) throws Exception{
	// 1、先读取缓存
	Object cacheValue = redisTemplate.opsForValue().get (userId);
	if (cacheValue != null) {
	System.out.println("###缓存命中:" + ((User)cacheValue).getUname());
	return (User) cacheValue;
}
	//以下代码是异常情况下执行
	// 2、如果缓存miss, 则查询数据库
	User user = userDao.findUserById (userId);
	System.out.println("###缓存miss:" + user.getUname());
	
	// 3、设置缓存(重建缓存)
	redisTemplate.opsForValue().set(userld, user);
	return user;
}

缓存+注解

@chche
public User findUserById(String userId){
	User user = userDao.findUserById(userId);
	System.out.println("###缓存miss:" + user.getUname());
	return user;
)

二、Redis相关术语

  • 缓存命中:数据直接从Redis缓存中读取到;
  • 缓存Miss:数据在Redis缓存中没有读取到,查询数据库;
  • 缓存重建:从数据库中读取数据并放到Redis缓存中;

1、缓存穿透

(1)缓存穿透是指用户想要查询一个数据,发现redis内存数据库没有,缓存Miss,于是向持久层数据库查询,发现也没有,查询失败,当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库,给持久层数据库造成很大的压力,出现缓存穿透。

  缓存穿透查询一个根本不存在的数据,导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储层的意义。

(2)解决方案:

  • 布隆过滤器:对所有可能查询的参数以hash形式存储,当用户想要查询的时候,使用布隆过滤器发现不在集合中,就直接丢弃,不再对持久层查询。
  • 缓存空对象:当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;

2、缓存击穿

(1)缓存击穿是指针对一个设置了过期时间的key,某些时间段大并发的集中对这个key进行访问,当这个key在一个瞬间失效(缓存过期)了,大量的访问会直接请求数据库,给数据库瞬间造成极大的压力。

缓存击穿是针对某一特定的key缓存,缓存雪崩是针对很多key缓存。

(2)解决方案:

  • 设置热点数据永不过期;
  • 后台定义一个job(定时任务)专门主动更新缓存数据,比如,一个缓存中的数据过期时间是30分钟,那么job每隔29分钟定时刷新数据(将从数据库中查到的数据更新到缓存中),这种方案比较容易理解,但会增加系统复杂度,适合那些 key 相对固定,cache 粒度较大的业务,key 比较分散的则不太适合,实现起来也比较复杂。
  • 使用分布式锁,保证在高并发情况下每次同时只有一个线程去查询数据库,没有获得分布式锁权限的线程,等待。

3、缓存雪崩

(1)缓存雪崩是指大量设置了过期时间的key,过期时间相同,在同一时间失效,所有的访问会直接请求数据库,数据库无法承受如此大的压力,系统崩溃,单系统挂掉,其他依赖于该系统的应用也会出现问题甚至崩溃。
(2)解决方案:

  • 搭建集群,Redis集群,一台挂掉之后其他的还可以继续工作;
  • 限流降级,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量;
  • 数据预热,在正式部署之前,先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。
  • 过期时间,在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

三、Redis线上部署

性能指标:

  • TPS(Transaction Per Second):数据库每秒执行的事务数,以commit为准。
  • QPS(Query Per Second):数据库每秒执行的SQL数(含insert、select、update、delete 等)。

1、涉及到的系统(3个)

  • JAVA服务器——接收用户请求
  • Redis服务器——数据缓存
  • MySQL服务器——数据存取

2、部署设计

JAVA服务器集群 + 单个Redis服务器 + 单个MySQL服务器

  双11之前,服务器会预热(开放访问之前,把数据库中的数据按需提前加载到Redis缓存中),商家的活动会提前配置好,用户的信息等会进行资源锁定。

MySQL硬件配置要求高于Redis,在同样的硬件条件下,性能远低于Redis。

四、线上注意问题

1、什么情况下会出现缓存Miss?

  肯定是经过测试,缓存功能有效的情况下才上线运行,持续运行一段时间后才出现缓存Miss,运行过程中出现缓存Miss,会导致一系列问题(缓存穿透、缓存击穿、缓存雪崩),运行前出现缓存Miss,可以通过预热解决。

  • 重启之后内存中的数据消失
  • 内存中存放的数据太多
  • 有效期机制,过期自动删除
  • 网络不通,Redis服务器和Java服务器之间网络不通

2、如何避免缓存Miss?

  内存方面的问题没有办法解决,网络方面的问题可以不考虑,所以关键在于过期时间方面。Redis默认永不过期。
(1)针对数据量小的不设置过期时间,在缓存场景中要注意增量数据;
(2)数据量大的情况下,要设置过期时间

  • 固定过期时间,所有缓存数据过期时间一致;
  • 动态过期时间,缓存数据随机,避免同一时间大量缓存失效;
  • 不同数据不同过期时间,热点数据可以设置为永不过期;

3、能不能完全规避缓存Miss?

  从Redis的原理角度来说,无法100%规避。因为重启之后内存中的数据就没了。

  • 不能保证硬件不出问题,断电、硬件损坏、磁盘损坏;
  • 持久化也不能解决重启数据丢失问题;
  • 主从集群,避免单台机器出故障,数据不一致;

  Redis默认数据存放在内存中,持久化机制就是把内存中的数据放到一个文件中,保存在磁盘中,将内存的数据同步到磁盘。

  • 全量持久化RDB,定时任务,周期性保存当前所有数据,系统可能在持久化之前就挂掉;
  • 增量持久化AOF,实时记录Redis每条写操作,三种方式(不记录、每秒记录一次、每个操作都记录),效率会受影响。

  主从集群是为了防止出现单点故障,同步数据到多台Redis服务器,同步过程是异步的,会导致主从集群的数据不一致。

4、如何降低缓存Miss的影响?

  系统雪崩需要满足两个条件:缓存Miss、大量访问请求数据库
  缓存Miss只能避免出现,不能100%规避,所以关键在于数据库,不要让大量访问直接请求数据库,数据库查询代码执行时,同一时间不要太多次。

  • 加锁,每次只让一个请求查询数据库,其他请求等待;
public User findUserById(String userId) throws Exception{
	// 1、先读取缓存
	Object cacheValue = redisTemplate.opsForValue().get (userId);
	if (cacheValue != null) {
	System.out.println("###缓存命中:" + ((User)cacheValue).getUname());
	return (User) cacheValue;
}
	//以下代码是异常情况下执行
	 synchronized(this){  //1个请求拿到锁请求数据库,其他排队等待
		// 2、如果缓存miss, 则查询数据库
		User user = userDao.findUserById (userId);
		System.out.println("###缓存miss:" + user.getUname());
	
		// 3、设置缓存(重建缓存)
		redisTemplate.opsForValue().set(userld, user);
		return user;
  }
}
  • 多线程,加锁可能会导致大量用户请求等待,浪费数据库资源,使用Semaphore信号量限流。

Semaphore semaphore = new Semaphore(permits:200);

public User findUserById(String userId) throws Exception{
	// 1、先读取缓存
	Object cacheValue = redisTemplate.opsForValue().get (userId);
	if (cacheValue != null) {
	System.out.println("###缓存命中:" + ((User)cacheValue).getUname());
	return (User) cacheValue;
}
	semaphore.acquire();//获取200个资源
	try{

		// 2、如果缓存miss, 则查询数据库
		User user = userDao.findUserById (userId);
		System.out.println("###缓存miss:" + user.getUname());
	
		// 3、设置缓存(重建缓存)
		redisTemplate.opsForValue().set(userld, user);
		return user;
	}finally {
		semaphore.release();//放回
	}
}