何为并发安全,就是多个并发体在同一段时间内访问同一个共享数据,共享数据能被正确处理。数据库
并发不安全最典型的案例就是卖票超售,设想有一家电影院,有两个售票窗口,售票员售票时候先看一下当前剩余票数是否大于0,若是大于0则售出票。
用伪代码就是以下:安全
# 售票操做(一张票) # 若是票数大于0 totalNum = getTotalNum() if totalNum > 0 # 则售出一张票 totalNum = totalNum - 1 else failedToSold()
看上去也没有什么问题,流程图以下:
此时票数剩下一张票,两个售票窗口同时来了顾客,两个售票人都看了一下剩余票数还有一张,不约而同地收下顾客的钱,余票还剩一张,可是却售出了两张票,就会出现致命的问题。
并发
目前最最主流的办法就是加锁就行操做,其实售票的整个操做同时间内只能一我的进行,在我看来归根到底加锁其实就是让查询和售票两个步骤原子化,只能一块执行,不能被其余程序中断,让这步操做变成串行化。下面就介绍一下使查询和售票原子化的常见程序操做:函数
锁的作法就是每次进入这段变量共享的程序片断,都要先获取一下锁,若是获取成功则能够继续执行,若是获取失败则阻塞,直到其余并发体把锁给释放,程序获得执行调度才能够执行下去。
锁本质上就是让并发体建立一个程序临界区,临界区一次只能进去一个并发体,伪代码示意以下:性能
lock() totalNum = getTotalNum() if totalNum > 0 # 则售出一张票 totalNum = totalNum - 1 else failedToSold() unlock()
顺带一提的是锁能够分为写锁与排它锁,通常如无特殊说明,通常锁都是指写锁。atom
读锁也叫共享锁,写锁也叫排它锁,锁的概念被发明了以后,人们就想着若是我不少个并发体大部分时间都是读,若是就把变量读取的时候也要创建临界区,那就有点太大题小作了。因而人们发明了读锁,一个临界区若是加上了读锁,其余并发体执行到相同的临界区均可以加上读锁,执行下去,但不能加上写锁。这样就保证了能够多个并发体并发读取而又不会互相干扰。code
队列也是解决并发不安全的作法。多个并发体去获取队列里的元素,而后进行处理,这种作法和上锁其实大同小异,本质都是把并发的操做串行化,同一个数据同一个时刻只能交给一个并发体去处理。
伪代码:队列
# 第一个获取到队列的元素就能够进行下去 isCanSold = canSoldList.pop() totalNum = getTotalNum() if totalNum > 0 # 则售出一张票 totalNum = totalNum - 1 else failedToSold()
CAS(compare and swap),先比对,而后再进行交换,和数据库里的乐观锁的作法很类似。事务
数据库里的乐观锁并非真的使用了锁的机制,而是一种程序的实现思路。
乐观锁的想法是,每次拿取数据再去修改的时候很乐观,认为其余人不会去修改这个数据,表另外维护一个额外版本号的字段。
查数据的时候记录下该数据的版本号,若是成功修改的话,会修改该数据的版本号,若是修改的时候版本号和查询的时候版本号不一致,则认为数据已经被修改过,会从新尝试查询再次操做。
设咱们表有一个user表,除了必要的字段,还有一个字段version,表以下:get
id | username | money | version |
---|---|---|---|
1 | a | 10 | 100 |
2 | b | 20 | 100 |
这时候咱们须要修改a的余额-10元,执行事务语句以下:
while select @money = money, @version = version from user where username = a; if @money < 10 print('余额成功') break # 扣费前的预操做 paied() # 实行扣费 update user set money = money - 10, version = version + 1 where username = a and version = @version # 影响条数等于1,证实执行成功 if @@ROWCOUNT == 1 print('扣费成功') break else rollback print('扣费失败,从新进行尝试')
乐观锁的作法就是使用版本的形式,每次写数据的时候会比对一下最开始的版本号,若是不一样则证实有问题。
CAS的作法也是同样的,在代码里面的实现稍有一点不一样,因为SQL每条语句都是原子性,查询对应版本号的数据再更新的这个条件是原子性的。
update user set money = money - 10, version = version + 1 where username = a and version = @version
可是在代码里面两条查询和赋值两个语句不是原子性的,须要有特定的函数让cpu底层把两个操做变成一个原子操做,在go里面有atomic包支持实现,是这样实现的:
for { user := getUserByName(A) version := user.version paied() if atomic.CompareAndSwapInt32(&user.version, version, version + 1) { user.money -= 10 } else { rollback() } }
atomic.CompareAndSwapInt32须要依次传入要比较变量的地址,旧变量的值,修改后变量的值,函数会判断旧变量的值是否与如今变量的地址是否相同,相同则把新变量的值写入到该变量。 CAS的好处是不须要程序去建立临界区,而是让CPU去把两个指令变成原子性操做,性能更好,可是若是变量会被频繁更改的话,重试的次数变多反而会使得效率不如加锁高。