本文主要是介绍Redis RU101课程 Introduction to Redis Data Structures 第3周学习笔记,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
Transactions
Introduction
为了保证单条命令的原子性,Redis使用了单线程,命令都是顺序依次执行(unlink是个例外,是异步的)。
事务保证了将多条命令作为一个单元执行。
Using Transactions
事务所有命令:
127.0.0.1:6379> help @transactionsDISCARD -summary: Discard all commands issued after MULTIsince: 2.0.0EXEC -summary: Execute all commands issued after MULTIsince: 1.2.0MULTI -summary: Mark the start of a transaction blocksince: 1.2.0UNWATCH -summary: Forget about all watched keyssince: 2.2.0WATCH key [key ...]summary: Watch the given keys to determine execution of the MULTI/EXEC blocksince: 2.2.0
MULTI相当于begin transaction, MULTI后面的命令会存入命令队列,EXEC一次性执行队列中所有命令,DISCARD放弃队列中所有命令。
可以用2个client来演示事务的隔离性。
multi中不能再嵌套multi,也就是事务不能嵌套。
对于语法错误和数据类型操作错误,不会影响事务中其它的操作,或者说不会引发rollback。其实redis就不支持rollback:
127.0.0.1:6379> set key01 1
OK
127.0.0.1:6379> set key02 a
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incrby key01 1
QUEUED
127.0.0.1:6379> incrby key02 1
QUEUED
127.0.0.1:6379> exec
1) (integer) 2
2) (error) ERR value is not an integer or out of range
127.0.0.1:6379> get key01
"2"
rollback的开销很大,避免rollback也保证了最小延迟和最大吞吐。
对于系统错误,如内存不够,Redis会保证不会出现部分写和不一致性。
传统数据库交易中的命令是马上执行,因此消耗了系统资源;Redis中是将所有命令放入队列,然后一次性执行。
扩展阅读
- Redis Clients Handling
- Transactions
Optimistic Concurrency Control
Optimistic Concurrency Control是指监控1个或多个key的变化,如果改变了就放弃事务。这通常是由于你已经读取了这个值。
Optimistic Concurrency Control是通过WATCH和UNWATCH实现的。WATCH必须在事务开始前,事务结束后,会自动UNWATCH。
WATCH命令不是全局的,只影响到调用它的客户端。
client1> set key01 200
OK
client1> watch key01
OK
client1> multi
OK
client1> incrby key01 1
QUEUED
client2> get key01
"200"
client2> decr key01
(integer) 199
client1> exec
(nil)
client1> get key01
"199"
EXEC何时会失败呢? 这里的失败可认为是DISCARD。当命令有语法错误,或WATCH的key被更改时,都会失败。但如果只是操作错误的数据类型,如对字符串加1,则只是忽略这个命令而已。
Object Storage with Hashes
Introduction
用String和Hash都可以存取对象存储,但前者不能增量存取,后者可以单独操作filed,如读写,测试存在,但TTL是针对整个key来设的。
Storing a Simple Object
Hash的多field加value的结构非常类似JSON,而且灵活的schema避免了alter table操作。
HSET可实现JSON结构。相关命令包括HGET, HMGET和HGETALL(阻塞操作,建议小于100 field时用),HSCAN(非阻塞操作)。
小数据集时用HMGET方便,大数据集时建议HSCAN。
HSET时,会自动创建不存在的field,相关包括HEXISTS, HSETNX。
HINCR,HINCRBYFLOAT可对field执行数字操作。
HDEL可删除filed。
Storing Complex Objects
复杂对象,在关系型数据库里就是主从表,在NOSQL中就是带子文档的JSON。
可以用3种方式实现:
- 多个Hash
- 多个Hash+Set
- 单个Hash
Redis推荐方式3,完全平的方式。好处是简单,支持原子操作,无需事务。实现非常类似于层级文件系统到对象存储文件名的映射,用不同的filed name即可实现。例如假设B为子文档:
hmset key B:C value B:D value
不好的地方是删除子文档时需要删除多个field。还有就是设计上的考虑,子文档放在那个父文档中,后续子文档移动时会涉及很多操作。还有就是对象会比拆开存要大。
第2种方式即将子文档单独存为Hash,它和父文档的关系需要通过应用体现,当然在key命名上也可以提示。这种方式的好处为,可独立存放,可扩展,可单独设置TTL。不好的地方是一个对象需要多个key表示,关系需要维护。然后对象所有key需要存在一个shard,这是后话。
Use Case: Inventory Control
Use Case Overview
售票系统,每个用户可以买多张票,有3场比赛,柔道资格赛,男子100m决赛和女子4x100接力预赛。
Inventory Control
流程参考这个图:
在此流程中,减库存和生成订单需放在一个事务中。
示例程序为uc02-inventory-control/inventory.py
示例程序演示了3个测试场景,依次由简单到复杂:
- 票不够(仅检查票)
- 票够钱不够,这里假设用户为joan时,钱总是不够(检查票和钱)
- 持票超过30秒,票返回库存
7个用户, 数据结构如下:
127.0.0.1:6379> scan 0 match uc02:customer* count 30000
1) "0"
2) 1) "uc02:customer:jim"2) "uc02:customer:joan"3) "uc02:customer:jamie"4) "uc02:customer:amy"5) "uc02:customer:mary"6) "uc02:customer:bill"7) "uc02:customer:fred"127.0.0.1:6379> type uc02:customer:jim
hash127.0.0.1:6379> hgetall uc02:customer:jim
1) "id"
2) "jim"
3) "customer_name"
4) "jim somebody"
有3个比赛的票,数据结构如下, 注意其中的余票数和票价:
127.0.0.1:6379> scan 0 match uc02:event:* count 30000
1) "0"
2) 1) "uc02:event:320-GHI-921"2) "uc02:event:123-ABC-723"3) "uc02:event:737-DEF-911"
127.0.0.1:6379> type uc02:event:320-GHI-921
hash
127.0.0.1:6379> hgetall uc02:event:320-GHI-9211) "sku"2) "320-GHI-921"3) "name"4) "Womens Judo Qualifying"5) "disabled_access"6) "False"7) "medal_event"8) "False"9) "venue"
10) "Nippon Budokan"
11) "category"
12) "Martial Arts"
13) "capacity"
14) "14471"
15) "available:General"
16) "500"
17) "price:General"
18) "15.25"
19) "held:General"
20) "0"127.0.0.1:6379> hgetall uc02:event:123-ABC-7231) "sku"2) "123-ABC-723"3) "name"4) "Men's 100m Final"5) "disabled_access"6) "True"7) "medal_event"8) "True"9) "venue"
10) "Olympic Stadium"
11) "category"
12) "Track & Field"
13) "capacity"
14) "60102"
15) "available:General"
16) "10"
17) "price:General"
18) "25.0"127.0.0.1:6379> hgetall uc02:event:737-DEF-9111) "sku"2) "737-DEF-911"3) "name"4) "Women's 4x100m Heats"5) "disabled_access"6) "True"7) "medal_event"8) "False"9) "venue"
10) "Olympic Stadium"
11) "category"
12) "Track & Field"
13) "capacity"
14) "60102"
15) "available:General"
16) "10"
17) "price:General"
18) "19.5"
19) "held:General"
20) "0"
为防止在订票期间库存发生改变,程序使用WATCH监控了相关的库存(例如uc02:event:123-ABC-723)。
订单的数据结构如下,和库存之间通过sku关联:
127.0.0.1:6379> scan 0 match uc02:sales_order:* count 30000
1) "0"
2) 1) "uc02:sales_order:CUJXZN-ITBCOW"2) "uc02:sales_order:RSNKID-SOCIFH"
127.0.0.1:6379> type uc02:sales_order:CUJXZN-ITBCOW
hash
127.0.0.1:6379> hgetall uc02:sales_order:CUJXZN-ITBCOW1) "order_id"2) "CUJXZN-ITBCOW"3) "customer"4) "bill"5) "tier"6) "General"7) "qty"8) "5"9) "cost"
10) "125.0"
11) "event_sku"
12) "123-ABC-723"
13) "ts"
14) "1606361972"
Python代码建议好好看看。
Reservations
场景2中加入了信用卡验证的环节。因此售票过程分多个事务完成。
首先扣票是第一个事务,扣票成功后会生成临时的持票记录。第二个事务围为信用卡验证环节,如果信用卡验证失败,就将持有票返回票库,成功则生成订单,清空持票记录。
通过调试python(python -m pdb)程序,我们得到持票的数据结构, 包括扣票数量,票级别和扣票时间,field名字的后缀时随机字符串:
127.0.0.1:6379> scan 0 match uc02:ticket_hold* count 30000
1) "0"
2) 1) "uc02:ticket_hold:737-DEF-911"
127.0.0.1:6379> type uc02:ticket_hold:737-DEF-911
hash
127.0.0.1:6379> hgetall uc02:ticket_hold:737-DEF-911
1) "qty:KUEBNB-BKFHGK"
2) "5"
3) "tier:KUEBNB-BKFHGK"
4) "General"
5) "ts:KUEBNB-BKFHGK"
6) "1606368660"
对应的库存记录中,在held:General字段也会记录扣票数量。
Expiration of Reservations
场景3模拟的是自动将持票退回库存的场景。比如购票过程中网断了。程序自动生成了3条持票记录,然后进入循环,每个1秒检查持票记录,超过30秒后自动将票返回票库。直到没有持票就退出循环。注意,这里并没有设TTL。
持票定义如下,可以看到到期时间一次为14秒后,8秒后和立刻:
holds = {'qty:VPIR6X': 3, 'tier:VPIR6X': tier, 'ts:VPIR6X': int(cur_t - 16),'qty:B1BFG7': 5, 'tier:B1BFG7': tier, 'ts:B1BFG7': int(cur_t - 22),'qty:UZ1EL0': 7, 'tier:UZ1EL0': tier, 'ts:UZ1EL0': int(cur_t - 30)}
因此输出如下:
uc02:event:320-GHI-921
== Create ticket holds, expire > 30 sec, return tickets to inventory
320-GHI-921, Available:485, Reservations:['3', '5', '7']
320-GHI-921, Available:492, Reservations:['3', '5', None]
320-GHI-921, Available:492, Reservations:['3', '5', None]
320-GHI-921, Available:492, Reservations:['3', '5', None]
320-GHI-921, Available:492, Reservations:['3', '5', None]
320-GHI-921, Available:492, Reservations:['3', '5', None]
320-GHI-921, Available:492, Reservations:['3', '5', None]
320-GHI-921, Available:492, Reservations:['3', '5', None]
320-GHI-921, Available:492, Reservations:['3', '5', None]
320-GHI-921, Available:497, Reservations:['3', None, None]
320-GHI-921, Available:497, Reservations:['3', None, None]
320-GHI-921, Available:497, Reservations:['3', None, None]
320-GHI-921, Available:497, Reservations:['3', None, None]
320-GHI-921, Available:497, Reservations:['3', None, None]
320-GHI-921, Available:497, Reservations:['3', None, None]
320-GHI-921, Available:500, Reservations:[None, None, None]
最后的建议是,当持票记录多时,建议从Hash改为Sorted Set,然后将时间戳作为value。这样方便提取最先过期的持票记录。
None, None]
320-GHI-921, Available:497, Reservations:[‘3’, None, None]
320-GHI-921, Available:497, Reservations:[‘3’, None, None]
320-GHI-921, Available:497, Reservations:[‘3’, None, None]
320-GHI-921, Available:500, Reservations:[None, None, None]
最后的建议是,当持票记录多时,建议从Hash改为Sorted Set,然后将时间戳作为value。这样方便提取最先过期的持票记录。
这篇关于Redis RU101课程 Introduction to Redis Data Structures 第3周学习笔记的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!