Redis-09Redis的基础事务

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: Redis-09Redis的基础事务

概述


和其他大部分的 NoSQL 不同,Redis 是存在事务的,尽管它没有数据库那么强大,但是它还是很有用的,尤其是在那些需要高并发的网站当中 ,使用 Redis 读/写数据要比数据库快得多,如果使用 Redis 事务在某种场合下去替代数据库事务,则可以在保证数据一致性的同时,大幅度提高数据读/写 的响应速度。


互联网系统很多用户同时访问服务器的可能性很大,尤其在一些商品抢购、抢红包等场合,对性能和数据的一致性有着很高的要求,而存储系统的读/写响应速度对于这类场景的性能的提高是十分重要的 。


在 Redis 中,也存在多个客户端同时向 Redis 系统发送命令的并发可能性,因此同一个数据,可能在不同的时刻被不同的线程所操纵,这样就出现了并发下的数据一致的问题。为了保证异性数据的安全性, Redis 为提供了事务方案。而 Redis 的事务是使用 MULTI-EXEC的命令组合,使用它可以提供两个重要的保证 :


事务是一个被隔离的操作,事务中的方法都会被 Redis 进行序列化并按顺序执行,事务在执行的过程中不会被其他客户端发生的命令所打断。

事务是一个原子性的操作,它要么全部执行,要么就什么都不执行。

在一个 Redis 的连接中,请注意要求是一个连接,所以更多的时候在使用 Spring 中会使用 SessionCallback 接口进行处理,在 Redis 中使用事务会经过 3 个过程


  1. 开启事务
  2. 命令进入队列
  3. 执行事务

Redis 事务命令

官网: https://redis.io/commands#transactions


image.png


Redis 的基础事务


在 Redis 中开启事务是 multi 命令,而执行事务是 exec 命令。 multi 到 exec 命令之间的Redis 命令将采取进入队列的形式,直至 exec 命令的出现,才会一次性发送队列里的命令去执行,而在执行这些命令的时候其他客户端就不能再插入任何命令了,这就是 Redis 的事务机制。

[redis@artisan bin]$ ./redis-cli 
127.0.0.1:6379> auth artisan
OK
127.0.0.1:6379> MULTI 
OK
127.0.0.1:6379> set key1 value1
QUEUED
127.0.0.1:6379> get key1
QUEUED
127.0.0.1:6379> exec
1) OK
2) "value1"
127.0.0.1:6379> 

从上述命令中可以看到,先使用 multi 启动了 Redis 的事务,因此进入了 set 和 get 命令,我们可以发现它并未马上执行,而是返回了 一个飞回归D”的结果。这说明 Redis 将其放入队列中,并不会马上执行,当命令执行到 exec 的时候它就会把队列中的命令发送给Redis 服务器 , 这样存储在队列中的命令就会被执行了,所以才会"OK"和"value1"的输出返回 。


如果回滚事务,则可以使用 discard 命令,它就会进入在事务队列中的命令,这样事务中的方法就不会被执行了,使用 discard 命令取消事务如下所示

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key2 value2
QUEUED
127.0.0.1:6379> GET key2
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> EXEC
(error) ERR EXEC without MULTI
127.0.0.1:6379> 

当使用了 discard 命令后 ,再使用 exec 命令时就会报错,因为 discard 命令已经取消了事务中的命令,而到了 exec 命令时,队列里面己经没有命令可以执行了,所以就出现了报错的情况。


在 Spring 中使用 Redis 事务命令


