栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

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

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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

文章目录

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==

参考牛客网高级项目教程

狂神说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.导入依赖

版本号父类依赖中有测试好兼容性比较好的版本,不写,默认使用父类的中指明的版本

   
      org.springframework.boot
      spring-boot-starter-data-redis
   
3. 源码分析和配置连接 源码分析 RedisAutoConfiguration

我们在学习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和自定义序列化方式

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 redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
       // 将template 泛型设置为 
        RedisTemplate 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 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值

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]]
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/774582.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号