乐趣区

关于redis:一文讲透-Redis-事务-事务模式-VS-Lua-脚本

精确的讲,Redis 事务蕴含两种模式 : 事务模式Lua 脚本

先说论断:

Redis 的事务模式具备如下特点:

  • 保障隔离性;
  • 无奈保障持久性;
  • 具备了肯定的原子性,但不反对回滚;
  • 一致性的概念有一致,假如在一致性的外围是束缚的语意下,Redis 的事务能够保障一致性。

但 Lua 脚本更具备实用场景,它是另一种模式的事务,他具备肯定的原子性,但脚本报错的状况下,事务并不会回滚。Lua 脚本能够保障隔离性,而且能够完满的反对 前面的步骤依赖后面步骤的后果

Lua 脚本模式的身影简直无处不在,比方分布式锁、提早队列、抢红包等场景。

1 事务原理

Redis 的事务蕴含如下命令:

序号 命令及形容
1 MULTI 标记一个事务块的开始。
2 EXEC 执行所有事务块内的命令。
3 DISCARD 勾销事务,放弃执行事务块内的所有命令。
4 WATCH key [key …] 监督一个(或多个) key,如果在事务执行之前这个(或这些) key 被其余命令所改变,那么事务将被打断。
5 UNWATCH 勾销 WATCH 命令对所有 key 的监督。

事务蕴含三个阶段:

  1. 事务开启,应用 MULTI , 该命令标记着执行该命令的客户端从非事务状态切换至事务状态;
  2. 命令入队,MULTI 开启事务之后,客户端的命令并不会被立刻执行,而是放入一个事务队列;
  3. 执行事务或者抛弃。如果收到 EXEC 的命令,事务队列里的命令将会被执行,如果是 DISCARD 则事务被抛弃。

上面展现一个事务的例子。

 redis> MULTI 
 OK
 redis> SET msg "hello world"
 QUEUED
 redis> GET msg
 QUEUED
 redis> EXEC
 1) OK
 1) hello world

这里有一个疑难?在开启事务的时候,Redis key 能够被批改吗?

在事务执行 EXEC 命令之前,Redis key 仍然能够被批改。在事务开启之前,咱们能够 watch 命令监听 Redis key。在事务执行之前,咱们批改 key 值,事务执行失败,返回 nil。

通过下面的例子,watch 命令能够实现相似乐观锁的成果。

2 事务的 ACID

2.1 原子性

原子性是指:一个事务中的所有操作,或者全副实现,或者全副不实现,不会完结在两头某个环节。事务在执行过程中产生谬误,会被回滚到事务开始前的状态,就像这个事务素来没有执行过一样。

第一个例子:

在执行 EXEC 命令前,客户端发送的操作命令谬误,比方:语法错误或者应用了不存在的命令。

 redis> MULTI
 OK
 redis> SET msg "other msg"
 QUEUED
 redis> wrongcommand  ### 成心写谬误的命令
 (error) ERR unknown command 'wrongcommand' 
 redis> EXEC
 (error) EXECABORT Transaction discarded because of previous errors.
 redis> GET msg
 "hello world"

在这个例子中,咱们应用了不存在的命令,导致入队失败,整个事务都将无奈执行。

第二个例子:

事务操作入队时,命令和操作的数据类型不匹配,入队列失常,但执行 EXEC 命令异样。

 redis> MULTI  
 OK
 redis> SET msg "other msg"
 QUEUED
 redis> SET mystring "I am a string"
 QUEUED
 redis> HMSET mystring name  "test"
 QUEUED
 redis> SET msg "after"
 QUEUED
 redis> EXEC
 1) OK
 2) OK
 3) (error) WRONGTYPE Operation against a key holding the wrong kind of value
 4) OK
 redis> GET msg
 "after"

这个例子里,Redis 在执行 EXEC 命令时,如果呈现了谬误,Redis 不会终止其它命令的执行,事务也不会因为某个命令执行失败而回滚。