在 Spring 中要使用同一个连接操作 Redis 命令的场景,这个时候我们借助的是 Spring 提供的 SessionCallback 接口,采用 Spring 去实现上述的命令.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:p="http://www.springframework.org/schema/p"
  xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context 
    http://www.springframework.org/schema/context/spring-context.xsd">
    <context:property-placeholder location="classpath:redis/redis.properties" />
    <!--2,注意新版本2.3以后,JedisPoolConfig的property name,不是maxActive而是maxTotal,而且没有maxWait属性,建议看一下Jedis源码或百度。 -->
    <!-- redis连接池配置 -->
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <!--最大空闲数 -->
        <property name="maxIdle" value="${redis.maxIdle}" />
        <!--连接池的最大数据库连接数 -->
        <property name="maxTotal" value="${redis.maxTotal}" />
        <!--最大建立连接等待时间 -->
        <property name="maxWaitMillis" value="${redis.maxWaitMillis}" />
        <!--逐出连接的最小空闲时间 默认1800000毫秒(30分钟) -->
        <property name="minEvictableIdleTimeMillis" value="${redis.minEvictableIdleTimeMillis}" />
        <!--每次逐出检查时 逐出的最大数目 如果为负数就是 : 1/abs(n), 默认3 -->
        <property name="numTestsPerEvictionRun" value="${redis.numTestsPerEvictionRun}" />
        <!--逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1 -->
        <property name="timeBetweenEvictionRunsMillis" value="${redis.timeBetweenEvictionRunsMillis}" />
      <property name="testOnBorrow" value="true"></property>
    <property name="testOnReturn" value="true"></property>
    <property name="testWhileIdle" value="true"></property>
    </bean>
  <!--redis连接工厂 -->
    <bean id="jedisConnectionFactory"
        class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
        destroy-method="destroy">
        <property name="poolConfig" ref="jedisPoolConfig"></property>
        <!--IP地址 -->
        <property name="hostName" value="${redis.host.ip}"></property>
        <!--端口号 -->
        <property name="port" value="${redis.port}"></property>
        <!--如果Redis设置有密码 -->
        <property name="password" value="${redis.password}" /> 
        <!--客户端超时时间单位是毫秒 -->
        <property name="timeout" value="${redis.timeout}"></property>
        <property name="usePool" value="true" />
        <!--<property name="database" value="0" /> -->
    </bean>
  <!-- 键值序列化器设置为String 类型 -->
  <bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
  <!-- redis template definition -->
  <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"
    p:connection-factory-ref="jedisConnectionFactory"
    p:keySerializer-ref="stringRedisSerializer"
    p:valueSerializer-ref="stringRedisSerializer">
  </bean>
</beans>
package com.artisan.redis.transaction;
import java.util.List;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
public class SpringRedisTransaction {
  @SuppressWarnings({ "unchecked", "rawtypes", "resource" })
  public static void main(String[] args) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:spring/spring-redis-string.xml");
    RedisTemplate<String, String> redisTemplate = ctx.getBean(RedisTemplate.class);
    // 清掉key
    redisTemplate.delete("key1");
    SessionCallback sessionCallback = (SessionCallback) (RedisOperations ops) -> {
      // 开启事务
      ops.multi();
      // 设置值
      ops.boundValueOps("key1").set("artisan");
      // 注意由于命令只是进入队列 ,而没有被执行,所以此处采用 get 命令 ,而 value 却返回为null
      String value = (String) ops.boundValueOps("key1").get();
      System.out.println("事务执行过程中 , 命令入队列,而没有被执行,所以 value 为空 :value=" + value);
      // 此时 list 会保存之前进入队列的所有命令的结果
      List list = ops.exec();
      for (int i = 0; i < list.size(); i++) {
        System.out.println("队列中的命令返回的结果:" + list.get(i).toString());
      }
      // 事务结束后 , 获取 valuel
      value = (String) redisTemplate.opsForValue().get("key1");
      System.out.println("----:" + value);
      return value;
    };
    // 执行Redis命令
    String value = (String) redisTemplate.execute(sessionCallback);
    System.out.println("value:" + value);
  }
}

执行结果

INFO : org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@73a8dfcc: startup date [Thu Sep 27 12:10:45 CST 2018]; root of context hierarchy
INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [spring/spring-redis-string.xml]
事务执行过程中 , 命令入队列,而没有被执行,所以 value 为空 :value=null
队列中的命令返回的结果:true
队列中的命令返回的结果:artisan
----:artisan
value:artisan


采用了 Lambda 表达式( Java 8 以后才引入 Lambda 表达式)来为 SessionCallBack 接口 实现了业务逻辑.


