文章目录
参考牛客网高级项目教程
狂神说Redis教程笔记
1.SpringBoot整合redis基本配置
1. 使用java操作redis基础
Jedis
-
**使用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 模式
- Spring-data-redis是spring大家族的一部分,提供了在srping应用中通过简单的配置访问redis服务,
- 对reids底层开发包(Jedis, JRedis, and RJC)进行了高度封装,
- RedisTemplate提供了redis各种操作、异常处理及序列化,支持发布订阅,并对spring 3.1 cache进行了实现。
2.导入依赖
- 版本号父类依赖中有测试好兼容性比较好的版本,不写,默认使用父类的中指明的版本
<!-- 整合redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3. 源码分析和配置连接
源码分析
RedisAutoConfiguration
-
我们在学习SpringBoot自动配置的原理时,整合一个组件并进行配置一定会有一个自动配置类
- xxxAutoConfiguration,并且在spring.factories中也一定能找到这个类的完全限定名。Redis也不例外。
-
那么就一定还存在一个RedisProperties类
-
之前我们说SpringBoot2.x后默认使用Lettuce来替换Jedis,现在我们就能来验证了。
-
先看Jedis:
-
@ConditionalOnClass注解中有两个类是默认不存在的,所以Jedis是无法生效的
-
然后再看Lettuce:
- 完美生效。
现在我们回到RedisAutoConfiguratio
-
只有两个简单的Bean
- RedisTemplate
- StringRedisTemplate
-
当看到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和自定义序列化方式
4. 自定义RedisTemplate
源码分析-为何需要自定义RedisTemplate
@ConditionalOnMissingBean
- 使用**@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<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
// 将template 泛型设置为 <String, Object>
RedisTemplate<String, Object> template = new RedisTemplate();
// 连接工厂,不必修改
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
序列化的设置与启用
RedisSerializer
- 需要改用JSON或者String类型的序列化
- RedisSerializer提供了多种序列化方案:
- 对于key,使用框架中自带的json序列化
- 对于value,使用框架中自带的String序列化
setKeySerializer
RedisTemplate调用set序列化的方法
-
将RedisSerializer的静态方法获取的序列化传给这个方法
afterPropertiesSet
-
最后调用afterPropertiesSet方法启动设置
- 设置好的序列化器,就不会使用默认的jdk序列化器
-
afterPropertiesSet方法,初始化bean的时候执行,可以针对某个具体的bean进行配置。
- afterPropertiesSet 必须实现 InitializingBean接口。
- 实现 InitializingBean接口必须实现afterPropertiesSet方法。
@Configuration
public class RedisConfig {
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<String, Object> 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,不但效率高并且易用。 -
工具类参考博客:
2. 使用自定义的RedisTemplate操作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的
- BoundValueOperations
- BoundHashOperations…
// 多次访问一个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"));
}
测试中常见错误总结
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值
-
3.事务处理
Redis的单条命令是保证原子性的,但是redis事务不能保证原子性
- Redis事务没有隔离级别的概念
- redis启动事务后,不立即执行命令,而是将命令先后放入队列中,提交时,再一并执行
- 因此,若存在运行期错误,只是当前命令不执行,事务中其他的命令依旧执行,不能保证原子性
- 还要注意,在事务中查询,提交前不会有结果,故,要在提交后再查询
Redis事务本质:一组命令的集合。
----------------- 队列 set set set 执行 -------------------
事务中每条命令都会被序列化,执行过程中按顺序执行,不允许其他命令进行干扰。
- 一次性
- 顺序性
- 排他性
Redis事务操作过程
multi-开启事务
命令入队
exec-执行事务
所以事务中的命令在加入时都没有被执行,直到提交时才会开始执行(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]]