综上,我对 Redis 事务原子性的了解如下:

  1. 命令入队时报错,会放弃事务执行,保障原子性;
  2. 命令入队时失常,执行 EXEC 命令后报错,不保障原子性;

也就是:Redis 事务在特定条件下,才具备肯定的原子性

2.2 隔离性

数据库的隔离性是指:数据库容许多个并发事务同时对其数据进行读写和批改的能力,隔离性能够避免多个事务并发执行时因为穿插执行而导致数据的不统一。

事务隔离分为不同级别,别离是:

  • 未提交读(read uncommitted)
  • 提交读(read committed)
  • 可反复读(repeatable read)
  • 串行化(serializable)

首先,须要明确一点:Redis 并没有事务隔离级别的概念。这里咱们探讨 Redis 的隔离性是指:并发场景下,事务之间是否能够做到互不烦扰

咱们能够将事务执行能够分为 EXEC 命令执行前 EXEC 命令执行后 两个阶段,离开探讨。

  1. EXEC 命令执行前

在事务原理这一大节,咱们发现在事务执行之前,Redis key 仍然能够被批改。此时,能够应用 WATCH 机制 来实现乐观锁的成果。

  1. EXEC 命令执行后

因为 Redis 是单线程执行操作命令,EXEC 命令执行后,Redis 会保障命令队列中的所有命令执行完。这样就能够保障事务的隔离性。

2.3 持久性

数据库的持久性是指:事务处理完结后,对数据的批改就是永恒的,即使系统故障也不会失落。

Redis 的数据是否长久化取决于 Redis 的长久化配置模式。

  1. 没有配置 RDB 或者 AOF,事务的持久性无奈保障;
  2. 应用了 RDB 模式,在一个事务执行后,下一次的 RDB 快照还未执行前,如果产生了实例宕机,事务的持久性同样无奈保障;
  3. 应用了 AOF 模式;AOF 模式的三种配置选项 no、everysec 都会存在数据失落的状况。always 能够保障事务的持久性,但因为性能太差,在生产环境个别不举荐应用。

综上,redis 事务的持久性是无奈保障的

2.4 一致性

一致性的概念始终很让人困惑,在我搜查的材料里,有两类不同的定义。

  1. 维基百科

咱们先看下维基百科上一致性的定义:

Consistency ensures that a transaction can only bring the database from one valid state to another, maintaining database invariants: any data written to the database must be valid according to all defined rules, including constraints, cascades, triggers, and any combination thereof. This prevents database corruption by an illegal transaction, but does not guarantee that a transaction is correct. Referential integrity guarantees the primary key – foreign key relationship.

在这段文字里,一致性的外围是“束缚”,“any data written to the database must be valid according to all defined rules”。

如何了解束缚?这里援用知乎问题 如何了解数据库的外部一致性和内部一致性,蚂蚁金服 OceanBase 研发专家韩富晟答复的一段话:

“束缚”由数据库的使用者通知数据库,使用者要求数据肯定合乎这样或者那样的束缚。当数据产生批改时,数据库会检查数据是否还合乎约束条件,如果约束条件不再被满足,那么批改操作不会产生。

关系数据库最常见的两类束缚是“唯一性束缚”和“完整性束缚”,表格中定义的主键和惟一键都保障了指定的数据项绝不会呈现反复,表格之间定义的参照完整性也保障了同一个属性在不同表格中的一致性。

“Consistency in ACID”是如此的好用,以至于曾经消融在大部分使用者的血液里了,使用者会在表格设计的时候盲目的加上须要的约束条件,数据库也会严格的执行这个约束条件。

所以 事务的一致性和事后定义的束缚无关,保障了束缚即保障了一致性

咱们细细品一品这句话:This prevents database corruption by an illegal transaction, but does not guarantee that a transaction is correct

写到这里可能大家还是有点含糊,咱们举经典 转账 的案例。

咱们开启一个事务,张三和李四账号上的初始余额都是 1000 元,并且余额字段没有任何束缚。张三给李四转账 1200 元。张三的余额更新为 -200,李四的余额更新为 2200。

