前言
分布式锁目前的方案有很多比较流行的比如redis、zookeeper等,本文主要讲述mysql分布式锁的使用
服务集群多实例部署的情况下,就会有多个服务连接同一个数据库,某些业务操作可能就会操作多次数据库,导致数据重复,比如前端重复点击(前端说我可以控制置灰,但接口单独拎出来就不行了)、接口重复调用等。如果服务是单机版直接利用同步锁或者Lock锁即可,集群部署就需要用到分布式锁,这里用mysql做分布式锁主要用在并发不高或者又懒得引用额外的依赖的情况下使用。
直接上代码
代码如下(示例):
这里createByLock方法做成一个通用的方法,为了不在此方法里面写业务操作,可以给方法的入参传一个函数式接口,接口的返回值类型可自定义,示例返回的是String,通过接口回调,再调用业务方法。另外这里单独建立一张表job_lock ,表里面就只有一个字段job_name作为锁操作的表。
public void createByLock(Supplier<String> supplier) {
Connection connection = null;
Boolean connAutoCommit = null;
PreparedStatement preparedStatement = null;
try {
//这里dataSource是数据源,自己业务实际写的时候需要在类里面设置好数据源这个变量
//可以通过spring的自动注入也可以通过其他方式等
connection = dataSource.getConnection();
connAutoCommit = connection.getAutoCommit();
//设置连接为手动提交
connection.setAutoCommit(false);
preparedStatement = connection.prepareStatement("select * from job_lock where job_name='create_lock' for update");
//获取锁 (如果当前事务没有提交别的线程进来会阻塞)
preparedStatement.execute();
//执行业务操作
String result = supplier.get();
logger.info("业务操作执行结果:{}", result);
} catch (Exception ex) {
logger.error(ex.toString());
} finally {
if (connection != null) {
try {
//提交事务
connection.commit();
//恢复自动提交
connection.setAutoCommit(connAutoCommit);
connection.close();
} catch (SQLException ex) {
logger.error(ex.toString());
}
}
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException ex) {
logger.error(ex.toString());
}
}
}
}
调用方法示例
代码如下(示例):
业务操作执行的时候只须记住7个字:一锁二查三更新
/**
* 业务操作示例
*/
public void create() {
createByLock(() -> {
//1.获取锁之后比如先查询数据是不是存在或者是否更新
//2.不存在或者没有更新执行相关操作
logger.info("在这里执行业务操作");
return "业务操作成功";
});
}
其他问题
由于job_lock表只有一条数据,所有调用此方法的请求都要去拿同一把锁,在并发量高或者业务操作执行比较耗时的情况下很有可能导致锁等待超时等问题,导致业务执行失败,比如业务表里不存在的数据,在保存的时候因为获取锁超时导致插入失败。
怎么解决:多增加一些锁数据或者加大Innodb最大锁等待时间。
具体方案:
1.这边建议可以给锁操作表多增加一些数据,以此减少锁的粒度。createByLock方法可以传入自己的job_name值,现在job_name默认是create_lock,锁操表的数据可以是这样:create_lock0、create_lock1、create_lock2、create_lock3、、、,选取锁的时候可以用业务上的某个字段取hash值然后再和数据库的总共锁数量做位运算或者取模运算,create_lock+取模后的值
就是作为入参传入即可。
2.如果是想通过更改Innodb引擎的最大锁等待时间,可以通过设置innodb_lock_wait_timeout
变量值来更改即可默认是50s,执行sql命令set global innodb_lock_wait_timeout=120
这样所有的连接会话的锁超时时间都是120s,但一般不采取这种方式,因为可能会导致大量的请求堆积,得不偿失,尽可能在业务代码优化逻辑处理,减少执行时间。