Redis restudy 5 事务 监视Jedis SpringBootRedis
uwupu 啦啦啦啦啦

事务

要么同时成功,要么同时失败:原子性。

  • Redis单条命令保证原子性,但事务不保证原子性

    • Redis没有事务隔离级别的概念
  • Redis事务:一组命令的集合。

  • 一个事务的所有命令都会被序列化,在事务执行过程中,会按照顺序执行

  • 所有命令在事务中,不会被直接执行!只有发起执行命令的时候才会执行!

    • 创建事务,然后发起执行命令,然后开始执行
  • 一次性,顺序性,排他性

执行流程

  • 开启事务 multi
  • 命令入队
  • 执行事务 exec

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 k2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> exec #执行
1) OK
2) OK
3) "v2"
4) OK
5) "v1"

命令

multi:开启事务

exec:执行事务

discard:取消事务

1
2
3
4
5
6
7
8
127.0.0.1:6379> multi  #开启事务
OK
127.0.0.1:6379> set k5 qweasd
QUEUED
127.0.0.1:6379> discard #取消事务
OK
127.0.0.1:6379> get k5 # 事务队列命令不会被执行
(nil)

特性

  • 若事务中出现错误命令,exec后事务中所有命令都不会被执行;
  • 若事务队列中出现“运行时异常”,在事务执行过程中,其他命令可以被正常执行。
    • 错误命令抛出异常

编译时异常

一个命令出现错误,事务中所有命令都不会被执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
127.0.0.1:6379> multi
OK
127.0.0.1:6379>
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> getset k3 #错误的命令
(error) ERR wrong number of arguments for 'getset' command #报错后事务没有停止
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> set k5 v4
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
#所有的命令都不会被执行

运行时异常

所有的命令中,除了报错的,都正常执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr k1 #k1是一个字符串,不能进行incr,所以是一个会报错的命令
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k3
QUEUED
127.0.0.1:6379> exec
1) (error) ERR value is not an integer or out of range
2) OK
3) OK
4) "v3"
127.0.0.1:6379> get k3
"v3"

Redis监视/Redis乐观锁

介绍

watch key:监视某个key,在后面事务创建过程中,若另一个线程修改了key对应的值,那个这个事务执行会失败。

正常执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20

执行失败示例

1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> set money 10
OK
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK #在这里,另一个线程执行了set money 100
127.0.0.1:6379> incrby money 20
QUEUED
127.0.0.1:6379> exec
(nil) #由于money值被另一个线程修改,这里执行失败
127.0.0.1:6379> get money
"100"

其他

解决:重新执行流程。

命令

  • watch key:监视某个元素

  • unwatch:放弃监视

    • exec和discard后会自动执行这个命令。

Jedis

Redis官方推荐的Java连接开发工具。使用Java操作Redis的一个中间件。

Jedis的API与Redis的命令完全一致。

使用

  1. 导入依赖
1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.16</version>
</dependency>
</dependencies>
  1. 执行

    • 连接数据库
    • 操作命令
    • 断开连接
    1
    2
    3
    4
    5
    6
    7
    public class RedisTest {
    public static void main(String[] args) {
    // 1. new Jedis 对象
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    System.out.println(jedis.ping());
    }
    }
    1
    PONG

事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class RedisTest_TX {
public static void main(String[] args) {
Jedis jedis = null;
jedis = new Jedis("127.0.0.1", 6379);
jedis.flushDB();
//开启事务
Transaction multi = jedis.multi();
try {
//输入事务指令
String s = "qweasd";
multi.set("user:1",s);
// int i = 1/0;//代码抛出异常,事务执行失败
//执行事务
List<Object> exec = multi.exec();
System.out.println(exec);
} catch (Exception e) {
System.out.println(multi.discard());
throw new RuntimeException(e);
} finally {
System.out.println(jedis.get("user:1"));
jedis.close();
}
}
}

SpringBoot整合Redis

Jedis和Lettuce历史

在SpringBoot2.x之后,原来使用的Jedis被替换为lettuce。

区别

  • Jedis:采用直连的方式,多线程操作,是不安全的。(BIO)
    • 使用Jedis Pool连接池解决。
  • lettuce:采用netty,实例可以在多个线程中共享,不存在线程不安全的情况,可以减少线程数量。(更像NIO模式)

原理

通过SpringData访问数据

(在Spring访问数据库数据,都是通过SpringData执行。jdbc,redis,JPA等)

SpringBoot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

@Bean
@ConditionalOnMissingBean(name = "redisTemplate") // 当不存在redisTemplate时,则使用这个配置,可以自定义一个redisTemplate来替换
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
//默认的RedisTemplate没有过多的设置,
//两个泛型都是Object,后面使用需要强制转换
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

@Bean
@ConditionalOnMissingBean // 由于String是redis中最常使用的类型,所以有单独一个Bean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}

}

使用示例

  1. 导入依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  2. 配置连接

    1
    2
    spring.redis.url=127.0.0.1
    spring.redis.port=6379
  3. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    @Autowired
    RedisTemplate<Object,Object> redisTemplate;
    @Test
    void contextLoads() {
    redisTemplate.opsForValue().set("name","yn");
    System.out.println(redisTemplate.opsForValue().get("name"));

    }

基本使用

1.先注入一个RedisTemplate

1
2
@Autowired
RedisTemplate<Object,Object> redisTemplate;
  1. 然后选择操作方式

image

  • Value:字符串

  • Cluster:集群

  • Geo:地理位置信息

  • Hash:哈希

  • List:列表

  • HyperLogLog:基数统计

  • Set:集合

  • Stream:流

  1. 对于结果

image

与Redis基本一致,直接使用就可。

其他API

  • redisTemplate
    • ops****
      • set
      • get
    • multi 事务
    • discard
    • delete
    • watch
    • redisTemplate.getConnectionFactory().getConnection() 获取连接对象
      • flushAll 清除所有
      • flushDb 清除数据库

关于中文乱码解决

原理

在RedisTemplate.class下,默认使用的是Jdk序列化方式,Jdk序列化方式会对中文进行编码。

1
2
3
if (this.defaultSerializer == null) {
this.defaultSerializer = new JdkSerializationRedisSerializer(this.classLoader != null ? this.classLoader : this.getClass().getClassLoader());
}

解决

自己定义一个redisTemplate。

自定义RedisTemplate

一般情况下关于对象的保存

JSON

1
2
3
4
5
6
7
8
@Test
public void test() throws JsonProcessingException {
//真实开发一般使用Json传递对象
User user = new User("yn", 22);
String s = new ObjectMapper().writeValueAsString(user);//将对象转换为Json
redisTemplate.opsForValue().set("user",s);
System.out.println(redisTemplate.opsForValue().get("user"));
}

自定义RedisTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Configuration
public class RedisConfig {

//编写自己的redisTemplate

@Bean
// @ConditionalOnMissingBean(name = "redisTemplate")
// @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
//为了开发方便,一般直接使用StringObject类型
RedisTemplate<String, Object> template = new RedisTemplate<>();

//序列化配置
Jackson2JsonRedisSerializer<Object> objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectJackson2JsonRedisSerializer.setObjectMapper(om);

StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

//key使用String序列化方式
template.setKeySerializer(stringRedisSerializer);
//hashKey也使用String序列化方式
template.setHashKeySerializer(stringRedisSerializer);
//value使用jackson
template.setValueSerializer(objectJackson2JsonRedisSerializer);
//hashValue使用Jackson
template.setHashValueSerializer(objectJackson2JsonRedisSerializer);
template.setConnectionFactory(redisConnectionFactory);
template.afterPropertiesSet();
return template;
}
}
 评论