准确的讲,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 则业务被丢掉。

下面展现一个业务的比方。

1
 redis> MULTI 
2
 OK
3
 redis> SET msg "hello world"
4
 QUEUED
5
 redis> GET msg
6
 QUEUED
7
 redis> EXEC
8
 1) OK
9
 1) hello world

这里有一个疑问?在敞开业务的时分,Redis key 能够被修正吗?

一文讲透  Redis 事务 (事务模式 VS Lua 脚本)

在业务履行 EXEC 指令之前 ,Redis key 仍然能够被修正

在业务敞开之前,咱们能够 watch 指令监听 Redis key 。在业务履行之前,咱们修正 key 值 ,业务履行失利,回来 nil

一文讲透  Redis 事务 (事务模式 VS Lua 脚本)

经过上面的比方,watch 指令能够完结类似乐观锁的作用

2 业务的ACID

2.1 原子性

原子性是指:一个业务中的一切操作,或许全部完结,或许全部不完结,不会结束在中心某个环节。业务在履行过程中产生过错,会被回滚到业务开端前的状况,就像这个业务从来没有履行过一样。

第一个比方:

在履行 EXEC 指令前,客户端发送的操作指令过错,比方:语法过错或许运用了不存在的指令。

1
 redis> MULTI
2
 OK
3
 redis> SET msg "other msg"
4
 QUEUED
5
 redis> wrongcommand  ### 故意写过错的指令
6
 (error) ERR unknown command 'wrongcommand' 
7
 redis> EXEC
8
 (error) EXECABORT Transaction discarded because of previous errors.
9
 redis> GET msg
10
 "hello world"

在这个比方中,咱们运用了不存在的指令,导致入队失利,整个业务都将无法履行 。

第二个比方:

业务操作入队时,指令和操作的数据类型不匹配 ,入行列正常,但履行 EXEC 指令反常 。

1
 redis> MULTI 
2
 OK
3
 redis> SET msg "other msg"
4
 QUEUED
5
 redis> SET mystring "I am a string"
6
 QUEUED
7
 redis> HMSET mystring name  "test"
8
 QUEUED
9
 redis> SET msg "after"
10
 QUEUED
11
 redis> EXEC
12
 1) OK
13
 2) OK
14
 3) (error) WRONGTYPE Operation against a key holding the wrong kind of value
15
 4) OK
16
 redis> GET msg
17
 "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 的解说:

一文讲透  Redis 事务 (事务模式 VS Lua 脚本)

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 简介

一文讲透  Redis 事务 (事务模式 VS Lua 脚本)

Lua 由标准 C 编写而成,代码简练美丽,简直在一切操作系统平和台上都能够编译,运转。Lua 脚本能够很容易的被 C/C ++ 代码调用,也能够反过来调用 C/C++ 的函数,这使得 Lua 在应用程序中能够被广泛应用。

Lua 脚本在游戏范畴大放异彩,咱们耳熟能详的《大话西游II》,《魔兽国际》都大量运用 Lua 脚本。Java 后端工程师触摸过的 api 网关,比方 OpenrestyKong 都能够看到 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 指令

指令格局:

1
 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]获取。

简单实例:

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

下面演示下 Lua 怎么调用 Redis 指令 ,经过redis.call()来履行了 Redis 指令 。

1
 redis> set mystring 'hello world'
2
 OK
3
 redis> get mystring
4
 "hello world"
5
 redis> EVAL "return redis.call('GET',KEYS[1])" 1 mystring
6
 "hello world"
7
 redis> EVAL "return redis.call('GET','mystring')" 0
8
 "hello world"

3.3 EVALSHA 指令

运用 EVAL 指令每次恳求都需求传输 Lua 脚本 ,若 Lua 脚本过长,不只会耗费网络带宽,并且也会对 Redis 的性能形成必定的影响。

思路是先将 Lua 脚本先缓存起来 , 回来给客户端 Lua 脚本的 sha1 摘要。 客户端存储脚本的 sha1 摘要 ,每次恳求履行 EVALSHA 指令即可。

一文讲透  Redis 事务 (事务模式 VS Lua 脚本)

EVALSHA 指令基本语法如下:

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

实例如下:

1
 redis> SCRIPT LOAD "return 'hello world'"
2
 "5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
3
 redis> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0
4
 "hello world"

4 业务 VS Lua 脚本

从界说上来说, Redis 中的脚本自身便是一种业务, 所以任何在业务里能够完结的事, 在脚本里面也能完结。 并且一般来说, 运用脚本要来得更简单,并且速度更快

由于脚本功能是 Redis 2.6 才引进的, 而业务功能则更早之前就存在了, 所以 Redis 才会一起存在两种处理业务的方法。

不过咱们并不打算在短时间内就移除业务功能, 由于业务供给了一种即便不运用脚本, 也能够防止竞争条件的方法, 并且业务自身的完结并不杂乱。

— redis.io/

Lua 脚本是另一种形式的业务,他具有必定的原子性,但脚本报错的状况下,业务并不会回滚。Lua 脚本能够确保阻隔性,并且能够完美的支撑后边的过程依靠前面过程的成果

Lua 脚本形式的身影简直无处不在,比方分布式锁、延迟行列、抢红包等场景。

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

  1. 为了防止 Redis 阻塞,Lua 脚本业务逻辑不能过于杂乱和耗时;
  2. 仔细查看和测验 Lua 脚本 ,由于履行 Lua 脚本具有必定的原子性,不支撑回滚。