1.SpringBoot整合redis基本配置
1. 使用java操作redis基础
==Jedis====Spring Data redis==
SpringDatalettuce 2.导入依赖3. 源码分析和配置连接
源码分析
==RedisAutoConfiguration====RedisProperties== 自定义连接配置
直接使用RedisTemplate测试 4. 自定义RedisTemplate
源码分析-为何需要自定义RedisTemplate
==@ConditionalOnMissingBean====源码默认jdk序列化==
问题:原因 自定义RedisTemplate
配置类的创建序列化的设置与启用
==RedisSerializer====setKeySerializer====afterPropertiesSet== 扩展:自定义Redis工具类 2. 使用自定义的RedisTemplate操作redis
1.opsForValue()-String2.opsForHash()-哈希表3.opsForList()-list数据4.opsForSet()-set数据5.opsForZSet()-有序set6.测试全局数据7.多次访问一个key,可以进行绑定,简化代码测试中常见错误总结 3.事务处理
Redis事务操作过程
==multi-开启事务==命令入队==exec-执行事务====discard-取消事务== 事务中出现错误的处理
代码语法错误(编译时异常)代码逻辑错误 (运行时异常) 监控
锁的思想
悲观锁:乐观锁: ==watch key== Spring编程式事务管理redis事务
==RedisOperations==
1.SpringBoot整合redis基本配置 1. 使用java操作redis基础 Jedis参考牛客网高级项目教程
狂神说Redis教程笔记
**使用java操作redis的一个中间件,Redis 官方推荐的 java连接开发工具 **
Jedis是Redis官方推出的一款面向Java的客户端,提供了很多接口供Java语言调用。
使用jedis对象,操作方法函数与redis的api完全一致
public class TestPing {
public static void main(String[] args) {
// 1、 new Jedis 对象即可
Jedis jedis = new Jedis("127.0.0.1", 6379);
// jedis 所有的命令就是我们之前学习的所有指令!所以之前的指令学习很重要
String response = jedis.ping();
System.out.println(response); // PONG
}
}
Spring Data redis
SpringData
SpringData 也是和 SpringBoot 齐名的项目!
SpringBoot 操作数据全部封装在spring-data这个接口中
例如:jpa jdbc mongodb redis!
即,操作redis,使用spring-data-redis
lettuce
在 SpringBoot2.x 之后,原来使用的jedis 被替换为了 lettuce?
jedis : 采用的直连,多个线程操作的话,是不安全的,
如果想要避免不安全的,使用 jedis pool 连接池! 更像 BIO(阻塞) 模式
lettuce : 采用netty,实例可以再多个线程中进行共享,不存在线程不安全的情况!
可以减少线程数据了,更像 NIO 模式
SpringDataRedis简介
Spring-data-redis是spring大家族的一部分,提供了在srping应用中通过简单的配置访问redis服务,对reids底层开发包(Jedis, JRedis, and RJC)进行了高度封装,RedisTemplate提供了redis各种操作、异常处理及序列化,支持发布订阅,并对spring 3.1 cache进行了实现。 2.导入依赖
版本号父类依赖中有测试好兼容性比较好的版本,不写,默认使用父类的中指明的版本
3. 源码分析和配置连接 源码分析 RedisAutoConfigurationorg.springframework.boot spring-boot-starter-data-redis
我们在学习SpringBoot自动配置的原理时,整合一个组件并进行配置一定会有一个自动配置类
xxxAutoConfiguration,并且在spring.factories中也一定能找到这个类的完全限定名。Redis也不例外。
那么就一定还存在一个RedisProperties类
之前我们说SpringBoot2.x后默认使用Lettuce来替换Jedis,现在我们就能来验证了。
先看Jedis:
@ConditionalOnClass注解中有两个类是默认不存在的,所以Jedis是无法生效的
然后再看Lettuce:
完美生效。
现在我们回到RedisAutoConfiguratio
只有两个简单的Bean
RedisTemplateStringRedisTemplate
当看到xxTemplate时可以对比RestTemplat、SqlSessionTemplate,通过使用这些Template来间接操作组件。那么这俩也不会例外。分别用于操作Redis和Redis中的String数据类型。
在RedisTemplate上也有一个条件注解@ConditionalOnMissingBean,说明我们是可以对其进行定制化的
编写配置文件然后连接Redis,就需要阅读RedisProperties
RedisProperties这是一些基本的配置属性。
还有一些连接池相关的配置。注意使用时一定使用Lettuce的连接池。
有默认的配置,可以根据自己的需要进行自定义修改
自定义连接配置本项目中,使用空白的数据库11注意spring.redis.host在linux系统下不能写localhost
#redis相关配置 spring.redis.database=11 spring.redis.host=虚拟机ip地址 spring.redis.port=6379直接使用RedisTemplate测试
编写配置文件
一定是虚拟机的ip,不是localhost或127.0.0.1
#redis相关配置 spring.redis.database=11 spring.redis.host=192.168.***.*** spring.redis.port=6379
使用RedisTemplate
@SpringBootTest
class Redis02SpringbootApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
// redisTemplate 操作不同的数据类型,api和我们的指令是一样的
// opsForValue 操作字符串 类似String
// opsForList 操作List 类似List
// opsForHah
// 除了基本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务和基本的CRUD
// 获取连接对象
//RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
//connection.flushDb();
//connection.flushAll();
redisTemplate.opsForValue().set("mykey","kuangshen");
System.out.println(redisTemplate.opsForValue().get("mykey"));
}
}
测试结果
此时我们回到Redis查看数据时候,惊奇发现全是乱码,可是程序中可以正常输出:
这时候就关系到存储对象的序列化问题,在网络中传输的对象也是一样需要序列化,否者就全是乱码。
因此需要自定义RedisTemplate和自定义序列化方式
使用**@ConditionalOnMissingBean,没有自定义value值名称的bean时,才会注入当前类**
即表明只要自定义name的值的类,就会注入自定义的类, RedisTemplate是用来访问redis数据的模板类Spring自带的key是Object类,使用范围更广,但对于redis的key一般都是String,使用不方便
源码默认jdk序列化 问题:源码中的RedisTemplate默认使用的是jdk序列化,
对于自定义的类对象,容易乱码等 原因
在最开始就能看到几个关于序列化的参数,默认都是null,即不指明序列化器。
在启动配置的函数afterPropertiesSet中,会新建默认的jdk序列化器,
默认的序列化器是采用JDK序列化器
而默认的RedisTemplate中的所有序列化器都是使用这个序列化器:
即,如果没有自定义设置序列化器,就使用默认的序列化器
后续我们定制RedisTemplate就可以对其进行修改
自定义RedisTemplate 配置类的创建参照源码,将Object类型改成String类型,并创建实例注入连接工厂redisConnectionFactory,并将连接工厂传给实例最后返回这个bean,即将这个bean注入到SpringIOC中
@Configuration
public class RedisConfig {
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
// 将template 泛型设置为
RedisTemplate template = new RedisTemplate();
// 连接工厂,不必修改
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
序列化的设置与启用
RedisSerializer
需要改用JSON或者String类型的序列化RedisSerializer提供了多种序列化方案:
对于key,使用框架中自带的json序列化对于value,使用框架中自带的String序列化
setKeySerializerRedisTemplate调用set序列化的方法
将RedisSerializer的静态方法获取的序列化传给这个方法
afterPropertiesSet
最后调用afterPropertiesSet方法启动设置
设置好的序列化器,就不会使用默认的jdk序列化器
afterPropertiesSet方法,初始化bean的时候执行,可以针对某个具体的bean进行配置。
afterPropertiesSet 必须实现 InitializingBean接口。实现 InitializingBean接口必须实现afterPropertiesSet方法。
@Configuration
public class RedisConfig {
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
// 设置key的序列号方式
template.setKeySerializer(RedisSerializer.string());
// 设置value的序列化方式
template.setValueSerializer(RedisSerializer.json());
// 设置hash的key的序列化方式
template.setHashKeySerializer(RedisSerializer.string());
// 设置hash的value的序列化方式
template.setHashValueSerializer(RedisSerializer.json());
// 启动设置
template.afterPropertiesSet();
return template;
}
}
扩展:自定义Redis工具类
使用RedisTemplate需要频繁调用.opForxxx然后才能进行对应的操作,这样使用起来代码效率低下,工作中一般不会这样使用,而是将这些常用的公共API抽取出来封装成为一个工具类,然后直接使用工具类来间接操作Redis,不但效率高并且易用。
工具类参考博客:
SpringBoot整合Redis及Redis工具类撰写
java redisUtils工具类很全
2. 使用自定义的RedisTemplate操作redis解决不能连接redis的问题
spring boot连接linux服务器上的redis
1.opsForValue()-String使用opsForValue()访问String类型数据
// 测试redis添加字符串
@Test
public void testString() {
String redisKey = "test:count";
redisTemplate.opsForValue().set(redisKey, 1);
System.out.println(redisTemplate.opsForValue().get(redisKey)); // 1
System.out.println(redisTemplate.opsForValue().increment(redisKey)); // 2
System.out.println(redisTemplate.opsForValue().decrement(redisKey)); // 1
}
2.opsForHash()-哈希表
使用opsForHash()访问哈希表数据
// redis对哈希表的处理
@Test
public void testHashes() {
String redisKey = "test:user";
redisTemplate.opsForHash().put(redisKey, "id", 1);
redisTemplate.opsForHash().put(redisKey, "username", "zhangSan");
System.out.println(redisTemplate.opsForHash().get(redisKey, "id")); // 1
System.out.println(redisTemplate.opsForHash().get(redisKey, "username"));
}
3.opsForList()-list数据
opsForList()访问list有序集合数据
// 测试redis对list集合的处理
@Test
public void testLists() {
String redisKey = "test:ids";
redisTemplate.opsForList().leftPush(redisKey, 101);
redisTemplate.opsForList().leftPush(redisKey, 102);
redisTemplate.opsForList().leftPush(redisKey, 103);
System.out.println(redisTemplate.opsForList().size(redisKey)); // 3
System.out.println(redisTemplate.opsForList().index(redisKey, 0)); // 103
System.out.println(redisTemplate.opsForList().index(redisKey, 2)); // 101
System.out.println(redisTemplate.opsForList().range(redisKey, 0, 2));//[103,102,101]
System.out.println(redisTemplate.opsForList().leftPop(redisKey)); //103
System.out.println(redisTemplate.opsForList().leftPop(redisKey)); //102
System.out.println(redisTemplate.opsForList().leftPop(redisKey)); //101
}
4.opsForSet()-set数据
opsForSet()访问set集合元素
// 测试对set集合的操作
@Test
public void testSets() {
String redisKey = "test:teachers";
redisTemplate.opsForSet().add(redisKey, "刘备", "关羽", "张飞", "赵云", "诸葛亮");
System.out.println(redisTemplate.opsForSet().size(redisKey)); // 5
System.out.println(redisTemplate.opsForSet().pop(redisKey)); //
System.out.println(redisTemplate.opsForSet().members(redisKey));
}
5 诸葛亮 [张飞, 刘备, 赵云, 关羽]5.opsForZSet()-有序set
opsForZSet()访问有序set数据
// 测试redis对有序集合Set的处理
@Test
public void testSortedSets() {
String redisKey = "test:students";
redisTemplate.opsForZSet().add(redisKey, "唐僧", 80);
redisTemplate.opsForZSet().add(redisKey, "孙悟空", 90);
redisTemplate.opsForZSet().add(redisKey, "猪八戒", 70);
redisTemplate.opsForZSet().add(redisKey, "沙僧", 60);
redisTemplate.opsForZSet().add(redisKey, "白龙马", 50);
System.out.println(redisTemplate.opsForZSet().zCard(redisKey));
System.out.println(redisTemplate.opsForZSet().score(redisKey, "猪八戒"));
System.out.println(redisTemplate.opsForZSet().reverseRank(redisKey, "猪八戒"));
System.out.println(redisTemplate.opsForZSet().reverseRange(redisKey, 0, 2));
}
5 70.0 2 [孙悟空, 唐僧, 猪八戒]6.测试全局数据
// 测试公用方法
@Test
public void testKeys() {
redisTemplate.delete("test:user");
System.out.println(redisTemplate.hasKey("test:user")); // false
redisTemplate.expire("test:students", 10, TimeUnit.SECONDS);
}
7.多次访问一个key,可以进行绑定,简化代码
调用绑定的函数接口,可以将一种数据类型的key绑定,这样,所有操作都是基于这个key的
BoundValueOperationsBoundHashOperations…
// 多次访问一个key,可以进行绑定,简化代码
@Test
public void testBoundOperations() {
String redisKey = "test:count";
BoundValueOperations operations = redisTemplate.boundValueOps(redisKey);
operations.increment();
System.out.println(operations.get());
redisKey = "test:user";
BoundHashOperations hashOperations = redisTemplate.boundHashOps(redisKey);
hashOperations.put("username", "zhang");
System.out.println(hashOperations.get("username"));
}
测试中常见错误总结
spring boot连接linux服务器上的redis
SpringBoot连接不上linux虚拟机启动的redis服务,一般是以下几个坑:
linux设置了防火墙,阻止了外在客户端的访问
解决:最简单直接的方法就是把linux的防火墙关了,(本人学习的虚拟机上可以这样)
service iptables stop
或者也可以试试把6379端口暴露出来。。
firewall-cmd --zone=public --add-port=6379/tcp --permanent
然后重启一下防火墙
systemctl restart firewalld
redis配置文件中,
要将保护模式去掉,否则在没有设置密码的情况下依旧会阻止外在客户端访问redis服务
要注释掉127.0.0.1的限制
虚拟机采用NAT模式,查看下面两个勾是否勾中,子网地址必须是你上面那个ip的同段,比如虚拟机ip地址为192.168.59.128,那么这里的子网地址必须是192.168.59.*。
如果不是,网络中心-找到VMware Virtual Ethernet Adapter for VMnet8右键属性,找到Ipv4属性修改。
连接时,要使用虚拟机的ip,而不是localhost或者127.0.0.1
输入ip查询命名 ip addr
也可以输入 ifconfig查看ip,但此命令会出现3个条目,centos的ip地址是ens33条目中的inet值
Redis的单条命令是保证原子性的,但是redis事务不能保证原子性
- Redis事务没有隔离级别的概念redis启动事务后,不立即执行命令,而是将命令先后放入队列中,提交时,再一并执行
因此,若存在运行期错误,只是当前命令不执行,事务中其他的命令依旧执行,不能保证原子性还要注意,在事务中查询,提交前不会有结果,故,要在提交后再查询
Redis事务操作过程 multi-开启事务 命令入队 exec-执行事务Redis事务本质:一组命令的集合。
----------------- 队列 set set set 执行 -------------------
事务中每条命令都会被序列化,执行过程中按顺序执行,不允许其他命令进行干扰。
一次性顺序性排他性
所以事务中的命令在加入时都没有被执行,直到提交时才会开始执行(Exec)一次性完成。
127.0.0.1:6379> multi # 开启事务 OK 127.0.0.1:6379> set k1 v1 # 命令入队 QUEUED 127.0.0.1:6379> set k2 v2 # .. QUEUED 127.0.0.1:6379> get k1 QUEUED 127.0.0.1:6379> set k3 v3 QUEUED 127.0.0.1:6379> keys * QUEUED 127.0.0.1:6379> exec # 事务执行 1) OK 2) OK 3) "v1" 4) OK 5) 1) "k3" 2) "k2" 3) "k1"discard-取消事务
事务一旦取消,就结束了事务,在事务中执行的命令均不会提交,即均不会执行
127.0.0.1:6379> multi OK 127.0.0.1:6379> set k1 v1 QUEUED 127.0.0.1:6379> set k2 v2 QUEUED 127.0.0.1:6379> DISCARD # 放弃事务 OK 127.0.0.1:6379> EXEC (error) ERR EXEC without MULTI # 当前未开启事务 127.0.0.1:6379> get k1 # 被放弃事务中命令并未执行 (nil)事务中出现错误的处理 代码语法错误(编译时异常)
代码语法错误(编译时异常)所有的命令都不执行,相当于回滚
127.0.0.1:6379> multi OK 127.0.0.1:6379> set k1 v1 QUEUED 127.0.0.1:6379> set k2 v2 QUEUED 127.0.0.1:6379> error k1 # 这是一条语法错误命令 (error) ERR unknown command `error`, with args beginning with: `k1`, # 会报错但是不影响后续命令入队 127.0.0.1:6379> get k2 QUEUED 127.0.0.1:6379> EXEC (error) EXECABORT Transaction discarded because of previous errors. # 执行报错 127.0.0.1:6379> get k1 (nil) # 其他命令并没有被执行代码逻辑错误 (运行时异常)
**其他命令可以正常执行 ** >>> 所以不保证事务原子性
127.0.0.1:6379> multi OK 127.0.0.1:6379> set k1 v1 QUEUED 127.0.0.1:6379> set k2 v2 QUEUED 127.0.0.1:6379> INCR k1 # 这条命令逻辑错误(对字符串进行增量) QUEUED 127.0.0.1:6379> get k2 QUEUED 127.0.0.1:6379> exec 1) OK 2) OK 3) (error) ERR value is not an integer or out of range # 运行时报错 4) "v2" # 其他命令正常执行 # 虽然中间有一条命令报错了,但是后面的指令依旧正常执行成功了。 # 所以说Redis单条指令保证原子性,但是Redis事务不能保证原子性。监控 锁的思想 悲观锁:
很悲观,认为什么时候都会出现问题,无论做什么都会加锁 乐观锁:
很乐观,认为什么时候都不会出现问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过这个数据获取version更新的时候比较version watch key
使用watch key监控指定数据,相当于乐观锁加锁。
正常执行
127.0.0.1:6379> set money 100 # 设置余额:100 OK 127.0.0.1:6379> set use 0 # 支出使用:0 OK 127.0.0.1:6379> watch money # 监视money (上锁) OK 127.0.0.1:6379> multi OK 127.0.0.1:6379> DECRBY money 20 QUEUED 127.0.0.1:6379> INCRBY use 20 QUEUED 127.0.0.1:6379> exec # 监视值没有被中途修改,事务正常执行 1) (integer) 80 2) (integer) 20
测试多线程修改值,使用watch可以当做redis的乐观锁操作(相当于getversion)
我们启动另外一个客户端模拟插队线程。
线程1:
127.0.0.1:6379> watch money # money上锁 OK 127.0.0.1:6379> multi OK 127.0.0.1:6379> DECRBY money 20 QUEUED 127.0.0.1:6379> INCRBY use 20 QUEUED 127.0.0.1:6379> # 此时事务并没有执行
模拟线程插队,线程2:
127.0.0.1:6379> INCRBY money 500 # 修改了线程一中监视的money (integer) 600 12
回到线程1,执行事务:
127.0.0.1:6379> EXEC # 执行之前,另一个线程修改了我们的值,这个时候就会导致事务执行失败 (nil) # 没有结果,说明事务执行失败 127.0.0.1:6379> get money # 线程2 修改生效 "600" 127.0.0.1:6379> get use # 线程1事务执行失败,数值没有被修改 "0"
解锁获取最新值,然后再加锁进行事务。
unwatch进行解锁。
注意:每次提交执行exec后都会自动释放锁,不管是否成功
Spring编程式事务管理redis事务由于redis只对局部一些命令执行事务,因此使用编程式事务比较合适 RedisOperations
RedisOperations会代替redisTemplate去执行redis的访问
// 编程式事务
// 事务统一处理,即先将数据一起打包,一同处理,在中间查询,是查询不到的
@Test
public void testTransaction() {
Object obj = redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
String redisKey = "test:tx";
// 事务启动
redisOperations.multi();
redisOperations.opsForSet().add(redisKey, "zhangSan");
redisOperations.opsForSet().add(redisKey, "liShi");
redisOperations.opsForSet().add(redisKey, "wanWu");
// 尝试在事件中间查询,是查询不到的
System.out.println(redisOperations.opsForSet().members(redisKey));
// 事件开始处理
return redisOperations.exec();
}
});
System.out.println(redisTemplate.opsForSet().members("test:tx"));
System.out.println(obj);
}
[] [wanWu, liShi, zhangSan] [1, 1, 1, [wanWu, liShi, zhangSan]]



