Java后端学习日记(一):第一个Springboot应用——Hello World!
Java后端学习日记(二):POJO的基本概念,编写,转化和简化
Java后端学习日记(三):Springboot整合Mybatis-Plus
Java后端学习日记(四):Springboot 2.X 整合Redis作为数据缓存
Java后端学习日记(五):Springboot使用@ControllerAdvice捕获和处理异常
项目源码同步更新中
上一期中我们整合了Mybatis-Plus,对数据库进行CRUD,这一期我们来讲讲数据缓存
如果能用内存查找代替磁盘IO查找,那么效率就会高快几十到上百倍
常用的内存型数据库有Memcached,Redis和mongoDB,本文我们选择Redis进行整合
作为一个服务端,用户访问服务端时,大部分的请求都是查找请求,而增删改只是小部分请求。如果因为查找请求而频繁地访问数据库,对数据库来说是一种重担:
拿一个场景作为例子:
当电商举办活动,一个商品(假设商品id为3)热卖中,很多用户点击这个商品想要获取这个商品的信息,如果没有缓存的情况下,就会有大量的
# TB_COMMODITY 为商品表
SELECT * FROM TB_COMMODITY WHERE ID=3;
并发访问数据库,且不说磁盘IO效率低下,大量的连接请求会使数据库无力处理其它请求,造成数据库宕机
所以说数据缓存在面对大量用户请求的时候是必不可少的
Redis 是当前互联网世界最为流行的 NoSQL(Not Only SQL)数据库。NoSQL 在互联网系统中的作用很大,因为它可以在很大程度上提高互联网系统的性能。
Redis 具备一定持久层的功能,也可以作为一种缓存工具。对于 NoSQL 数据库而言,作为持久层,它存储的数据是半结构化的,这就意味着计算机在读入内存中有更少的规则,读入速度更快。
Redis的更多介绍自行百度
Redis支持的数据结构众多,性能好,支持持久化,具有原子性(单线程),用户还多…总而言之,redis,yes!
初学Redis可以将Redis先当成一个外置的HashMap^_^,存储键值对
见菜鸟教程
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<exclusions>
<exclusion>
<groupId>io.lettucegroupId>
<artifactId>lettuce-coreartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
说明:Spring Boot starter(spring-boot-starter-data-redis)默认使用 lettuce。使用jedis时,需要排除该依赖关系,并包含Jedis。
#配置redis
spring:
redis:
host:localhost
password:
port:6379
Redis默认密码为空,如需配置密码,则将redis.conf 或Redis Windows的 redis.windows.conf中的# requirepass foobared
这一行的井号去掉,再将foobared
改为你要设置的密码即可
org.springframework.data.redis.core.RedisTemplate
提供了丰富的api供程序员进行Redis的操作,但相对来说较为复杂,其中:
redisTemplate.opsForValue();//操作字符串
redisTemplate.opsForHash();//操作hash
redisTemplate.opsForList();//操作list
redisTemplate.opsForSet();//操作set
redisTemplate.opsForZSet();//操作有序set
作为数据缓存,redis字符串操作是最为常用的:
/**
* Redis操作工具类
*
* @author YuC
*/
@Component
public class RedisUtil {
// StringRedisTemplate == RedisTemplate
@Autowired
private RedisTemplate<String, String> redisTemplate;
}
在类的开头,通过@Autowired
获取RedisTemplate的实例
所谓一般方法,就是无所谓你传入的key的数据结构类型的方法
/**
* common
*/
/**
* 指定缓存失效时间
*
* @param key
* @param time
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 获取key过期时间
*
* @param key 不能为null
* @return 时间(秒)| 返回0代表永久有效
*/
public Long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key
* @return true存在 | false不存在
*/
public boolean hasKey(String key) {
try {
// 使 redisTemplate.hasKey 返回的空值也变为 false
return redisTemplate.hasKey(key) != null;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key
*/
public void delete(String... key) {
if (key != null && key.length > 0) {
redisTemplate.delete(new ArrayList<>(Arrays.asList(key)));
}
}
这里对RedisTemplate中的api进行包装,增加了易用性
/**
* 对象转json工具
*
* @param object
* @return
*/
private String objectToJson(Object object) {
if (object instanceof Integer ||
object instanceof Long ||
object instanceof Short ||
object instanceof Byte ||
object instanceof Float ||
object instanceof Double ||
object instanceof Boolean ||
object instanceof Character ||
object instanceof String) {
return String.valueOf(object);
} else {
return JSON.toJSONString(object);
}
好像有点多此一举=-=
get、set方法:
/**
* 数据结构: String
*/
/**
* 缓存获取
*
* @param key
* @param clazz
* @param
* @return
*/
public <T> T get(String key, Class<T> clazz) {
// 先对传入的key进行判空,以免报错
if (key == null) {
return null;
}
String val = redisTemplate.opsForValue()
.get(key);
return JSONObject.parseObject(val, clazz);
}
/**
* 缓存置入并设置时间
*
* @param key
* @param object
* @param expire
* @return
*/
public boolean set(String key, Object object, long expire) {
try {
redisTemplate.opsForValue()
.set(key, objectToJson(object), expire, TimeUnit.SECONDS);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 缓存置入
*
* @param key
* @param value
* @return
*/
public Boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, objectToJson(value));
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
包装了get、限时key set以及永久key set,存取策略和刚刚说的1是一样的
当key不存在时set方法:setnx
SETNX key value
将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
/**
* 当key不存在时,缓存置入
*
* @param key
* @param value
* @param expire
* @param timeUnit
* @return key存在时,返回false
*/
public Boolean setnx(String key, Object value, long expire, TimeUnit timeUnit) {
return redisTemplate.opsForValue().setIfAbsent(key, objectToJson(value), expire, timeUnit);
}
public Boolean setnx(String key, Object value) {
return redisTemplate.opsForValue().setIfAbsent(key, objectToJson(value));
}
包装了限时、不限时的2种setnx方法
递增、递减方法:
/**
* 递增
*
* @param key
* @param by
* @return
*/
public Long increment(String key, long by) {
if (by < 0) {
throw new RuntimeException("递增值须大于0");
}
return redisTemplate.opsForValue().increment(key, by);
}
/**
* 递减
*
* @param key
* @param by
* @return
*/
public Long decrement(String key, long by) {
if (by < 0) {
throw new RuntimeException("递减值须大于0");
}
return redisTemplate.opsForValue().increment(key, -by);
}
递增、递减方法能使设置的整数型value递增/递减,可以达到计数的效果,由于redis操作具有原子性,所以能够在利用这个特性并发地对整数进行递增/递减,可以应用在单机部署的情况下的电商秒杀超卖问题
注:必须是对整数型的value进行操作,否则会报InvalidDataAccessApiUsageException: ERR value is not an integer or out of range;
我们在Test文件夹下创建测试类RedisTester,进行单元测试:
缓存时间测试:
@SpringBootTest
public class RedisTester {
@Autowired
private RedisUtil redisUtil;
@Test
public void redisSetter() throws InterruptedException {
UserPO userPO = new UserPO();
userPO.setUserName("abc");
userPO.setPassword("123");
userPO.setProfile("Hello Redis!");
// 在redis中,以"user"为key缓存该userPO对象2秒
redisUtil.set("user",userPO,2);
// 调用redisGetter方法读取缓存
redisGetter();
}
public void redisGetter() throws InterruptedException {
// 沉睡1秒
Thread.sleep(1000);
System.out.println(redisUtil.get("user", UserPO.class));
}
}
结果:
在缓存时间为2秒,等待读取时间为1秒的情况下,redisGetter方法顺利地读取到了存入缓存的对象,而在缓存过期的情况下:
public void redisGetter() throws InterruptedException {
// 沉睡3秒
Thread.sleep(3000);
System.out.println(redisUtil.get("user", UserPO.class));
}
incr递减测试:
@Test
public void decrTest(){
int i=3;
redisUtil.set("num",i);
while(i-->0){
System.out.println(redisUtil.decrement("num", 1));
}
}
调用redisTemplate.opsForValue().increment(key, by);会返回被递增/递减后的值,利用此特性,在电商场景中,可以将商品库存数据先set到redis中,当有人下单时,便将redis中的商品库存递减1,再异步操作数据库,这样就可以减轻数据库的压力,使用高效的redis判断操作是否成功,来对用户进行下单成功与否的反馈,提升用户体验。
参考自敖丙的Redis 缓存雪崩、击穿、穿透
倘若在同一时间内有大量的缓存过期/失效,大量请求一下子打入数据库,数据库一下就被打死了,这就是redis缓存雪崩:
缓存穿透是指,当用户请求一个缓存和数据库都没有的值(如在一个主键自增数据表中查询一个主键为-1的值),服务端在缓存查询无果后会进入数据库查询,当然是查不到结果的
当这个请求被攻击者恶意利用,发送很多个这样的请求,就会导致我们的数据库压力过大,甚至将我们的数据库搞垮
解决方法:
缓存击穿指的是,在缓存中有一个key是热点,热点的意思即是有很多用户都在请求着这个key,比如说上面说的商品热卖,很多用户都想点开这个id为3的商品了解详情,这时候我们用redis缓存了id为3的商品信息,本来是没问题的,这个key本身在运行中也扛着大量的并发
倘若这个key现在过期了,而用户请求的并发数量还是没有减少,就会有大量的请求打入数据库,数据库就难顶了
解决方法:设置热点数据永不过期,或在缓存失效入库查询时使用互斥锁
缓存击穿跟雪崩有点相似,不同的地方是:
由于在本专栏系列第二篇中已经引入过fastjson,这里的序列化/反序列化就使用fastjson进行操作 ↩︎