从利用层面来看,这个事务显著不非法,因为事实场景中,用户余额不可能小于 0,然而它齐全遵循数据库的束缚,所以从数据库层面来看,这个事务仍然保障了一致性。

Redis 的事务一致性是指:Redis 事务在执行过程中合乎数据库的束缚,没有蕴含非法或者有效的谬误数据。

咱们分三种异样场景别离探讨:

  1. 执行 EXEC 命令前,客户端发送的操作命令谬误,事务终止,数据放弃一致性;
  2. 执行 EXEC 命令后,命令和操作的数据类型不匹配,谬误的命令会报错,但事务不会因为谬误的命令而终止,而是会继续执行。正确的命令失常执行,谬误的命令报错,从这个角度来看,数据也能够放弃一致性;
  3. 执行事务的过程中,Redis 服务宕机。这里须要思考服务配置的长久化模式。

    • 无长久化的内存模式:服务重启之后,数据库没有保持数据,因而数据都是放弃一致性的;
    • RDB / AOF 模式:服务重启后,Redis 通过 RDB / AOF 文件复原数据,数据库会还原到统一的状态。

综上所述,在一致性的外围是束缚的语意下,Redis 的事务能够保障一致性

  1. 《设计数据密集型利用》这本书是分布式系统入门的神书。在事务这一章节有一段对于 ACID 的解释:

Atomicity, isolation, and durability are properties of the database,whereas consistency (in the ACID sense) is a property of the application. The application may rely on the database’s atomicity and isolation properties in order to achieve consistency, but it’s not up to the database alone. Thus, the letter C doesn’t really belong in ACID.

原子性,隔离性和持久性是数据库的属性,而一致性(在 ACID 意义上)是应用程序的属性。利用可能依赖数据库的原子性和隔离属性来实现一致性,但这并不仅取决于数据库。因而,字母 C 不属于 ACID。

很多时候,咱们始终在纠结的一致性,其实就是指 合乎事实世界的一致性,事实世界的一致性才是事务谋求的最终目标。

为了实现事实世界的一致性,须要满足如下几点:

  1. 保障原子性,持久性和隔离性,如果这些特色都无奈保障,那么事务的一致性也无奈保障;
  2. 数据库自身的束缚,比方字符串长度不能超过列的限度或者唯一性束缚;
  3. 业务层面同样须要进行保障。

2.5 事务特点

咱们通常称 Redis 为内存数据库 , 不同于传统的关系数据库,为了提供了更高的性能,更快的写入速度,在设计和实现层面做了一些均衡,并不能齐全反对事务的 ACID。

Redis 的事务具备如下特点:

  • 保障隔离性;
  • 无奈保障持久性;
  • 具备了肯定的原子性,但不反对回滚;
  • 一致性的概念有一致,假如在一致性的外围是束缚的语意下,Redis 的事务能够保障一致性。

从工程角度来看,假如事务操作中每个步骤须要依赖上一个步骤返回的后果,则须要通过 watch 来实现乐观锁。

3 Lua 脚本

3.1 简介

Lua 由规范 C 编写而成,代码简洁柔美,简直在所有操作系统战争台上都能够编译,运行。
Lua 脚本能够很容易的被 C/C ++ 代码调用,也能够反过来调用 C/C++ 的函数,这使得 Lua 在应用程序中能够被广泛应用。Lua 脚本在游戏畛域大放异彩,大家耳熟能详的《大话西游 II》,《魔兽世界》都大量应用 Lua 脚本。
Java 后端工程师接触过的 api 网关,比方 Openresty,Kong 都能够看到 Lua 脚本的身影。
从 Redis 2.6.0 版本开始,Redis 内置的 Lua 解释器,能够实现在 Redis 中运行 Lua 脚本。应用 Lua 脚本的益处:缩小网络开销。将多个申请通过脚本的模式一次发送,缩小网络时延。原子操作。
Redis 会将整个脚本作为一个整体执行,两头不会被其余命令插入。复用。客户端发送的脚本会永恒存在 Redis 中,其余客户端能够复用这一脚本而不须要应用代码实现雷同的逻辑。