从代码看,使用了 SessionCallBack 接口,从而保证所有的命令都是通过同一个 Redis 的连接进行操作的。


在使用 multi 命令后 , 要特别注意的是,使用 get 等返回值的方法一律返回为空 ,因为在 Redis 中它只是把命令缓存到队列中,而没有去执行 。


使用 exec 后就会执行事务,行行完了事务后,执行 get 命令就能正常返回结果了。


最后使用 redisTemplate.execute(callBack);就能执行我们在 SessionCallBack 接口定义Lambda 表达式的业务逻辑,并将获得其返回值。


需要再强调的是 : 这里打印出来的 value=null,是因为在事务中,所有的方法都只会被

缓存到 Redis 事务队列中,而没有立即执行,所以返回为 null,


如果我们希望得到 Redis 执行事务各个命令的结果,可以用这行代码 :

List list = ops.exec();


这段代码将返回之前在事务队列中所有命令的执行结果,并保存在一个 List 中,只要在 SessionCallback 接口的 execute 方法中将 list 返回,就可以在程序中获得各个命令执行的结果了 .


代码


代码托管到了 https://github.com/yangshangwei/redis_learn

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
6月前
|
NoSQL Redis 数据库
10- 你们用过Redis的事务吗 ? 事务的命令有哪些 ?
```markdown Redis事务包括MULTI、EXEC、DISCARD、WATCH四个命令。虽具备事务功能,但在实际开发中使用较少。 ```
72 7
|
6月前
|
NoSQL Redis 数据库
什么是Redis的事务?
Redis事务提供原子性和顺序性,确保命令按顺序执行且不被打断。核心概念包括原子性、顺序性、隔离性和持久性。关键指令有MULTI、EXEC、DISCARD和WATCH,用于事务的开始、执行、取消和监视。这保障了命令的完整性,防止并发操作导致的数据不一致。
53 2
|
27天前
|
缓存 NoSQL Redis
Redis 事务
10月更文挑战第18天
25 1
|
6月前
|
缓存 NoSQL 数据处理
Redis事务悄然而至:命令的背后故事
Redis事务悄然而至:命令的背后故事
54 0
|
6月前
|
NoSQL Redis
Redis事务:保证数据操作的一致性和可靠性
Redis事务:保证数据操作的一致性和可靠性
117 0
|
3月前
|
负载均衡 NoSQL 算法
一天五道Java面试题----第十天(简述Redis事务实现--------->负载均衡算法、类型)
这篇文章是关于Java面试中Redis相关问题的笔记,包括Redis事务实现、集群方案、主从复制原理、CAP和BASE理论以及负载均衡算法和类型。
一天五道Java面试题----第十天(简述Redis事务实现--------->负载均衡算法、类型)
|
1月前
|
SQL 分布式计算 NoSQL
大数据-42 Redis 功能扩展 发布/订阅模式 事务相关的内容 Redis弱事务
大数据-42 Redis 功能扩展 发布/订阅模式 事务相关的内容 Redis弱事务
25 2
|
1月前
|
NoSQL 关系型数据库 MySQL
Redis 事务特性、原理、具体命令操作全方位诠释 —— 零基础可学习
本文全面阐述了Redis事务的特性、原理、具体命令操作,指出Redis事务具有原子性但不保证一致性、持久性和隔离性,并解释了Redis事务的适用场景和WATCH命令的乐观锁机制。
219 0
Redis 事务特性、原理、具体命令操作全方位诠释 —— 零基础可学习
|
3月前
|
NoSQL 关系型数据库 Redis
Redis6入门到实战------ 九、10. Redis_事务_锁机制_秒杀
这篇文章深入探讨了Redis事务的概念、命令使用、错误处理机制以及乐观锁和悲观锁的应用,并通过WATCH/UNWATCH命令展示了事务中的锁机制。
Redis6入门到实战------ 九、10. Redis_事务_锁机制_秒杀
|
2月前
|
监控 NoSQL 关系型数据库
9)Redis 居然也有事务
9)Redis 居然也有事务
33 0