乐观锁与悲观锁
乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
案例:
某商品,用户购买后库存数应-1,而某两个或多个用户同时购买,此时三个执行程序均同时读得库存为n,之后进行了一些操作,最后将均执行update table set
库存数=n-1,那么,很显然这是错误的。
解决:
1.使用悲观锁(其实说白了也就是排他锁)
程序A在查询库存数时使用排他锁(
select * from table where id=10 for update
)
然后进行后续的操作,包括更新库存数,最后提交事务。
程序B在查询库存数时,如果A还未释放排他锁,它将等待。
程序C同B……
2.使用乐观锁(靠表设计和代码来实现)
一般是在该商品表添加version版本字段或者timestamp时间戳字段
程序A查询后,执行更新变成了:
update table set num=num-1 where id=10 and version=23
这样,保证了修改的数据是和它查询出来的数据是一致的,而其他执行程序未进行修改。当然,如果更新失败,表示在更新操作之前,有其他执行程序已经更新了该库存数,那么就可以尝试重试来保证更新成功。为了尽可能避免更新失败,可以合理调整重试次数(阿里巴巴开发手册规定重试次数不低于三次)。
总结:对于以上,可以看得出来乐观锁和悲观锁的区别。
1.悲观锁使用了排他锁,当程序独占锁时,其他程序就连查询都是不允许的,导致吞吐较低。如果在查询较多的情况下,可使用乐观锁。
2.乐观锁更新有可能会失败,甚至是更新几次都失败,这是有风险的。所以如果写入较频繁,对吞吐要求不高,可使用悲观锁。
也就是一句话:读用乐观锁,写用悲观锁。
表级锁与行级锁
表级锁:
- table-level locking,锁住整个表。
- 开销小,加锁快。
- 不会死锁(一次性加载所需的所有表)。
- 锁粒度大,发生锁冲突概率大,并发效率低。
- 适合查询。
行级锁:
- row-level loking,锁住一行记录。
- 开销大,加锁慢。
- 会死锁。
- 锁粒度小,发生所冲突概率小,并发效率高。
- 适合并发写,事务控制。
- 并不是直接丢记录行加锁,而是对行对应的索引加锁:
- 如果sql 语句操作了主键索引,Mysql 就会锁定这条主键索引。
- 如果sql语句操作了非主键索引,MySQL会先锁定该非主键索引,再锁定相关的主键索引。
- 在InnoDB中,如果SQL语句不涉及索引,则会通过隐藏的聚簇索引来对记录加锁。
- 对聚簇索引加锁,实际效果跟表锁一样,因为找到某一条记录就得扫描全表,要扫描全表,就得锁定表。