redis中使用事务保护数据完整性

事务是指一个执行过程,要么全部执行成功,要么失败什么都不改变。不会存在一部分成功一部分失败的情况,也就是事务的ACID四大特性(原子性、一致性、隔离性、持久性)。但是redis中的事务并不是严格意义上的事务,它只是保证了多个命令执行是按照顺序执行,在执行过程中不会插入其他的命令,并不会保证所有命令都成功。也就是说在命令执行过程中,某些命令的失败不会回滚前面已经执行成功的命令,也不会影响后面命令的执行。
redis中的事务跟pipeline很类似,但pipeline是批量提交命令到服务器,服务器执行命令过程中是一条一条执行的,在执行过程中是可以插入其他的命令。而事务是把这些命令批量提交到服务器,服务器执行命令过程也是一条一条执行的,但是在执行这一批命令时是不能插入执行其他的命令,必须等这一批命令执行完成后才能执行其他的命令。

1、事务基本结构

与数据库事务执行过程类似,redis事务是由multi、exec、discard三个关键字组成,对比数据库事务的begin、commit、rollback三个关键字。
命令行示例如下:

127.0.0.1:6379> set key1 value111
OK
127.0.0.1:6379> set key2 value222
OK
127.0.0.1:6379> mget key1 key2
1) "value111"
2) "value222"
127.0.0.1:6379> # 第一个事务
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set key1 value-111
QUEUED
127.0.0.1:6379(TX)> setm key2 value-222
(error) ERR unknown command `setm`, with args beginning with: `key2`, `value-222`, 
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> 
127.0.0.1:6379> mget key1 key2
1) "value111"
2) "value222"
127.0.0.1:6379> # 第二个事务
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set key1 value-111
QUEUED
127.0.0.1:6379(TX)> set key2 value-222
QUEUED
127.0.0.1:6379(TX)> discard
OK
127.0.0.1:6379> 
127.0.0.1:6379> mget key1 key2
1) "value111"
2) "value222"
127.0.0.1:6379> # 第三个事务
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set key1 value-111
QUEUED
127.0.0.1:6379(TX)> set key3 value-333 vddd
QUEUED
127.0.0.1:6379(TX)> set key2 value-222
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR syntax error
3) OK
127.0.0.1:6379> 
127.0.0.1:6379> mget key1 key2 key3
1) "value-111"
2) "value-222"
3) (nil)
127.0.0.1:6379> 

在上面的示例过程中,第一个事务执行时输入了一个错误的命令,在提交事务时整个命令都没有执行;第二个事务提交了多个命令,但是最后回滚了事务,整个事务也不会执行;第三个事务在提交命令时,故意设置一个执行失败的命令,会发现这个失败的命令并不会影响其他命令的成功。

2、事务的执行步骤

redis中的事务是分两步执行的:第一步命令排队,也就是将所有要执行的命令添加到一个队列中,在这一步中命令不会真正执行;第二步命令执行或取消,在这一步中真正处理队列中的命令,如果是exec命令,就执行这些命令;如果是discard命令,就取消执行命令。这里需要注意,如果在排队中某些命令解析出错,即使调用了exec命令,整个队列中的命令也不会执行;但是如果在执行过程中出现了错误,它并不会影响其他命令的正常执行,一般使用封装好的客户端不会出现这种命令错误情况。

3、并发事务

多线程的项目就会有并发问题,并发问题就会存在数据不一致,数据库中解决并发问题是通过锁来实现的,在操作数据前加锁,保证数据在整个执行过程中不被其他程序修改。这种方式加锁是悲观锁,每次更新操作都认为数据会被其他程序修改,导致程序的并发性能不好;还有一种加锁方式是乐观锁,每次直到真正更新数据时才判断数据是否被更新了,如果数据被更新就放弃操作,对于读多写少的场景非常适合,一般实现乐观锁是通过版本号机制。
redis中就支持这种乐观锁机制,它的实现是通过watch命令,在开始执行事务前先通过watch监控被更新的key,如果在事务执行时发现这些key被修改了,那么就不执行事务中的命令。
下面演示的是扣费场景:在进行扣费前,先判断用户的余额,如果余额够扣,就扣减用户的账号余额;如果余额不足,就不能扣减账号余额。

  1. watch某个key后,如果数据没有被其他客户端修改,事务将成功执行:
127.0.0.1:6379> set user:balance:1 100
OK
127.0.0.1:6379> watch user:balance:1
OK
127.0.0.1:6379> get user:balance:1
"100"
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby user:balance:1 -100
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 0
127.0.0.1:6379> get user:balance:1
"0"
127.0.0.1:6379> 
  1. watch某个key后,如果key对应的值被其他程序修改了,执行事务将不成功;如果不用watch命令,事务会成功执行。
    下图展示了在两个客户端验证事务:
    1、首先在下面的客户端设置键的值为100;
    2、然后设置 watch 该值,并且开启事务;
    3、执行减100的命令;
    4、在上面的客户端中修改这个键的值,减3;
    5、下面的客户端执行 exec 命令提交事务。
    这几个步骤执行完成后,发现数据没有修改成功,表示 watch 命令监控到数据变动没有执行事务中的命令。
    watch事务演示
    下面演示步骤与上面一样,只不过在事务前没有 watch 命令,发现数据被修改了。
    没有watch事务演示

  2. watch命令只对当前客户端中的 multi / exec 之间的命令有影响,不在它们之间的命令不受影响,可以正常执行:

127.0.0.1:6379> set user:balance:1 100
OK
127.0.0.1:6379> watch user:balance:1
OK
127.0.0.1:6379> incrby user:balance:1 -3
(integer) 97
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby user:balance:1 -100
QUEUED
127.0.0.1:6379(TX)> set watchkey aaa
QUEUED
127.0.0.1:6379(TX)> exec
(nil)
127.0.0.1:6379> mget user:balance:1 watchkey
1) "97"
2) (nil)
127.0.0.1:6379> 

上面这段代码在watch命令后对键的值进行了修改,发现更新成功;watch的key对应值被修改了,导致事务内的命令不执行,所以后面mget命令没有获取到新的值。

  1. 与watch对应有一个unwatch命令,它表示watch某个key后可以通过unwatch取消监控;如果watch某个key后有 exec 或 discard 命令执行,程序会自动取消监控,不必再使用unwatch取消监控:
127.0.0.1:6379> watch user:balance:1
OK
127.0.0.1:6379> unwatch
OK
127.0.0.1:6379> 
4、代码中使用
  1. 使用jedis实现扣费逻辑

首先还是先使用jedis测试上面提出扣费场景:
引入依赖:

<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>4.3.0</version>
</dependency>

主要代码逻辑如下:

