MySQL 的事务和锁(一)

最近做了一些和交易系统有关的东西,也乘机复习了一下 MySQL 的事务和锁机制。

1、事务

什么是事务呢?按照标准的描述:

A transaction symbolizes a unit of work performed within a database management system (or similar system) against a database, and treated in a coherent and reliable way independent of other transactions. A transaction generally represents any change in database. Transactions in a database environment have two main purposes:

  1. To provide reliable units of work that allow correct recovery from failures and keep a database consistent even in cases of system failure, when execution stops (completely or partially) and many operations upon a database remain uncompleted, with unclear status.
  2. To provide isolation between programs accessing a database concurrently. If this isolation is not provided, the programs' outcomes are possibly erroneous.

翻译过来就是,事务是对于数据库的操作序列,事务的目的有两个:

  • 提供一种从失败中回复的可靠机制,同时在系统挂掉的时候保证数据库的一致性
  • 为并发访问数据库提供一种隔离机制

如何使用事务:


# -*- coding: utf-8 -*-
import time
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.dialects.mysql import BIGINT, INTEGER, VARCHAR
from sqlalchemy.schema import Column

Model = declarative_base()
engine = create_engine(
     'mysql+mysqldb://root@127.0.0.1/test_session?charset=utf8mb4',
    echo=True,
    pool_size=2,
    max_overflow=5,
    pool_timeout=0,
    pool_recycle=3600
)

Session = sessionmaker(bind=engine)

import 了一大堆东东之后,创建了一个 engine,然后 bind 到一个 session 中,后面就可以用 Session() 生成可以用的 session 了.

使用事务创建两个用户

session = Session()
account = Account(
    member_id=1,
    amount=20
)

session.add(account)

account = Account(
    member_id=2,
    amount=0
)

session.add(account)session.commit()

上面,在账户里面创建了两个账户,第一个账户,初始有 20 元,第二个账户,初始为 0 元

使用事务进行转账


# 第一笔转账 account1 -> account2
session1 = Session()
account1 = session1.query(Account).get(1)
account2 = session1.query(Account).get(2)
account1.amount -= 10
account2.amount += 10

# 第二笔转账 account1 -> account2
session2 = Session()
account1 = session2.query(Account).get(1)
account2 = session2.query(Account).get(2)
account1.amount -= 10
account2.amount += 10

# 提交事务
session1.commit()
session2.commit()

# 查询余额
session = Session()
account1 = session.query(Account).get(1)
account2 = session.query(Account).get(2)
print account1.amount, account2.amount

上面大致模拟了,两个并发的事务,理论上,两笔转账以后,account1 的余额为 0 元,account2 的余额为 20 元,但实际上输出为:

10 10

可见,数据库的事务虽然提供了所谓的隔离,但是依然不能保证结果的正确性,这里得使用数据库提供的锁。

2、锁

首先看看 Mysql 关于锁的文档:

LOCK TABLES
tbl_name [[AS] alias] lock_type
[, tbl_name [[AS] alias] lock_type] ...

lock_type:
READ [LOCAL]
| [LOW_PRIORITY] WRITE

UNLOCK TABLES

所以看起来,锁的类型主要有 READ、READ LOCAL、LOW_PRIORITY WRITE、WRITE,但是,文档下面又有两句:

  1. For InnoDB tables, READ LOCAL is the same as READ.
  1. The LOW_PRIORITY modifier has no effect. In previous versions of MySQL, it affected locking behavior, but this is no longer true. It is now deprecated and its use produces a warning. Use WRITE without LOW_PRIORITY instead.

所以我们只需要关注,READ LOCK 和 WRITE LOCK 就可以了,接下来试验一下,两种锁的区别:

READ LOCK


session1 = Session()
account1 = session1.query(Account).with_lockmode('read').get(1)
account2 = session1.query(Account).with_lockmode('read').get(2)

account1.amount -= 10
account2.amount += 10

session2 = Session()
account1 = session2.query(Account).with_lockmode('read').get(1)
account2 = session2.query(Account).with_lockmode('read').get(2)

account1.amount -= 10
account2.amount += 10

session1.commit()
session2.commit()

我们的程序会阻塞在 session1.commit 这里,最后报了这个异常:

sqlalchemy.exc.OperationalError: (_mysql_exceptions.OperationalError) 
(1205, 'Lock wait timeout exceeded; try restarting transaction')
[SQL: u'UPDATE account SET amount=%s WHERE account.id = %s'] [parameters: ((960L, 1L), (2040L, 2L))]

Lock wait timeout exceeded 等待锁超时,此时如果我们捕获这个异常,那么 session2.commit 就会成功。

所以关于 READ 锁,结论就是:

  • 可以被多个 session 持有
  • 没有被释放之前,其他事务不能更新被加锁的内容

上面就是因为,READ LOCK 同时被 session1 和 session2 持有,所以 session1 试图更新内容的时候,会因为等待 session2 而超时,所以 READ LOCK 通常用在一个场景下:就是本事务未完成或者回滚之前,不希望其他事务更新读取的内容,比如需要定时导出某个记录的快照,我希望在我导出完成之前,这条记录不被更新,就可以用 READ LOCK.

WRITE LOCK

同样是上面的代码,把 read 换成 update

这次卡在了 account1 = session2.query(Account).with_lockmode('update').get(1) 上,所以结论是:

  • WRITE LOCK 只能被一个 session 持有
  • 没有释放前,其他会话不能读取加锁的内容

这个使用场景比较多,比如上面咱们的转账,当这里并发的转账请求到来的时候,第一个会话,持有了 12 的锁,session2 就需要等待 session1 完成,才能继续,在这里的结局就是超时报错

总结

所以,想要改写内容的时候,如果要求保证数据一致性,就得使用 WRITE LOCK。READ LOCK 的使用场景,多见与上下逻辑中,对于某个字段值有依赖,需要在进入会话之后,保持该字段不被其他会话修改。
同时,也总结出一点,就是写这种程序的时候,一定要专心。

关于加锁的规则,下次再说,这里面也有很多坑,搞不好,就锁了整个表

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容