Redis Lua 脚本常用命令:

序号 命令及形容
1 EVAL script numkeys key [key …] arg [arg …] 执行 Lua 脚本。
2 EVALSHA sha1 numkeys key [key …] arg [arg …] 执行 Lua 脚本。
3 SCRIPT EXISTS script [script …] 查看指定的脚本是否曾经被保留在缓存当中。
4 SCRIPT FLUSH 从脚本缓存中移除所有脚本。
5 SCRIPT KILL 杀死以后正在运行的 Lua 脚本。
6 SCRIPT LOAD script 将脚本 script 增加到脚本缓存中,但并不立刻执行这个脚本。

3.2 EVAL 命令

命令格局:

 EVAL script numkeys key [key ...] arg [arg ...]

阐明:

  • script是第一个参数,为 Lua 5.1 脚本;
  • 第二个参数 numkeys 指定后续参数有几个 key;
  • key [key ...],是要操作的键,能够指定多个,在 Lua 脚本中通过 KEYS[1], KEYS[2] 获取;
  • arg [arg ...],参数,在 Lua 脚本中通过 ARGV[1], ARGV[2] 获取。

简略实例:

 redis> eval "return ARGV[1]" 0 100 
 "100"
 redis> eval "return {ARGV[1],ARGV[2]}" 0 100 101
 1) "100"
 2) "101"
 redis> eval "return {KEYS[1],KEYS[2],ARGV[1]}" 2 key1 key2 first second
 1) "key1"
 2) "key2"
 3) "first"
 4) "second"

上面演示下 Lua 如何调用 Redis 命令,通过 redis.call() 来执行了 Redis 命令。

 redis> set mystring 'hello world'
 OK
 redis> get mystring
 "hello world"
 redis> EVAL "return redis.call('GET',KEYS[1])" 1 mystring
 "hello world"
 redis> EVAL "return redis.call('GET','mystring')" 0
 "hello world"

3.3 EVALSHA

命令应用 EVAL 命令每次申请都须要传输 Lua 脚本,若 Lua 脚本过长,不仅会耗费网络带宽,而且也会对 Redis 的性能造成肯定的影响。思路是先将 Lua 脚本先缓存起来 , 返回给客户端 Lua 脚本的 sha1 摘要。客户端存储脚本的 sha1 摘要,每次申请执行 EVALSHA 命令即可。

EVALSHA 命令根本语法如下:

 redis> EVALSHA sha1 numkeys key [key ...] arg [arg ...] 

实例如下:

 redis> SCRIPT LOAD "return'hello world'""5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
 redis> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0
 "hello world"

4 事务 VS Lua 脚本

从定义上来说,Redis 中的脚本自身就是一种事务 ,所以任何在事务里能够实现的事,在脚本外面也能实现。并且一般来说,应用 脚本要来得更简略,并且速度更快

因为脚本性能是 Redis 2.6 才引入的,而事务性能则更早之前就存在了,所以 Redis 才会同时存在两种处理事务的办法。

不过 咱们并不打算在短时间内就移除事务性能,因为事务提供了一种即便不应用脚本,也能够防止竞争条件的办法,而且事务自身的实现并不简单。

— https://redis.io/

Lua 脚本是另一种模式的事务,他具备肯定的原子性,但脚本报错的状况下,事务并不会回滚。Lua 脚本能够保障隔离性,而且能够完满的反对 前面的步骤依赖后面步骤的后果

Lua 脚本模式的身影简直无处不在,比方分布式锁、提早队列、抢红包等场景。

不过在编写 Lua 脚本时,要留神如下两点:

  1. 为了防止 Redis 阻塞,Lua 脚本业务逻辑不能过于简单和耗时;
  2. 仔细检查和测试 Lua 脚本,因为执行 Lua 脚本具备肯定的原子性,不反对回滚。
退出移动版