1:基于行锁实现分布式锁

实现原理
首先我们的表lock要提前存好相对应的lockName,这时候多个客户端来执行
1 | select lock_name from lock where lock_name = #{lockName} for update
|
由于第一个客户端来执行这条sql语句,给这行记录加了行锁,在这个客户端没有提交事务之前,其它客户端就会被阻塞住。所以这时候就只能有一个客户端去执行我们自己的业务了,其它客户端就只能阻塞等待,那么这个过程就是加锁
那么释放锁该怎么操作呢?
其实释放锁就很简单了,也就是将获取到锁的这个客户端的事务提交,这样其它客户端就可以来获取到这把行锁了,所以这时候就需要我们手动的提交事务了
代码实现
首先就是编写我们的加锁SQL语句了
1 2 | @Select ( "select lock_name from lock where lock_name = #{lockName} for update" )
List<String> queryLockNameForUpdate( @Param ( "lockName" ) String lockName);
|
然后我们需要实现我们的加锁 和 解锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | public class MySqlDistributeLock {
private String lockName;
private DataSourceTransactionManager dataSourceTransactionManager;
private MySqlLockMapper mySqlLockMapper;
private TransactionStatus status;
public MySqlDistributeLock(String lockName,DataSourceTransactionManager dataSourceTransactionManager,MySqlLockMapper mySqlLockMapper) {
this .lockName = lockName;
this .dataSourceTransactionManager = dataSourceTransactionManager;
this .mySqlLockMapper = mySqlLockMapper;
}
public void lock() {
TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
status = dataSourceTransactionManager.getTransaction(transactionDefinition);
while ( true ) {
try {
mySqlLockMapper.queryLockNameForUpdate( this .lockName);
break ;
} catch (Exception e) {
try {
Thread.sleep( 100 );
} catch (InterruptedException ignored) { }
}
}
}
public void unLock() {
dataSourceTransactionManager.commit( this .status);
}
}
|
最后看下业务方如何使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | @Service
public class LockService {
@Resource
private DataSourceTransactionManager dataSourceTransactionManager;
@Resource
private MySqlDistributeLock.MySqlLockMapper mySqlLockMapper;
public String deductStockMysqlLock(String productId,Integer count) {
MySqlDistributeLock lock = null ;
try {
lock = new MySqlDistributeLock(productId,dataSourceTransactionManager,mySqlLockMapper);
lock.lock();
} finally {
if (lock != null ) {
lock.unLock();
}
}
return "success" ;
}
}
|
2:基于唯一索引实现分布式锁

