目录
Jedis
SpringBoot集成Redis
序列化回顾与Redis序列化
自定义Tempate
Redis工具类
学习参考: 【狂神说Java】Redis最新超详细版教程通俗易懂
Jedis
我们要使用java来操作Redis,那么什么是Jedis呢,它是Redis官方推荐的java连接开发工具,是使用java来操作Redis的中间件。
使用Jedis十分简单
- 创建一个Maven项目
- 添加Jedis的Jar包
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
实例(一)连接到Redis服务
@Test
public void connectRedisTest(){
// 连接本机的Redis服务
Jedis jedis = new Jedis("127.0.0.1",6379);
// 测试服务是否连接
System.out.println(jedis.ping());
}
结果:
实例(二)通过Jedis再次理解事务
@Test
public void txTest() {
// 清空当前数据库所有数据
jedis.flushDB();
// 创建一个json对象
JSONObject jsonObject = new JSONObject();
jsonObject.put("hello","world");
jsonObject.put("name","claw");
// 开启事务
Transaction multi = jedis.multi();
String result = jsonObject.toJSONString();
try {
multi.set("user1",result);
multi.set("user2",result);
// 错误代码
int i = 1/0;
// 执行事务
multi.exec();
} catch (Exception e) {
// 放弃事务
multi.discard();
e.printStackTrace();
} finally {
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
}
}
SpringBoot集成Redis
在SpringBoot中,操作数据层面的行为都集成在SpringData里。
步骤:
- 创建一个新的SpringBoot项目
- 添加所需依赖,Spring Data Redis(Access+Driver)
值得一提的是,在SpringBoot2.x以后,原来使用的jedis被替换为了lettuce。
jedis:采用的是直连,多个线程操作的话,是不安全的。如果想要避免不安全,使用jedis poo连接池,更像Bio模式。
lettuce:采用netty,实例可以在多个线程中进行共享,不存在线程不安全的情况,可以减少线程数据,更像Nio模式。
找到 RedisAutoConfiguration类,这是Redis自动配置类,从它我们可以找到RedisProperties,可以直观的看到我们可以配置的东西,比如默认端口号是0,url,host,password等等。
public class RedisProperties {
/**
* Database index used by the connection factory.
*/
private int database = 0;
/**
* Connection URL. Overrides host, port, and password. User is ignored. Example:
* redis://user:password@example.com:6379
*/
private String url;
/**
* Redis server host.
*/
private String host = "localhost";
/**
* Login password of the redis server.
*/
private String password;
/**
* Redis server port.
*/
private int port = 6379;
/**
* Whether to enable SSL support.
*/
private boolean ssl;
/**
* Connection timeout.
*/
private Duration timeout;
/**
* Client name to be set on connections with CLIENT SETNAME.
*/
// 略。。。
}
源码分析:RedisAutoConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
// 默认的RedisTemplaye 没有过多的设置,redis的对象都是需要序列化的
// 两个泛型都是Object,Object类型,我们后使用需要强制转换 <String,Object>
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
// 由于String是redis是最常使用的类型,所以单独提出来一个bean
@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
源码中两个方法都需要 RedisConnectionFactory类型的参数,点击这个类,可以看到RedisConnectionFactory是一个接口。
有两个实现类,分别是JedisConnectionFactory和LettuceConnectionFactory,其中点开JedisConnectionFactory的源码,能够发现JedisConnectionFactory很多爆红,也就是说,它不是一个能生效的类了。而LettuceConnectionFactory配置齐全,可使用。
在SpringBoot2.0以后,底层配置已经采用了Lettuce了。因此如果我们在配置文件配置的时候,需要配跟Lettuce相关的。
redisTempate操作不同的数据类型
除了这些最基础的操作, 常用的方法可以直接通过redisTempate来操作比如事务和基本的增删改查。
实例(一)通过redisTempate进行一个简单的操作
@Test
void contextLoads() {
// 放入一组数据
redisTemplate.opsForValue().set("mykey", "claw");
// 得到mykey的值
System.out.println(redisTemplate.opsForValue().get("mykey"));
}
结果:
但是如果我们通过Redis-cli控制台查看mykey是有问题的,这是因为我们没有序列化的缘故。(默认使用了JDK序列化的原因)
可以看到RedisTempate本身也配置了许多东西,在源码里可以看到 如下几个变量是跟序列化有关的。
public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {
private boolean enableTransactionSupport = false;
private boolean exposeConnection = false;
private boolean initialized = false;
private boolean enableDefaultSerializer = true;
private @Nullable RedisSerializer<?> defaultSerializer;
private @Nullable ClassLoader classLoader;
@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 = RedisSerializer.string();
// 略
}
我们能在 RedisTempate源码里看到,默认的序列化是通过JDK来序列化的,JDK的序列化会有在命令行中获取值时出现刚刚的问题:有许多斜杠(转义),所以我们需要考虑Json来序列化。
为什么会产生无法识别的字符呢?
JdkSerializationRedisSerializer,这个序列化方法是jdk提供的,要求要被序列化的类继承自Serializeable接口,然后通过Jdk对象序列化的方法保存,这个序列化保存的对象,即使是个String类型的,在redis控制台,也是看不出来的,因为它保存了一些对象的类型什么的额外信息。
————————————————
参考:RedisTemplate的使用说明
也就是说,我们需要自定义一个redisTempate,再此之前,我们先回顾一下序列化。
首先编写一个POJO类
/**
* 一个没有序列化的POJO对象
* @author Claw
*/
@Component
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {
private String name;
private Integer age;
}
实例(一) :测试一下没有序列化会产生的结果,去保存一个Java对象。
@Test
void test() throws JsonProcessingException {
// 真实的开发一般都使用Json来传递对象
User user = new User("claw", 10);
// 使用Json来传递对象
String josnValue = new ObjectMapper().writeValueAsString(user);
redisTemplate.opsForValue().set("user",josnValue);
System.out.println(redisTemplate.opsForValue().get("user"));
}
结果:
{"name":"claw","age":10}
但如果没有使用Json来传递对象,而是直接使用对象保存进Redis的时候,会抛出没有序列化的异常。
实例(二)直接用对象去保存
@Test
void test() throws JsonProcessingException {
User user = new User("claw", 10);
// 直接使用未经过序列化的对象进行保存
redisTemplate.opsForValue().set("user",user);
System.out.println(redisTemplate.opsForValue().get("user"));
}
抛出异常
我们把对象序列化,实现Serializable接口.
/**
* 一个序列化的POJO对象
* @author Claw
*/
@Component
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User implements Serializable {
private String name;
private Integer age;
}
此时就能够正常执行了。在企业中,所有的POJO类都要进行序列化。
序列化回顾与Redis序列化
参考:redis 与 序列化
序列化:把对象转化为可传输的字节序列过程称之为序列化。
反序列化:把字节序列还原为对象的过程称之为反序列化。
为什么需要序列化呢?
序列化的最终目的是为了对象可以跨平台存储和进行网络传输。我们进行跨平台存储和网络传输的方式是IO,IO支持的数据格式就是字节数组。
我们单方面的把对象转为字节数组还不行,因为没有规则的字节数组我们是没有办法将对象的本来面目还原回来的,所以我们必须把对象转为字节数组的时候定制一种规则(序列化),那么我们从IO流里读出数据的时候再以这种规则还原回来(反序列化)
什么情况下需要序列化?
凡是需要进行跨平台存储和网络传输的数据,都需要进行序列化。
序列化的方式
序列化只是一种拆装组装对象的一种规则,那么这种规则肯定也可能有多种多样,比如现在常见的序列化方式有:
- JDK(不支持跨语言)
- JSON
- XML
- Hessain
- Kryo(不支持跨语言)
- Thrift
- Protostuff
- FST(不支持跨语言)
Java序列化中常见的问题
- static属性不能序列化
原因:序列化保存的是对象的状态,静态变量属于类的状态,因此序列化并不保存静态变量。
- 序列化版本号serialVersionUID
所有实现序列化的对象都必须要有个版本号,这个版本号可以由我们自己定义,当我们没有定义的时候,JDK会按照我们的对象属性生成一个对应的版本号。使用JDK生成的serivalVersionUID,只要对象有一丁点改变serialVersionUID就会随着变更。因此建议自己手动定义该版本号。
这一点我曾在写JAVA的IO的时候写过,但是居然转眼间就又忘记了....
回顾自己写的 java:高级 对象的序列化
Redis 序列化
当我们使用Redis的key和Value时,value对于redis就是一个byte array,我们需要自己负责把数据结构转为byte array,等读取时再读出来。
redis的String是一个特例,因为它自己本身几乎已经是byte array了。所以不需要自己处理。
Spring的redisTempate默认使用 java serialization做序列化。如果使用StringRedisTempate,那么我们所set的所有数据都会被toStrng一下再转存到redis里。但这个toString不一定能反解析的回来。如果使用java原生序列化的方式,可能会有远程代码执行问题,因此建议使用其他序列化方式替代。
自定义Tempate
现在我们知道了对象需要序列化,我们也知道,默认的序列化是JDK的序列化。
如果我们想要实现其他的序列化方式,需要自定义RedisTemplate。
那么自定义的RedisTemplate该如何实现呢?我们需要配置具体的序列化方式。
在RedisTempate对象中有一个setKeySerializer()的方法,它需要一个RedisSerializer的类型作为参数,而RedisSerializer是一个接口,我们可以看到RedisSerializer有很多实现类,这就是它不同的序列化的方式。可以看到默认的JdkSerializationRedisSerializer是我们JDK的序列化方式,以及Json、StringRedis等序列化的方式。
根据不同的需要,选择不同的实现类来实现我们所需要的序列化
自定义RedisTemplate代码的的编写:
/**
* 自定义RedisTemplate
*
* @author Claw
* @date 2020/6/27 16:21.
*/
@Component
public class RedisConfig {
@Bean("myRedisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
// 设置键(key)的序列化采用StringRedisSerializer
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 设置值(value)的序列化才FastJsonRedisSerializer
redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
配置完自定义的RedisTemplate,在注入它的时候记得要和源码中的RedisTemplate区分开来。
使用Qualifier作为区分
@Autowired
@Qualifier("myRedisTemplate")
RedisTemplate redisTemplate;
我们通过自定义的RedisTemplate完成序列化以后,在redis存放对象时,就能够成功从redis-cil中查看到set的数据了。
127.0.0.1:6379> keys *
1) "user"
Redis工具类
RedisTemplate对不同的数据类型提供了不同的api给我们操作,但这么多,都会用到吗?在实际开发中,都不会使用原生的方式去编写代码。
常用的操作我们可以写个工具类去操作,对 RedisTemplate的底层做一层封装,调用的时候就方便了。
package com.claw.redis02springboot.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
// 在我们真实的分发中,或者你们在公司,一般都可以看到一个公司自己封装RedisUtil
@Component
public final class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
*/
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 {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
有了RedisUtil工具类,去调用就非常方便了。
class RedisTest {
@Autowired
@Qualifier("redisTemplate")
RedisTemplate redisTemplate;
@Autowired
RedisUtil redisUtil;
@Test
void test() throws JsonProcessingException {
User user = new User("claw", 10);
// 直接使用未经过序列化的对象进行保存
// 原来的未封装的
// redisTemplate.opsForValue().set("user",user);
// System.out.println(redisTemplate.opsForValue().get("user"));
// 使用RedisUtils进行操作
redisUtil.set("claw", 12);
System.out.println(redisUtil.get("claw"));
}