import redis.clients.jedis.*;
import java.time.Duration;
import java.util.List;public class JedisUtil {/*** 连接池*/private JedisPool jedisPool;/*** 连接初始化* @param host* @param port* @param password*/public JedisUtil(String host, int port, String password) {JedisPoolConfig config = new JedisPoolConfig();config.setMaxTotal(256);config.setMaxIdle(256);config.setMinIdle(1);config.setMaxWait(Duration.ofMillis(300));if(password != null && !"".equals(password)) {jedisPool = new JedisPool(config, host, port, 500, password);} else {jedisPool = new JedisPool(config, host, port, 500);}}/*** 关闭连接池*/public void close() {if(jedisPool != null && !jedisPool.isClosed()) {jedisPool.clear();jedisPool.close();}}/*** 获取连接* @return*/public Jedis getJedis() {if(jedisPool != null && !jedisPool.isClosed()) {return jedisPool.getResource();}return null;}/*** 归还jedis对象* @param jedis*/public void returnJedis(Jedis jedis) {if(jedis != null) {jedis.close();}}public static void main(String[] args) {// 获取jedis连接JedisUtil util = new JedisUtil("192.168.56.101", 6379, "");// 键String key = "user:balance:1";util.deduct(key, 100);}/*** 扣减金额*/public void deduct(String key, int money) {Jedis jedis = this.getJedis();// 监控键对应值的变化jedis.watch(key);// 获取账户余额,当余额足够时扣减金额String val = jedis.get(key);if(val != null && Integer.parseInt(val) >= money) {// 开启事务Transaction multi = jedis.multi();try {// 事务中的命令multi.incrBy(key, -money);System.out.println("run in multi!");// 执行事务List<Object> exec = multi.exec();System.out.println("run exec : " + exec);} catch (Exception e) {// 放弃事务multi.discard();e.printStackTrace();} finally {this.returnJedis(jedis);}} else {// 取消监控jedis.unwatch();System.out.println("余额不足...");}}
}

在上面代码中执行事务部分添加断点,并通过其他客户端更新watch对应key的值,发现事务并不执行,这就达到了数据逻辑的一致性,不会因为其他客户端扣减金额后,该客户端继续扣减余额导致剩余金额为负数的情况。

  1. redisTemplate使用事务

在redisTemplate中使用事务,有三种方式,下面的代码是实现上面扣费逻辑的过程:
(1)使用SessionCallback实现:

public void runTransaction(final String key, final String value) {List<Object> exec = redisTemplate.execute(new SessionCallback<List<Object>>() {@Overridepublic <K, V> List<Object> execute(RedisOperations<K, V> operations) throws DataAccessException {List<Object> exec = null;// 监控键对应值的变化operations.watch((K) key);ValueOperations<String, String> op1 = (ValueOperations<String, String>) operations.opsForValue();String val = op1.get(key);int num = Integer.parseInt(value);if(val != null && Integer.parseInt(val) >= num) {try {// 开启事务operations.multi();// 事务中的命令op1.increment(key, -num);// 执行事务exec = operations.exec();} catch (NumberFormatException e) {// 放弃事务operations.discard();e.printStackTrace();}} else {// 取消监控operations.unwatch();System.out.println("余额不足...");}return exec;}});System.out.println(exec);
}

(2)使用RedisCallback实现:

public void runTransaction(final String key, final String value) {List<Object> exec = redisTemplate.execute(new RedisCallback<List<Object>>() {@Overridepublic List<Object> doInRedis(RedisConnection connection) throws DataAccessException {List<Object> exec = null;// 监控键对应值的变化connection.watch(key.getBytes(StandardCharsets.UTF_8));byte[] val = connection.get(key.getBytes(StandardCharsets.UTF_8));int num = Integer.parseInt(value);if(val != null && Integer.parseInt(new String(val)) >= num) {try {// 开启事务connection.multi();// 事务中的命令connection.incrBy(key.getBytes(StandardCharsets.UTF_8), -num);// 执行事务exec = connection.exec();} catch (NumberFormatException e) {// 放弃事务connection.discard();e.printStackTrace();}} else {// 取消监控connection.unwatch();System.out.println("余额不足...");}return exec;}});System.out.println(exec);
}

(3)使用@Transactional注解实现:

@Transactional
public void runTransaction(final String key, final String value) {// 监控键对应值的变化redisTemplate.watch(key);String val = redisTemplate.opsForValue().get(key);int num = Integer.parseInt(value);if(val != null && Integer.parseInt(val) >= num) {// 开启事务支持// 开启这个值后,所有的命令都会在exec执行完才返回结果,所以需要返回值的命令要在这个方法前执行redisTemplate.setEnableTransactionSupport(true);try {// 开启事务redisTemplate.multi();// 事务中的命令redisTemplate.opsForValue().increment(key, -num);// 执行事务List<Object> exec = redisTemplate.exec();System.out.println(exec);} catch (Exception e) {// 放弃事务redisTemplate.discard();e.printStackTrace();} finally {// 关闭事务支持redisTemplate.setEnableTransactionSupport(false);}} else {// 取消监控redisTemplate.unwatch();System.out.println("余额不足...");}
}

事务只能保证每一条命令的原子性,并不能保证事务内所有命令的原子性,上面的示例代码已经验证了这个结论,其实redis中已经提供了一些多值指令,如:mset、mget、hmset、hmget。但是这些只能是一种数据类型的多键值对操作,这些命令是原子操作。
上面这种扣费逻辑,除了使用redis的事务支持,还可以使用lua脚本实现,lua脚本在服务端执行与事务中的命令类似,是不可分割的整体,下面演示lua脚本内容,可以实现上面一样的处理结果:
lua脚本如下:

local b = redis.call('get', KEYS[1]);
if tonumber(b) >= tonumber(ARGV[1]) thenlocal rs = redis.call('incrby', KEYS[1], 0 - tonumber(ARGV[1]));return rs;
else return nil;
end;

测试过程:

127.0.0.1:6379> set user:balance:1 100
OK
127.0.0.1:6379> get user:balance:1
"100"
127.0.0.1:6379> eval "local b = redis.call('get', KEYS[1]); if tonumber(b) >= tonumber(ARGV[1]) then local rs = redis.call('incrby', KEYS[1], 0 - tonumber(ARGV[1])); return rs; else return nil; end;" 1 user:balance:1 100
(integer) 0
127.0.0.1:6379> get user:balance:1
"0"
127.0.0.1:6379> set user:balance:1 100
OK
127.0.0.1:6379> incrby user:balance:1 -3
(integer) 97
127.0.0.1:6379> eval "local b = redis.call('get', KEYS[1]); if tonumber(b) >= tonumber(ARGV[1]) then local rs = redis.call('incrby', KEYS[1], 0 - tonumber(ARGV[1])); return rs; else return nil; end;" 1 user:balance:1 100
(nil)
127.0.0.1:6379> get user:balance:1
"97"
127.0.0.1:6379> 

第一次执行余额正常够扣场景,第二次设置余额不足,会发现扣减逻辑并没有执行。
以上内容就是redis中事务的全部内容,要记住几点:
(1)redis中的事务跟我们平时用的数据库中的事务有一些差异的,它能保证多条命令执行时中间不会插入其他的命令,但并不会保证所有命令都执行成功,单条redis命令能保证原子性,但事务中的多条命令并不是原子性。
(2)redis中事务分两步完成:第一步将所有命令添加到命令队列中,这一步并不会执行命令;第二步执行队列中的命令。如果第一步中的命令有错误,第二步并不会执行;如果第二步已经开始执行了,那么部分失败的命令并不会影响其他正确命令的结果,这样就会导致一部分命令成功而另外一部分命令失败的场景。
(3)事务中不宜执行过多的命令或非常耗时的命令,因为redis底层执行命令是单线程,如果单个事务中执行过多的命令会导致其他客户端的请求被阻塞。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/210768.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用flutter_native_splash替换启动图片,iOS端替换不成功

使用flutter_native_splash替换启动图片&#xff0c;iOS端替换不成功 1、删除App重启手机&#xff1b;2、重新创建一个新的LaunchScreen.storyboard&#xff0c;比如命名为NewLaunchScreen.storyboard&#xff0c;在General里面设置Launch Screen File为这个新的NewLaunchScree…

蓝桥杯 day01 奇怪的数列

题目描述 奇怪的数列 从 X 星截获一份电码&#xff0c;是一些数字&#xff0c;如下&#xff1a; 13 1113 3113 132113 1113122113 ⋯⋯ YY 博士经彻夜研究&#xff0c;发现了规律&#xff1a; 第一行的数字随便是什么&#xff0c;以后每一行都是对上一行"读出来…

智能优化算法应用:基于蝗虫算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于蝗虫算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于蝗虫算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.蝗虫算法4.实验参数设定5.算法结果6.参考文献7.MA…

【数据挖掘】国科大苏桂平老师数据库新技术课程作业 —— 第二次作业

1 设 F { A B → C , B → D , C D → E , C E → G H , G → A } F\{AB\rightarrow C,B\rightarrow D, CD\rightarrow E, CE\rightarrow GH, G\rightarrow A \} F{AB→C,B→D,CD→E,CE→GH,G→A}&#xff0c;用推理的方法证明 F ∣ A B → G F\;|AB\rightarrow G F∣AB→…

持续集成交付CICD:使用Maven命令上传Nexus制品

目录 一、实验 1.使用Maven命令上传Nexus制品&#xff08;第一种方式&#xff09; 2.使用Maven命令上传Nexus制品&#xff08;第二种方式&#xff09; 一、实验 1.使用Maven命令上传Nexus制品&#xff08;第一种方式&#xff09; &#xff08;1&#xff09;指定一个 hoste…

说说React jsx转换成真实DOM的过程?

在React中&#xff0c;JSX&#xff08;JavaScript XML&#xff09;是一种语法糖&#xff0c;用于描述用户界面的结构和组件关系。当你编写React组件并包含JS JSX解析&#xff1a;React中的JSX代码首先会被解析成JavaScript对象。这个过程通常是通过Babel等工具进行的&#xff0…

Flutter视频播放器在iOS端和Android端都能实现全屏播放

Flutter开发过程中&#xff0c;对于视频播放的三方组件有很多&#xff0c;在Android端适配都挺好&#xff0c;但是在适配iPhone手机的时候&#xff0c;如果设置了UIInterfaceOrientationLandscapeLeft和UIInterfaceOrientationLandscapeRight都为false的情况下&#xff0c;无法…

pytorch 笔记:dist 和 cdist

1 dist 1.1 基本使用方法 torch.dist(input, other, p2) 计算两个Tensor之间的p-范数 1.2 主要参数 input输入张量other另一个输入张量p范数 input 和 other的形状需要是可广播的 1.3 举例 import torchxtorch.randn(4) x #tensor([ 1.2698, -0.1209, 0.0462, -1.3271…

基于PaddleOCR银行卡识别实现(四)之uni-app离线插件

目的 在前三篇文章中完成了银行卡识别整个模型训练等工作&#xff0c;通过了解PaddleOCR的端侧部署&#xff0c;我们也可以将银行卡号检测模型和识别模型移植到手机中&#xff0c;做成一款uni-app手机端离线银行卡号识别的应用。 准备工作 为了不占用过多篇幅&#xff0c;这…

Nginx的性能优化、安全以及防盗链配置

目录 一、nginx的日志分割 二、nginx性能优化之启用epoll模型 三、nginx性能优化之设置worker进程数并与cpu进行绑核 四、nginx性能优化之调整worker的最大打开文件数和最大处理连接请求数量 五、nginx性能优化之启用gzip压缩&#xff0c;提高传输&#xff0c;减少带宽 六…

字节iconpark基于vue使用

1.安装 npm i icon-park/vue 2.导入 说明&#xff1a;导入并在main.js使用。 import { install } from icon-park/vue/es/all; import icon-park/vue/styles/index.css; Vue.use(install) 3.打开官网 ByteDance IconPark 4.复制 说明&#xff1a;点击官方图标库&#xff0c…

Java-JDBC操作MySQL

Java-JDBC操作MySQL 文章目录 Java-JDBC操作MySQL一、Java-JDBC-MySQL的关系二、创建连接三、登录MySQL四、操作数据库1、返回型操作2、无返回型操作 练习题目及完整代码 一、Java-JDBC-MySQL的关系 #mermaid-svg-B7qjXrosQaCOwRos {font-family:"trebuchet ms",verd…

国产Type-C PD芯片—接口快充取电芯片

常用USB PDTYPE-C受电端&#xff0c;即设备端协议IC芯片&#xff08;PD Sink&#xff0c;也叫PD诱骗芯片&#xff09;&#xff0c;诱导取电芯片。 产品介绍 LDR6328: ◇ 采用 SOP-8 封装 ◇ 兼容 USB PD 3.0 规范&#xff0c;支持 USB PD 2.0 ◇ 兼容 QC 3.0 规范&#x…

TailwindCSS 支持文本文字超长溢出截断、文字文本省略号

前言 文本文字超长截断并自动补充省略号&#xff0c;这是前端日常开发工作中常用的样式设置能力&#xff0c;文字超长截断主要分为单行超长截断和多行超长截断。本文通过介绍基本CSS样式、tailwindcss 类设置两种基础方式来实现文字超长截断。 TailwindCSS 设置 单行文字超长…

WPF仿网易云搭建笔记(2):组件化开发

文章目录 前言专栏和Gitee仓库依赖属性实战&#xff1a;缩小&#xff0c;全屏&#xff0c;关闭按钮依赖属性操作封装主窗口传递this本身给TitleView标题控件主要代码MainWindow.xmalMainWindow.cs依赖属性方法封装TitleView.csTitleViewModelTitleViewModel实现效果 前言 这次…

基于以太坊的智能合约开发Solidity(函数继承篇)

参考教程&#xff1a;【实战篇】1、函数重载_哔哩哔哩_bilibili 1、函数重载&#xff1a; pragma solidity ^0.5.17;contract overLoadTest {//不带参数function test() public{}//带一个参数function test(address account) public{}//参数类型不同&#xff0c;虽然uint160可…

发送、接收消息,界面不及时刷新

发送、接收消息后 UI 没展示&#xff0c;不及时刷新&#xff0c;大概率 是 SDK 的 UI 刷新功能被干扰&#xff0c;参考下面排查&#xff1a; 检查 initWithAppkey 和 connectWithToken 使用的是否是 IMKit 核心类 RCIM 的方法&#xff0c;如果不是&#xff0c;请换成 RCIM 的。…

【刷题】位运算

2 n 2^n 2n 1<<n判断某一位是否为1 s&1<<k将上面两个组合&#xff0c;可以得到判断一个集合中哪些内容包含&#xff0c;遍历所有情况。 100140. 关闭分部的可行集合数目 一个公司在全国有 n 个分部&#xff0c;它们之间有的有道路连接。一开始&#xff0c;…

CentOS 7 离线安装达梦数据库8.0

前期准备工作 确认操作系统的版本和数据库的版本是否一致 ## 查看系统版本&#xff1a;cat /etc/redhat-release CentOS Linux release 7.5.1804 (Core)关闭防火墙和Selinux # 查看selinux是不是disabled / enforce cat /etc/selinux/config## 查看防火墙状态 firewall-cmd …

数据结构之归并排序及排序总结

目录 归并排序 归并排序的时间复杂度 排序的稳定性 排序总结 归并排序 归并排序大家只需要掌握其递归方法即可&#xff0c;非递归方法由于在某些特殊场景下边界难控制&#xff0c;我们一般很少使用非递归实现归并排序。那么归并排序的递归方法我们究竟是怎样实现呢&#xff…