实现原理
首先我们的lock表要给lock_name字段建立一个唯一索引,这时候有多个客户端来加锁,本质上也就是添加一条记录,只不过lockName的值都是一样的
这时候客户端A成功的把lockName保存到lock表中了,那么其它客户端要保存这个lockName的时候(也就是执行加锁),由于唯一索引的缘故,就会插入失败。也就保证了同一个时间只能有一个客户端保存成功,也就是加锁成功了
那么如何释放锁呢?
在这个客户端业务执行完之后,手动的把这条记录删除掉,那么其它客户端就可以来继续加锁了
代码实现
首先我们在mapper文件中编写 加锁 和 解锁 的SQL,这里为什么还要保存个uuid,后续会讲到(主要是防止锁被误删)
1 2 3 4 5 6 7 | @Insert ( "insert into record_lock (lock_name, uuid) values (#{lockName}, #{uuid})" )
Integer insert( @Param ( "lockName" ) String lockName, @Param ( "uuid" ) String uuid);
@Delete ( "delete from record_lock where lock_name = #{lockName} and uuid = #{uuid}" )
Integer delete( @Param ( "lockName" ) String lockName, @Param ( "uuid" ) String uuid);
|
然后我们需要实现我们的加锁 和 解锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | public class MySqlDistributeLock {
private String lockName;
private MySqlLockMapper mySqlLockMapper;
private String uuid;
public MySqlDistributeLock(String lockName,MySqlLockMapper mySqlLockMapper,String uuid) {
this .lockName = lockName;
this .mySqlLockMapper = mySqlLockMapper;
this .uuid = uuid;
}
public void lock() {
while ( true ) {
try {
int result = mySqlLockMapper.insert( this .lockName, this .uuid);
if (result > 0 ) {
break ;
}
} catch (Exception e) {
}
try {
Thread.sleep( 100 );
} catch (InterruptedException interruptedException) {
throw new RuntimeException();
}
}
}
public void unLock() {
mySqlLockMapper.delete( this .lockName, this .uuid);
}
}
|
最后看下业务方如何使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | @Service
public class LockService {
@Resource
private MySqlDistributeLock.MySqlLockMapper mySqlLockMapper;
public String deductStockMysqlLock(String productId,Integer count) {
MySqlDistributeLock lock = null ;
try {
lock = new MySqlDistributeLock(productId, mySqlLockMapper,UUID.randomUUID().toString());
lock.lock();
} finally {
if (lock != null ) {
lock.unLock();
}
}
return "success" ;
}
}
|
基于唯一索引实现的分布式锁有没有什么问题呢??
死锁问题
我们试想一下,如果客户端A来加锁成功了,业务也执行完了,但是这时候释放锁的时候,也就是执行删除语句的时候因为一些原因导致删除失败了,那么这条记录一直存在,后续的线程就没办法再获取到锁了,这就是所谓的死锁
所以这时候我们还需要另外一个服务来定时扫描这些记录,如果这个记录超过了10分钟,或者20分钟还没有被删除掉,那么大概率是释放锁的时候失败了,所以需要再次删除这条记录
锁误删
为什么锁会误删呢? 为了防止死锁,我们会有一个单独的定时任务来扫描,假设我们判断一把锁超过10分钟就认为是释放锁失败了,这时候定时任务就会把这条记录删除掉,但是这时候就会有问题了,举个例子
客户端A首先获取到锁了,然后开始执行业务,但是因为业务比较复杂,执行完业务可能需要15分钟,这时候到第10分钟的时候,定时任务就会把这条记录给删除掉了
这时候因为记录没有了,客户端B来获取锁是能成功获取到的,所以这时候这把锁的持有者应该是客户端B的
到第15分钟的时候,客户端A业务执行完了,就是执行释放锁的逻辑,那么客户端A就会把这条记录给删除掉了,也就导致客户端A把客户端B的锁给释放掉了
所以在开头的时候,我们加锁除了要保存lockName,还要保存一个uuid,在释放锁的时候,判断一下uuid是否相等,如果不相等,那就不能删除这条记录了,因为这时候这把锁已经不是当前客户端持有的了
锁续期
大家可以想一下,分布式锁的主要目的就是同一个时间点只能有一个线程去执行业务,但是在上面我们可以看到,即使加了uuid来保证了锁误删,但是在同一个时间点可能是有多个线程在一起执行业务的,为了避免这种情况,就需要保证一个客户端在没有执行完业务以前,是不允许其它客户端执行业务的
但是定时任务判断的时间我们没办法预估,可能业务需要10分钟,也有可能是20分钟,我们没办法准确预估这个时间
所以我们在一个客户端加锁成功之后,可以起一个额外的线程,时时的更新加锁的时间,这就类似Redisson的看门狗机制了,那么如何去做呢??
1:加锁的时候,除了保存lockName,uuid,额外保存一个加锁时间lockTime
2:加锁成功之后,额外开启一个线程,每过10秒就更新lockTime为当前时间
3:定时任务扫描到lcokTime距离当前时间超过10分钟或者5分钟的记录就删除掉这条记录
3:基于乐观锁实现分布式锁
基于乐观锁机制就是依靠版本机制来实现,我们一般在数据库会保存version,或者是时间戳,至于实现方式大家可以自己实现一下,这里就不做赘述了