前言:redis作为一种优秀的key-value型数据库在解决数据库查询瓶颈方面有着显著的优势,因为Redis性能极高 ,读和写的速度分别达到了110000次/s和81000次/s 。当你的项目页面遇到需要高并发调用后台接口,而且数据量越来越大导致查询异常缓慢时,这时候使用Redis的作为数据的缓存就是一个很好的解决方案。Redis不仅仅支持简单的key-value类型的数据,同时还提供string, list,set,zset,hash等数据结构的存储。那么今天这篇这篇文章我就分享一下自己在linux操作系统中搭建redis服务器并在spring-boot项目中使用spring-data-redis的API缓存接口数据的demo吧。
1. 开发环境
- VMware Workstation 14.0
- windows 10操作系统
- Linux CentOS7操作系统(安装在VMware虚拟机上)
- MySql5.6数据库
- JDK1.8
- IDEA IntelliJ (2018.03版本)
2. linux系统服务器上安装redis服务器
1) 打开VMware Workstation工具,启动之前安装的CentOS7_64虚拟机,然后再打开Xshell6使用root账号和密码工具登录虚拟机,登录成功后显示如下界面
2)安装GCC
输入命令:cd +/ 切换到根目录下,再执行命令:yum install -y gcc(如果控制台显示:yum command not found,请参考这篇博客自行配置yum源:https://www.cnblogs.com/renpingsheng/p/7845096.html)
命令执行结束后,输入 gcc -v
命令检查 GCC 是否安装成功,如果执行命令后输出:gcc version 4.8.5 20150623 (Red Hat 4.8.5-28) (GCC)
类似的信息说明 GCC 安装成功;
3)执行命令:mkdir ./software,并执行命令cd ./software切换到software目录下
然后执行命令:wget http://download.redis.io/releases/redis-4.0.6.tar.gz
下载redis安装包(也可点击redis linux版本下载 下载最新稳定版本redis安装包,然后利用点击Xshell窗口的新建文件传输子菜单,如下图所示:左边选中redis.tar安装包,右边选择Linunx系统目录,最后点击上面的单向右箭头将redis安装包上传到Linux服务器 );
4) 执行命令 tar -zxvf redis-4.0.6.tar.gz -C ../
usr/local/
将 Redis 安装包解压到 usr/local
目录 下;
5)执行命令 cd ../
usr/local/ 切换到redis-4.06所在目录,然后执行命令 mv ./redis-4.06 ./redis 修改目录名称
6) 执行命令 cd redis
,再执行命令 make MALLOC=libc
编译 Redis 安装包源文件;
7)执行命令 cd src && make install
安装 Redis;
8)新建目录 mkdir -p /usr/local/redis/bin /usr/local/redis/etc
;
9) 执行命令 cd /usr/local/redis/src
,打开 src 目录,执行命令将对应的文件复制到上面新建的 bin、etc 目录下:
cp redis-server ../bin/
cp redis-benchmark ../bin/
cp redis-check-rdb ../bin/
cp redis-sentinel ../bin/
cp redis-cli ../bin/
cp ../redis.conf ../etc/
10) 执行命令 vi /etc/profile 命令后按i键进入编辑模式
修改环境变量文件,在文件中添加下面内容:
export REDIS_HOME=/usr/local/redis/
export PATH=${JAVA_HOME}/bin:${REDIS_HOME}/bin:$PATH
编辑完成后按esc键,输入:wq! 按回车键保存退出, 执行 source /etc/profile
命令让修改环境变量文件生效
11)执行vi /usr/local/redis/etc/redis.conf
修改redis 配置文件,修改内容如下:
bind 192.168.220.1
bind 127.0.0.1
daemonize yes
dir /usr/local/redis/data/
appendonly yes
appendfsync always
12)执行命令 redis-server /usr/local/redis/etc/redis.conf
启动 Redis 服务端;
13)执行命令 ps -ef | grep 6379
检查 redis-server
是否已经正常启动。如输出如下信息说明启动成功:
3. Spring-Boot项目整合Redis
3.1 添加spring-boot-starter-redis模块依赖
在我上一篇博客Spring Boot整合Mybatis项目开发Restful API接口搭建的mybatis的demo项目的pom文件中添加如下依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
3.2 Redis连接池配置
在application.yml配置文件中添加如下内容
redis:
database: 0
host: 192.168.220.1
port: 6379
jedis:
pool:
max-active: 8 #连接池最大连接数(使用负值表示没有限制)
max-wait: -1ms
max-idle: 8 #连接池中的最大空闲连接
min-idle: 0
timeout: 60000 #超时时间(ms)
3.3 配置RedisTemplate模板类的对于操作不同数据结构的序列化器。
我们知道redis底层set和get数据都是操作字节数据,所以需要对操作的对象进行字节数据与对象的转换,也即序列化和反序列化。所有要实现序列化的类都要实现java.io.Serializable接口
查看RedisTemplate类的源码,知道它为5种数据类型的对象设置了序列化器, 分别是stringSerializer, keySerializer, valueSerializer,hashKeySerializer,hashValueSerializer和一个默认的序列化器defaultSerializer。除了stringSerializer使用了StringRedisSerializer,其他几个序列化器如果为空都采用defaultSerializer,而如果defaultSerializer如果为空的话,则采用了JdkSerializationRedisSerializer。查看JdkSerializationRedisSerializer类的源码,我们发现该类有个私有属性类Converter<Object, byte[]>接口类serializer和Converter<byte[], Object> deserializer用于序列化对象为byte数组和反序列化byte数组为对象。它们的实现类分别为SerializingConverter和DeserializingConverter。感兴趣的读者可以通过intellj开发工具查看这两个类的源码。
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer keySerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer valueSerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashKeySerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashValueSerializer = null;
private RedisSerializer<String> stringSerializer = new StringRedisSerializer();
3.4 RedisTemplate模板类对不通数据结构的操作类介绍
同时RedisTemplate模板类为操作不通类型的数据结构提供了不通的操作类
- 为操作字符窜和对象提供了ValueOperations<K, V> 接口;
- 为操作List类型数据提供了ListOperations<K, V>接口;
- 为操作Set类型的数据提供了SetOperations<K, V> 接口;
- 为操作有序集合ZSet提供了ZSetOperations<K, V>接口;
- 为操作Map类型的数据提供了HashOperations<K, HK, HV>接口
- 为操作地图提供了GeoOperations<K, V>接口
// cache singleton objects (where possible)
private @Nullable ValueOperations<K, V> valueOps;
private @Nullable ListOperations<K, V> listOps;
private @Nullable SetOperations<K, V> setOps;
private @Nullable ZSetOperations<K, V> zSetOps;
private @Nullable GeoOperations<K, V> geoOps;
private @Nullable HyperLogLogOperations<K, V> hllOps;
RedisTemplate模板类获取几种数据结构的操作类方法源码如下,其实现类都采用了相应的默认操作类。
@Override
public ValueOperations<K, V> opsForValue() {
if (valueOps == null) {
valueOps = new DefaultValueOperations<>(this);
}
return valueOps;
}
@Override
public ListOperations<K, V> opsForList() {
if (listOps == null) {
listOps = new DefaultListOperations<>(this);
}
return listOps;
}
@Override
public SetOperations<K, V> opsForSet() {
if (setOps == null) {
setOps = new DefaultSetOperations<>(this);
}
return setOps;
}
@Override
public ZSetOperations<K, V> opsForZSet() {
if (zSetOps == null) {
zSetOps = new DefaultZSetOperations<>(this);
}
return zSetOps;
}
@Override
public <HK, HV> HashOperations<K, HK, HV> opsForHash() {
return new DefaultHashOperations<>(this);
}
@Override
public GeoOperations<K, V> opsForGeo() {
if (geoOps == null) {
geoOps = new DefaultGeoOperations<>(this);
}
return geoOps;
}
关于以上几个操作类的方法API, 建议读者去查看Spring Data Redis 2.1.5.RELEASE API文档,不建议读者逐个类地去看源码。
文档链接:https://docs.spring.io/spring-data/redis/docs/current/api/
3.5 RedisTemplate模板类bean配置
在spring-boot项目的启动类中添加如下bean的配置,在这里我对其他序列化器使用了Jackson2JsonRedisSerializer类,参考了这篇博客:https://www.cnblogs.com/shamo89/p/8622152.html
@Bean(name="objectRedisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory){
RedisSerializer<String> stringSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean(name="listOpsTemplate")
public RedisTemplate<String,CityTO> listOpsTemplate(RedisConnectionFactory connectionFactory){
RedisSerializer<String> stringSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer<CityTO> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<CityTO>(CityTO.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
RedisTemplate<String, CityTO> template1 = new RedisTemplate<String, CityTO>();
template1.setConnectionFactory(connectionFactory);
template1.setKeySerializer(stringSerializer);
template1.setValueSerializer(jackson2JsonRedisSerializer);
template1.setHashKeySerializer(jackson2JsonRedisSerializer);
template1.setHashValueSerializer(jackson2JsonRedisSerializer);
return template1;
}
4. Demo项目开发实战
4.1 数据库建表与添加数据sql
1) 使用mysql-workbench客户端, 登录mysql数据库服务器,执行以下以下sql建表语句,建立province_cities表
use test;
create or replace table province_cities(
city_code int8 primary key,
parent_code int4 not null,
city_name varchar(20) not null
)ENGINE = InnoDB default CHARSET=utf8;
在com.example.mybatis.model包下新建CityTO实体类,代码如下
package com.example.mybatis.model;
import java.io.Serializable;
public class CityTO implements Serializable {
private Integer cityCode;
private Integer parentCode;
private String cityName;
public Integer getCityCode() {
return cityCode;
}
public void setCityCode(Integer cityCode) {
this.cityCode = cityCode;
}
public Integer getParentCode() {
return parentCode;
}
public void setParentCode(Integer parentCode) {
this.parentCode = parentCode;
}
public String getCityName() {
return cityName;
}
public void setCityName(String cityName) {
this.cityName = cityName;
}
}
2) 执行以下sql脚本添加数据
insert into province_cities(city_code,parent_code,city_name)
values(0000,-1,"中国");
insert into province_cities(city_code,parent_code,city_name)
values(1000,0,"北京市");
insert into province_cities(city_code,parent_code,city_name)
values(100001,1000,"东城区");
insert into province_cities(city_code,parent_code,city_name)
values(100002,1000,"西城区");
insert into province_cities(city_code,parent_code,city_name)
values(100003,1000,"朝阳区");
insert into province_cities(city_code,parent_code,city_name)
values(100004,1000,"丰台区");
insert into province_cities(city_code,parent_code,city_name)
values(100005,1000,"石景山区");
insert into province_cities(city_code,parent_code,city_name)
values(100006,1000,"海淀区");
insert into province_cities(city_code,parent_code,city_name)
values(100007,1000,"门头沟区");
insert into province_cities(city_code,parent_code,city_name)
values(100008,1000,"房山区");
insert into province_cities(city_code,parent_code,city_name)
values(100009,1000,"通州区");
insert into province_cities(city_code,parent_code,city_name)
values(100010,1000,"顺义县");
insert into province_cities(city_code,parent_code,city_name)
values(100011,1000,"昌平区");
insert into province_cities(city_code,parent_code,city_name)
values(100012,1000,"大兴区");
insert into province_cities(city_code,parent_code,city_name)
values(100013,1000,"怀柔区");
insert into province_cities(city_code,parent_code,city_name)
values(100014,1000,"平谷区");
insert into province_cities(city_code,parent_code,city_name)
values(100015,1000,"密云县");
insert into province_cities(city_code,parent_code,city_name)
values(100016,1000,"延庆县");
insert into province_cities(city_code,parent_code,city_name)
values(1001,0000,"湖南省");
insert into province_cities(city_code,parent_code,city_name)
values(100101,1001,"长沙市");
insert into province_cities(city_code,parent_code,city_name)
values(100102,1001,"株洲市");
insert into province_cities(city_code,parent_code,city_name)
values(100103,1001,"湘潭市");
insert into province_cities(city_code,parent_code,city_name)
values(100104,1001,"衡阳市");
insert into province_cities(city_code,parent_code,city_name)
values(100105,1001,"常德市");
insert into province_cities(city_code,parent_code,city_name)
values(100106,1001,"岳阳市");
insert into province_cities(city_code,parent_code,city_name)
values(100107,1001,"张家界市");
insert into province_cities(city_code,parent_code,city_name)
values(100108,1001,"益阳市");
insert into province_cities(city_code,parent_code,city_name)
values(100109,1001,"邵阳市");
insert into province_cities(city_code,parent_code,city_name)
values(100110,1001,"郴州市");
insert into province_cities(city_code,parent_code,city_name)
values(100111,1001,"永州市");
insert into province_cities(city_code,parent_code,city_name)
values(100112,1001,"娄底市");
insert into province_cities(city_code,parent_code,city_name)
values(100113,1001,"怀化市");
insert into province_cities(city_code,parent_code,city_name)
values(100114,1001,"湘西吉首自治州");
insert into province_cities(city_code,parent_code,city_name)
values(1002,0000,"广东省");
insert into province_cities(city_code,parent_code,city_name)
values(100201,1002,"广州市");
insert into province_cities(city_code,parent_code,city_name)
values(100202,1002,"深圳市");
insert into province_cities(city_code,parent_code,city_name)
values(100203,1002,"珠海市");
insert into province_cities(city_code,parent_code,city_name)
values(100204,1002,"佛山市");
insert into province_cities(city_code,parent_code,city_name)
values(100205,1002,"江门市");
insert into province_cities(city_code,parent_code,city_name)
values(100206,1002,"肇庆市");
insert into province_cities(city_code,parent_code,city_name)
values(100207,1002,"惠州市");
insert into province_cities(city_code,parent_code,city_name)
values(100208,1002,"东莞市");
insert into province_cities(city_code,parent_code,city_name)
values(100209,1002,"中山市");
insert into province_cities(city_code,parent_code,city_name)
values(100210,1002,"湛江市");
insert into province_cities(city_code,parent_code,city_name)
values(100211,1002,"茂名市");
insert into province_cities(city_code,parent_code,city_name)
values(100212,1002,"阳江市");
insert into province_cities(city_code,parent_code,city_name)
values(100213,1002,"云浮市");
insert into province_cities(city_code,parent_code,city_name)
values(100214,1002,"汕头市");
insert into province_cities(city_code,parent_code,city_name)
values(100215,1002,"汕尾市");
insert into province_cities(city_code,parent_code,city_name)
values(100216,1002,"潮州市");
insert into province_cities(city_code,parent_code,city_name)
values(100217,1002,"揭阳市");
insert into province_cities(city_code,parent_code,city_name)
values(100218,1002,"韶关市");
insert into province_cities(city_code,parent_code,city_name)
values(100219,1002,"梅州市");
insert into province_cities(city_code,parent_code,city_name)
values(100220,1002,"河源市");
insert into province_cities(city_code,parent_code,city_name)
values(100221,1002,"清远市");
4.2 RedisTemplate操作String数据,实现一个简单的用户登录功能(数据库用的之前的test.userinfo表)
新建RedisController控制器类,下面直接上代码
@RestController
@RequestMapping("/redis")
public class RedisController {
public static final String REDIS_CACHE_PREFIX = "cache_prefix:";
@Resource(name="objectRedisTemplate")
private RedisTemplate<String,Object> redisTemplate;
//一个简单的用户登录接口,用redis作缓存单个对象
@RequestMapping(value="/login",method = RequestMethod.POST)
public ServiceResponse<String> userLogin(@RequestParam("userAccount")String userAccount, @RequestParam("password")String password){
ServiceResponse<String> response = new ServiceResponse<>();
UserTO userTO = (UserTO) redisTemplate.opsForValue().get(REDIS_CACHE_PREFIX+userAccount);
if(null==userTO){
userTO = userService.queryUserInfoByAccount(userAccount).getData();
//缓存30分钟
if(null==userTO || null!=userTO && !userTO.getPassword().equals(password)){
response.setStatus(ConstantEnums.LOGIN_ACCOUNT_PASSWORD_ERROR.getStatus());
response.setMessage(ConstantEnums.LOGIN_ACCOUNT_PASSWORD_ERROR.getMessage());
response.setData("login failed");
return response;
}
redisTemplate.opsForValue().set(REDIS_CACHE_PREFIX+userAccount,userTO,30, TimeUnit.MINUTES);
}
response.setStatus(ConstantEnums.LOGIN_SUCCESS.getStatus());
response.setMessage(ConstantEnums.LOGIN_SUCCESS.getMessage());
response.setData("ok");
return response;
}
//退出登录
@RequestMapping(value="/loginout",method=RequestMethod.GET)
public ServiceResponse<String> loginOut(@RequestParam("userAccount")String userAccount){
ServiceResponse<String> response = new ServiceResponse<>();
if(redisTemplate.opsForValue().get(REDIS_CACHE_PREFIX+userAccount)==null){
response.setStatus(ConstantEnums.LOGIN_OFFLINE.getStatus());
response.setMessage(ConstantEnums.LOGIN_OFFLINE.getMessage());
return response;
}
Boolean result = redisTemplate.delete(REDIS_CACHE_PREFIX+userAccount);
if(result){
response.setStatus(ConstantEnums.LOGIN_OUT_SUCCESS.getStatus());
response.setMessage(ConstantEnums.LOGIN_OUT_SUCCESS.getMessage());
response.setData("index.html");
}else{
response.setStatus(ConstantEnums.LOGIN_OUT_ERROR.getStatus());
response.setMessage(ConstantEnums.LOGIN_OUT_ERROR.getMessage());
}
return response;
}
}
这里我的application.yml的server.port改成了8080端口
打开虚拟机,使用Xshell登录CentOS7系统后执行命令 redis-server /usr/local/redis/etc/redis.conf
启动 Redis 服务端。然后再启动spring-boot项目,项目启动项目成功后,使用postman测试用户登录接口。我们发现第1次登录用时1158ms, 之后在缓存失效之前再次调用接口只用时75ms,显著缩短了接口响应时间。
4.3 RedisTemplate操作List结构数据
1) 在RedisController类中注入name=listOpsTemplate的RedisTemplate和ICityService接口代码如下:
ICityService接口代码详情见我个人的码云项目相关链接:https://gitee.com/heshengfu1211/mybatisProjectDemo/tree/master/src/main/java/com/example/mybatis/service
@Resource(name="listOpsTemplate")
private RedisTemplate<String, CityTO> listOpsTemplate;
@Autowired
private ICityService cityService;
2) 再在RedisController类中添加如下一段代码,实现带有Redis缓存功能的查询parentCode相同的城市列表的接口
//redisTemplate操作List数据结构
@RequestMapping(value="/list/cities",method=RequestMethod.GET)
public ServiceResponse<List<CityTO>> queryCitiesByParentCode(@RequestParam("parentCode") Integer parentCode){
ServiceResponse<List<CityTO>> response = null;
ListOperations<String, CityTO> listOps = listOpsTemplate.opsForList();
List<CityTO> cityList = listOps.range(REDIS_CACHE_PREFIX+parentCode,0,-1);
if(cityList.size()==0){
response = cityService.queryCityByParentCode(parentCode);
cityList = response.getData();
CityTO[] tempArray = new CityTO[cityList.size()];
CityTO[] cityArray = cityList.toArray(tempArray);
if(cityList.size()>0){
listOps.leftPushAll(REDIS_CACHE_PREFIX+parentCode,cityArray);
//缓存12个小时
listOpsTemplate.expire(REDIS_CACHE_PREFIX+parentCode,12,TimeUnit.HOURS);
}
}else{
response = new ServiceResponse();
response.setStatus(200);
response.setMessage("ok");
response.setData(cityList);
}
return response;
}
使用postman测试接口,发现接口第1次调用用时203ms, 在缓存有效期间再次调用仅用时19ms
4.4 RedisTemplate操作Hash结构数据
1) RedisController类中新建返回结果data数据类型为Map的queryCitiesByParentCodes方法,实现一次性查询多个父节点城市下的子节点城市,代码如下:
@RequestMapping(value="/map/cities",method=RequestMethod.POST)
public ServiceResponse<Map<String,List<CityTO>>> queryCitiesByParentCodes(@RequestBody List<Integer> parentCodes){
ServiceResponse<Map<String,List<CityTO>>> response = new ServiceResponse<>();
HashOperations<String,String,CityTO> hashOperations = listOpsTemplate.opsForHash();
Map<String,List<CityTO>> resultMap = new HashMap<>();
List<CityTO> cityList = null;
for(Integer code:parentCodes){
//先从redis缓存里面查
cityList = hashOperations.values(REDIS_CACHE_PREFIX+"citisMap:"+code);
if(cityList.size()==0){
//redis缓存里没有再去数据库查,并把查出来的对象放入redis的Map当中并设置缓存有效时间12h
cityList = cityService.queryCityByParentCode(code).getData();
for(CityTO cityTO:cityList){
hashOperations.put(REDIS_CACHE_PREFIX+"citisMap:"+code,String.valueOf(cityTO.getCityCode()),cityTO);
listOpsTemplate.expire(String.valueOf(cityTO.getCityCode()),12,TimeUnit.HOURS);
}
}
resultMap.put(String.valueOf(code),cityList);
}
response.setStatus(200);
response.setMessage("ok");
response.setData(resultMap);
return response;
}
使用postman测试接口,同时查询cityCodes=[1000,1001,1002]父节点列表下的子城市列表,发现第1次调用耗时1000ms,在缓存有效时间再次调用仅用时42ms
5 小结
本文演示了spring-boot 整合redis,如何实现数据库缓存,显著提高接口响应速度。利用RedisTemplate模板类操作string, list和和hash 三中数据结构,限于文章篇幅,关于利用redis获取地图信息,操作管道,和实现分布式事物锁以及集群部署实现高可用将在以后的博客上继续演示。
6 参考文章
1)黄朝兵的达人课第3课:整合常用技术框架之 JPA 和 Redis
2)Redis(九):使用RedisTemplate访问Redis数据结构API大全