项目1在线交流平台-4. 使用radis高性能储存方案-2.SpringBoot整合redis-redis事务管理

参考牛客网高级项目教程

狂神说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 模式

    在这里插入图片描述

SpringDataRedis简介

  • 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

img

  • 只有两个简单的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测试
  1. 编写配置文件

    一定是虚拟机的ip,不是localhost或127.0.0.1

    #redis相关配置
    spring.redis.database=11
    spring.redis.host=192.168.***.***
    spring.redis.port=6379
    
  2. 使用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"));
        }
    }
    
  3. 测试结果

    此时我们回到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,不但效率高并且易用。

  • 工具类参考博客:

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的
    • 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"));
}

测试中常见错误总结

spring boot连接linux服务器上的redis

SpringBoot连接不上linux虚拟机启动的redis服务,一般是以下几个坑:

  1. linux设置了防火墙,阻止了外在客户端的访问

    • 解决:最简单直接的方法就是把linux的防火墙关了,(本人学习的虚拟机上可以这样)

      service iptables stop
      

      或者也可以试试把6379端口暴露出来。。

      firewall-cmd --zone=public --add-port=6379/tcp --permanent
      

      然后重启一下防火墙

      systemctl restart firewalld
      
  2. redis配置文件中,

    • 要将保护模式去掉,否则在没有设置密码的情况下依旧会阻止外在客户端访问redis服务

    img

    • 要注释掉127.0.0.1的限制

      img

  3. 虚拟机采用NAT模式,查看下面两个勾是否勾中,子网地址必须是你上面那个ip的同段,比如虚拟机ip地址为192.168.59.128,那么这里的子网地址必须是192.168.59.*。

    img

    如果不是,网络中心-找到VMware Virtual Ethernet Adapter for VMnet8右键属性,找到Ipv4属性修改。

    img

  4. 连接时,要使用虚拟机的ip,而不是localhost或者127.0.0.1

    • 输入ip查询命名 ip addr

      也可以输入 ifconfig查看ip,但此命令会出现3个条目,centos的ip地址是ens33条目中的inet值

      img

3.事务处理

Redis的单条命令是保证原子性的,但是redis事务不能保证原子性

  1. Redis事务没有隔离级别的概念
  2. 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]]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值