当我们网站的数据量过大时,使用Java频繁访问数据库会造成延迟过大、数据丢失等问题,这时候就需要使用缓存技术将经常访问的数据保存在缓存数据库以减少数据库访问。我们经常使用Redis作为缓存数据库。
当客户端在申请数据时会优先发送请求到Redis,如果其中存在数据则直接返回,否则Redis向数据库发送请求。数据库查询到结果后将直接返回给客户端,同时将数据更新到Redis存储中。当数据库中的数据发生变化时,Redis对清空相应地键值对,当下次请求到达时就回去数据库中查询相应的数据并更新到Redis,这样就保证了Redis和数据库数据的一致性。
1、Redis
Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。键的类型只能为字符串,值支持五种数据类型:字符串(String)、列表(List)、集合(Set)、散列表(Hash)、有序集合(Zset)。Redis的操作都是原子型的,所以不需要考虑并发事务管理问题。
关于Redis的安装配置在之前一篇文章中有记录:https://blog.youkuaiyun.com/theVicTory/article/details/107121264#t5
可以通过桌面工具Another Redis Desktop Manager对Redis进行远程管理,这是一款免费且好用的可视化工具
1.1、持久化策略
RDB策略
Redis Database 快照方式是Redis默认的持久化方式,它使用fork函数复制出一份当前进程的副本(子进程);父进程继续处理客户端指令。子进程将内存的数据写入硬盘中的临时文件,写入完成后替换之前的旧文件。优点:1. 多进程处理,性能最快。缺点:1. 若子进程数据未写完而redis异常退出,就会丢失最后一次快照以后更改的所有数据; 2. 数据集比较大的时候,fork比较耗时,造成服务器在一段时间内停止处理客户端的请求。
它会在如下四个场景中被触发
- 自动保存规则:按照redis.conf文件中设置的快照保存规则进行持久化。如下所示,save 900 1代表900秒内如果发生一次key值的变化就进行持久化,同理save 300 10为300秒内发生10次key值变化就持久化
- 执行flushall清除内存时会依据自动保存规则进行持久化
- 执行复制的时候
- 手动命令
save:将内存的数据同步到磁盘中,这个操作会阻塞客户端的请求(比较耗时)。
bgsave:在后台异步执行快照操作,这个操作不会阻塞客户端的请求。
redis默认的储存路径为./,即将持久化文件储存在当前执行目录下的dump.rdb文件,如果需要自定义储存路径,可以修改redis.conf中的dir为目标绝对路径
AOF策略
AOF(Append Only File)持久化会在执行redis命令的时候,把命令写入到.aof文件中,通过保存被执行的写命令来记录数据库状态。
AOF需要手动开启,在redis.conf中将appendonly默认值no改为yes。
随着时间.aof文件会变得越来越大,因此在其超过一定限制时后台会对它进行重写。即创建一个新的AOF文件来替代现有的AOF文件,新旧两个文件所保存的数据库状态是相同的,但是新的AOF文件命令更加简洁,通常体积会较旧AOF文件小很多。重写过程发生了停机,现有的.aof文件也不会丢失,所以它是绝对安全的。
在redis.conf文件中配置重写规则,如下所示,第一行auto-aof-rewrite-percentage 100代表新文件超过原文件100%后进行重写;第二行auto-aof-rewrite-min-size 64mb代表文件大小超过64MB后进行重写。
1.2、过期策略
reids是用内存来缓存的,内存资源很宝贵。所以我们 set key value 的时候一般都会给它设置过期时间。
定期删除:假设redis里放了10万个数据,redis默认会每隔100ms随机抽取设置了过期时间的key来删除。这会导致很多过期的key并没有被删除
惰性删除:当获取某个key的时候,redis会检查一下,这个key是否过期了,如果是就直接删除不返回。缺点很明显是过期且未被查询的key不会被删除
内存淘汰:由于上面的两个策略仍然会导致部分key堆积,所以当内存不足以容纳新写入数据时就会根据算法进行内存淘汰。如下所示为redis.conf中可以设置的内存淘汰策略。例如最常用的allkeys-lru是移除最近最少使用的key;volatile-ttl在设置了过期时间的键中,将更早过期时间的key优先移除。
2、使用Jedis操作redis
2.1、配置连接
Jedis是Redis官方推荐的Java连接开发工具,使用maven引入该依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
接着和jdbc.properties一样设置redis连接配置文件redis.properties
,其位置在src\main\resources\redis.properties
redis.hostname=127.0.0.1
redis.port=6379
redis.password=1234
redis.timeout=30000
redis.database=0
redis.pool.maxActive=600
redis.pool.maxIdle=300
redis.pool.maxWait=3000
redis.pool.testOnBorrow=true
创建redis配置文件src\main\resources\spring\spring-redis.xml,首先在文件中引入配置文件中redis.properties
<context:property-placeholder location="classpath:redis.properties"/>
接着创建redis设置对象JedisPoolConfig
,在其中通过${}读取redis.properties中的变量对连接数、等待时间等进行设置
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<!--最大连接数-->
<property name="maxTotal" value="${redis.pool.maxActive}" />
<!--最多空闲连接数-->
<property name="maxIdle" value="${redis.pool.maxIdle}" />
<!--最大等待时间-->
<property name="maxWaitMillis" value="${redis.pool.maxWait}" />
<!--检查连接有效性-->
<property name="testOnBorrow" value="${redis.pool.testOnBorrow}" />
</bean>
接着创建Redis连接池对象jedisPool
,即传入连接池的配置对象、主机、端口号去初始化连接池对象
<bean id="jedisPool" class="redis.clients.jedis.JedisPool">
<constructor-arg name="poolConfig" ref="jedisPoolConfig"/>
<constructor-arg name="host" value="${redis.hostname}"/>
<constructor-arg name="port" value="${redis.port}" type="int"/>
<constructor-arg name="timeout" value="${redis.timeout}" type="int"/>
<constructor-arg name="password" value="${redis.password}"/>
</bean>
2.2、编写工具类
通过工具类JedisUtil
完成对Redis数据库的实际操作
首先定义JedisPool
的set/get方法用于设置、获取连接池jedisPool。
定义内部方法getJedis()
用于从连接池获取一个具体的jedis连接对象,需要通过具体的jedis连接对象进行键值对的操作。
接着定义一个类Keys
包含键相关的具体操作,这里定义了exists()
方法用于判断键是否存在,其通过调用jedis.exists()实现;del()
用于删除键值。
定义类Strings
对应值为字符串的键值对相关操作,先定义了get()
、setnx()
用于获取、设置字符串类型的键值对。其实现也是通过调用jedis对象的相关方法。
package com.tory.shop.cache;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class JedisUtil {
private JedisPool jedisPool;
public void setJedisPool(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
public JedisPool getJedisPool() {
return jedisPool;
}
//获取一个jedis连接
public Jedis getJedis() {
return jedisPool.getResource();
}
//键
public class Keys {
public boolean exists(String key) { //查询键是否存在
Jedis jedis = getJedis();
boolean exis = jedis.exists(key);
jedis.close();
return exis;
}
//清除键值对
public long del(String... keys) {
Jedis jedis = getJedis();
long count = jedis.del(keys);
jedis.close();
return count;
}
}
//字符串类型
public class Strings {
public String get(String key) { //根据键获取值
Jedis jedis = getJedis();
String value = jedis.get(key);
jedis.close();
return value;
}
public long setnx(String key, String value) { //设置键值对
Jedis jedis = getJedis();
long num = jedis.setnx(key, value);
jedis.close();
return num;
}
}
}
最后在spring-redis.xml文件中注册jedisUtil
类及其子类jedisKeys
、jedisStrings
<bean id="jedisUtil" class="com.tory.shop.cache.JedisUtil">
<property name="jedisPool" ref="jedisPool"/>
</bean>
<bean id="jedisKeys" class="com.tory.shop.cache.JedisUtil$Keys">
<constructor-arg ref="jedisUtil"></constructor-arg>
</bean>
<bean id="jedisStrings" class="com.tory.shop.cache.JedisUtil$Strings">
<constructor-arg ref="jedisUtil"></constructor-arg>
</bean>
3、使用Redis进行缓存
如下所示为获取Area相关信息的AreaService
,在其中通过getAreaList()
获取Area信息数组areaList,由于这是经常访问的信息,我们可以将其用Redis缓存起来。
通过@Autowired自动注入jedisKeys、jedisStrings两个对象。
当查询areaList的请求到达时首先通过jedisKeys
判断“arealist”是否存在缓存中。若不存在则查询数据库,将得到的结果序列化为json字符串并通过jedisStrings
保存在Redis中;若存在则从redis读取到“arealist”对应的json字符串,并反序列化为areaList
对象数组。
当有更新area信息的操作到达时,需要清除Redis的缓存以保证其有效性。例如当执行addArea()操作时,除了调用areaDao完成数据库操作外,还需要通过jedisKeys.del()
清除Redis中对应的“arealist”键值缓存。
package com.tory.shop.service.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tory.shop.cache.JedisUtil;
import com.tory.shop.dao.AreaDao;
import com.tory.shop.entity.Area;
import com.tory.shop.service.AreaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class AreaServiceImpl implements AreaService {
@Autowired
private AreaDao areaDao;
@Autowired
private JedisUtil.Strings jedisStrings;
@Autowired
private JedisUtil.Keys jedisKeys;
public List<Area> getAreaList() {
ObjectMapper objectMapper = new ObjectMapper();
List<Area> areaList = new ArrayList<>();
//若不存在则从数据库中查询并保存到Redis中
if (!jedisKeys.exists("arealist")) {
areaList = areaDao.queryArea();
String jsonStr = null;
try {
jsonStr = objectMapper.writeValueAsString(areaList); //areaList序列化为json字符串
} catch (JsonProcessingException e) {
e.printStackTrace();
}
jedisStrings.setnx("arealist", jsonStr);
} else {
//如果缓存中存在则直接获取
String jsonStr = jedisStrings.get("arealist");
JavaType javaType = objectMapper.getTypeFactory()
.constructParametricType(ArrayList.class, Area.class); //json字符串反序列化为Area数组
try {
areaList = objectMapper.readValue(jsonStr, javaType);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
return areaList;
}
public Void addArea(Area area) {
//添加店铺信息
int affectedRows = areaDao.insertArea(area);
if (affectedRows <= 0)
throw new RuntimeException("插入数据库失败!");
//删除Redis中缓存
jedisKeys.del("arealist");
}
}