乐观锁和悲观锁
一、什么是乐观锁和悲观锁
乐观锁和悲观锁主要使用在并发的情况下,在多个事务中共同访问同一个数据库资源,为了避免因为同时访问造成的数据操作错误而产生。这里从两方面说,第一指的是数据库,第二是java.
数据库中的乐观锁和悲观锁
乐观锁,主要强调的是每次取数据的时候,都认为别的线程或事务不会修改数据,所以不会对数据进行上锁,只有在更新数据的时候才会判断是否有线程对要操作的数据进行修改;
悲观锁,主要强调的是每次取数据的时候,都认为别的线程会修改数据,所以每次都会对数据上锁,只有获取锁的情况下才有操作数据的机会;
从上面的描述中,可看出数据库层面的乐观锁和悲观锁,主要是在集中在对sql的处理上,即反映在数据库的能力上。
java中的乐观锁和悲观锁
对应于乐观锁和悲观锁的逻辑,即对资源的占用情况,在java中同时有对应的表现,如,synchronized关键字、reentrantlock等,都是独占资源的情况,所以属于悲观锁;CAS操作属于乐观锁;
二、乐观锁和悲观锁的具体实现
这里的具体实现以数据库层面的为主,java中并发的情况下次在写。
以一个具体的例子来说明乐观锁和悲观锁的具体使用。
有商品表(products),其表结构如下,
此表只有两列商品名称(p_name)、商品数量(p_num),以商城秒杀系统减库存场景来说明。假设,现在要对p_name为“手表”的库存进行更新,现在有多个线程都要进行减库存操作。
1)、未使用锁
首先,要从数据库中查出当前的库存p_num1,然后把库存减去p_num2,即p_num_new=p_num1-p_num2;
select p_num from products where p_name ='手表'; update products set p_num=p_num_new where p_name ='手表';
上面的语句在单线程低并发下不会有问题,但在多线程高并发下,假如,两个线程T1、T2读到同一个p_num1,又同时去减库存,那么它们计算到的最新的库存为,
p_num_t1为线程T1减的库存数,p_num_t2为线程T2减的库存数,
T1:p_num_new=p_num1-p_num_t1;
T2:p_num_new=p_num1-p_num_t2;
从上边看出,T2的库存数计算的是有问题的,应该是:p_num_new=p_num1-p_num_t1-p_num_t2
那么上边的数据就是错误。
2)、乐观锁
实现乐观锁有两种方式,即version和CAS。下面以上面的例子一一说明
2.1、version,数据版本号,
需要在原有数据表结构中新增一列version,作为版本号,其数据类型可为bigint类型,其作用主要体现在更新过程中,代表数据被更新的次数,每成功更新一次加1;
其商品表(products)结构如下,
首先,要从数据库中查出当前的库存p_num1和version(记为version_old),然后把库存减去p_num2,即p_num_new=p_num1-p_num2;
select p_num,version from products where p_name ='手表'; update products set p_num=p_num_new,,version=version+1 where p_name ='手表' and version=#{version_old};
上面sql中新增了version条件,即使用取出的version作为更新的条件。看在两个线程下的过程,
假如,两个线程T1、T2读到同一个p_num1和version(记为version_old),又同时去减库存,那么它们计算到的最新的库存为,
p_num_t1为线程T1减的库存数,p_num_t2为线程T2减的库存数,
T1:p_num_new=p_num1-p_num_t1;
T2:p_num_new=p_num1-p_num_t2;
再看T1和T2的更新过程,假如T1先执行,T1更新成功后的version值记为version_t1
update products set p_num=p_num_t1,,version=version+1 where p_name ='手表' and version=#{version_old};
T2的更新为,
update products set p_num=p_num_t1,,version=version+1 where p_name ='手表' and version=#{version_old};
T1更新成功后,T2还会成功吗?肯定不会,因为此时的version执行了加1操作,变成了version_t1=version+1,而T2的条件中的version依旧为version,即version+1≠version,此时T2是不会执行成功,那么这样的话数据就不会造成混乱。
在执行更新操作的时候,只有现在数据的版本号,和读出的版本号一致,才更新成功,否则更新失败;
2.2、CAS操作
CAS中包含三个值,内存值V,现在值A,新值B,只有在保证V=A的情况下,才会把V更新为B;应用到数据库方面,V代表的是首先读出的值;
以上面的例子说明,
要从数据库中查出当前的库存p_num1,然后把库存减去p_num2,即p_num_new=p_num1-p_num2;
select p_num,version from products where p_name ='手表';
update products set p_num=p_num_new,,version=version+1 where p_name ='手表' and
p_num=#{p_num1};
假如,两个线程T1、T2读到同一个p_num1,又同时去减库存,那么它们计算到的最新的库存为,
p_num_t1为线程T1减的库存数,p_num_t2为线程T2减的库存数,
T1:p_num_new=p_num1-p_num_t1;
T2:p_num_new=p_num1-p_num_t2;
再看T1和T2的更新过程,假如T1先执行,T1更新成功后的p_num变成了p_num1-p_num_t1,
update products set p_num=p_num_new where p_name ='手表' and p_num=#{p_num1}
T2再执行,
update products set p_num=p_num_new where p_name ='手表' and p_num=#{p_num1}
此时数据中的p_num值已经变成了p_num1-p_num_t1,那么where条件就不成立,那么更新便不会成功。
通过在更新时比较现在的值和之前的值是否一致,来判断是否可更新成功,其原理类似于version。但此种方式无法避免ABA的问题,即,如果p_num被更新过,且正好更新为了p_num1,使用CAS的方式是可以更新成功的,但最终的结果是一致的。
2)、悲观锁
即,对数据库的所有操作均加上数据库锁,其实现主要依赖于数据层面,
Select * from products for update; 对查询结果的每行均加排他锁
悲观锁能够防止丢失更新和不可重复读这类问题,但是它非常影响并发性能,因此应该谨慎使用
三、总结
乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大系统的整体吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适,即对所有的操作加锁控制。
有不当之处欢迎指正,谢谢!