1.1乐观锁与悲观锁
悲观锁:
总是假设最坏的情况,当一个线程每次去拿数据的时候都认为其他线程会修改数据,所以线程每次就会在数据处理之前上锁,这样其他线程想拿到这个数据就会阻塞,直到获取到锁为止。悲观锁的实现往往依靠数据库提供的锁机制,即在数据库中,对数据操作前给记录加排他锁,如果锁获取失败,则说明数据正在被其他线程修改,当前线程则等待或者抛出异常,如果获取锁成功,则对记录进行操作,然后提交事务后释放排他锁。
下面看一个典型的例子:
public int updateEntry(long id){
//1.使用悲观锁获取指定记录
//for update是一种行级锁,也就是排他锁,在mysql中,如果sql语句明确主键,那么只锁这一条记录
//如果不明确或者没有主键,那么它就会锁整张表,直到commit时释放
EntryObject entry= query("select * from table where id=#{id} for update",id);
//2.修改记录内容,根据计算修改entry记录的属性
String name=generatorName(entry);
entry.setName(name);
......
//3.update操作
int count=update("update table set name=#{name},age=#{age} where id=#{id}",entry);
return count;
}
如上述代码,如果updateEntry(),query(),update()都使用了事务切面的方法,并且事务的传播行为是required(如果当前没有事务,新建一个事务,如果已存在一个事务,则加入这个事务)。执行updateEntry()方法时如果上次没有开启事务,它就会创建一个事务,进入方法里面,执行到1处时,由于传播行为时required,query方法不会新增事务,而是加入updateEntry()的事务,并且开启行级锁,直到事务结束,也就是updateEntry方法结束,事务才提交,锁才结束,下面2,3同理,当多个线程调用updateEntry(),切id相同,只会由一个线程成功,其他线程阻塞,保证了同一时间只有一个线程可以对记录进行修改。
乐观锁:
总是假设最好的i情况,每次去拿数据的时候都认为其他线程不会修改数据,所以不会上锁,只要在进行数据提交更新的时候,才会正式对数据冲突与否进行检测。
更改上述悲观锁代码:
public int updateEntry(long id){
//1.使用乐观锁获取指定记录
//这里去掉了for update
EntryObject entry= query("select * from table where id=#{id}",id);
//2.修改记录内容,根据计算修改entry记录的属性
String name=generatorName(entry);
entry.setName(name);
......
//3.update操作
//这里加了一个version字段,每次去判断一下是否与数据库的一致
int count=update("update table set name=#{name},age=#{age},version=${version}+1 where id=#{id} and version=#{version}",entry);
return count;
}
如上述代码。当多个线程执行updateEntry方法时,并没有排他锁的存在,都会先经过1,2操作,将记录放入线程本地栈里面并进行修改。当进行当3的update时,假设此时version值为0,由于update语句本身时原子性的,假设线程A执行成功了,此时version变为了1,其他线程进行更新时,发现数据库中的version已经变为1了,而他们原先拿到的是0,不匹配了,此时就没有修改成功,返回的count影响行为0.上面version+1操作类似于CAS操作。
经过上述过程,接下来线程由两种选择,一种是更新失败,一种是重试
重试操作代码如下:
public int updateEntry(long id){
boolean result =false;
int retryNum=5;
while(retryNum>0){
//1.使用乐观锁获取指定记录
EntryObject entry= query("select * from table where id=#{id}",id);
//2.修改记录内容,根据计算修改entry记录的属性
String name=generatorName(entry);
entry.setName(name);
......
//3.update操作
int count=update("update table set name=#{name},age=#{age},version=${version}+1 where id=#{id} and version=#{version}",entry);
return count;
if(count == 1){
result=true;
break;
}
retryNum--;
}
return result;
}
如上述代码所示:使用retryNum设置更新失败后重试次数,每失败一次重新去数据拿新的version值,类似于CAS的自旋操作,只是这里不是死循环,而是指定了尝试次数
乐观锁并不会使用数据提供的锁机制,一般在表中添加version字段或者使用业务状态来实现。乐观锁直到提交时才锁定,所以不会产生任何死锁