1.使用数据库进行分布式加锁
行锁以mysql为例,进行举例 :
1.1 for update
在mysql中使用for update获得行锁。
for update是一种行级锁,又叫排它锁,一旦用户对某个行施加了行级锁,则该用户可以更新也可以查询也可以更新被加锁的数据行,其他用户只能查询,不能更新被加锁的数据行,如果其他用户想更新该表中的数据行,则也必须对表施加行级锁。
释放行级:
1.执行提交commit语句.
2.退出数据库
3.程序停止运行.
通常情况下,select语句是不会对数据加锁的,妨碍影响其他DDL或者DML操作。
select…for update是我们经常使用的手工加锁语句。如果查询条件带有主键,会锁行数据,没有则进行锁表操作。针对innodb引擎。
举个例子:
锁行示例:
drop table if exists data_user;
create table data_user(
id bigint(15) not null primary key,
name varchar(50),
code varchar(50)
)engine=innodb comment='测试用户表';
insert into data_user (id,name,code) values(1,'name1','code1');
insert into data_user (id,name,code) values(2,'name2','code2');
insert into data_user (id,name,code) values(3,'name3','code3');
在命令行1中输入:
mysql> begin;
select * from data_user where id = 2 for update;
Query OK, 0 rows affected (0.00 sec)
+----+-------+-------+
| id | name | code |
+----+-------+-------+
| 2 | name2 | code2 |
+----+-------+-------+
1 row in set (0.01 sec)
mysql>
在命令行2中输入
mysql> begin;
select * from data_user where id = 2 for update;
Query OK, 0 rows affected (0.00 sec)
会发现SQL卡住了,这就是锁行了.
然后再命令行1中操作
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
这时命令行2中就可以看到:
mysql> begin;
select * from data_user where id = 2 for update;
Query OK, 0 rows affected (0.00 sec)
+----+-------+-------+
| id | name | code |
+----+-------+-------+
| 2 | name2 | code2 |
+----+-------+-------+
1 row in set (46.07 sec)
之前卡住的信息又恢复了。
这就是行锁最简单的一个演示,如果在使用for update不加主键那将是锁表。
for update实现分布式锁
使用for update实现一个分布式场景下的锁:
先给出数据库的SQL:
drop table if exists goods;
create table goods(
id bigint(15) not null primary key,
name varchar(50),
dataNum int(5)
)engine=innodb comment='商品信息';
insert into goods values(1,'mac',800);
先看数据库操作:
public class JdbcOperator {
private static final String DRIVER_CLASS = "com.mysql.jdbc.Driver";
private static final String MYSQL_URL =
"jdbc:mysql://localhost:3306/zsc?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true";
private static final String USER_NAME = "root";
private static final String PASSWORD = "123456";
static {
// 加载驱动
try {
Class.forName(DRIVER_CLASS);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* 批量SQL操作
*
* @param sqlList
* @return
*/
public static int executeSql(List<String> sqlList) {
int updateRsp = 0;
try (Connection connection = DriverManager.getConnection(MYSQL_URL, USER_NAME, PASSWORD); ) {
// 关闭自动提交
connection.setAutoCommit(false);
for (int i = 0; i < sqlList.size(); i++) {
runStatementSql(sqlList.get(i), connection);
}
// 完成后提交事务
connection.commit();
} catch (SQLException e) {
e.printStackTrace();
}
return updateRsp;
}
private static void runStatementSql(String sql, Connection connection) {
try (PreparedStatement stat = connection.prepareStatement(sql)) {
stat.execute();
System.out.println(Thread.currentThread().getId() + "," + sql);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
调用进行扣减库存操作:
public class Goods {
public Goods() {}
/** 商品的库存扣减操作 */
public void minusGoods(int num) {
List<String> sqlList = new ArrayList<>();
sqlList.add("begin;");
sqlList.add("select * from goods where id = 1 for update;");
sqlList.add("update goods set dataNum = dataNum-" + num + " where id = 1;");
// 扣减库存操作
JdbcOperator.executeSql(sqlList);
}
}
订单信息:
public class Orders {
/** 商品服务 */
private Goods goods;
public Orders(Goods goods) {
this.goods = goods;
}
/**
* 创建订单
*
* @return
*/
public boolean createOrder(int num) {
// 执行扣减库存操作
goods.minusGoods(num);
return true;
}
}
测试分布式场景下的锁:
public class TestOrdersLock {
@Test
public void useOrder() throws InterruptedException {
int orderNumSum = 800;
Goods goods = new Goods();
// 并发进行下单操作
int maxOrder = 4;
int count = 0;
for (int i = 0; i < orderNumSum / maxOrder; i++) {
CountDownLatch startLatch = new CountDownLatch(maxOrder);
for (int j = 0; j < maxOrder; j++) {
TaskThreadPool.INSTANCE.submit(
() -> {
startLatch.countDown();
Orders instance = new Orders(goods);
instance.createOrder(1);
});
count++;
}
// 执行等待结果
try {
startLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("结束,共运行:" + count + "次");
TaskThreadPool.INSTANCE.shutdown();
Thread.sleep(500);
System.out.println("shutdown status:" + TaskThreadPool.INSTANCE.getPool().isShutdown());
}
}
单元测试的结果在控制台可以看到:
15,begin;
12,begin;
13,begin;
16,begin;
19,begin;
17,begin;
14,begin;
18,begin;
13,select * from goods where id = 1 for update;
13,update goods set dataNum = dataNum-1 where id = 1;
12,select * from goods where id = 1 for update;
12,update goods set dataNum = dataNum-1 where id = 1;
15,select * from goods where id = 1 for update;
15,update goods set dataNum = dataNum-1 where id = 1;
19,select * from goods where id = 1 for update;
19,update goods set dataNum = dataNum-1 where id = 1;
16,select * from goods where id = 1 for update;
16,update goods set dataNum = dataNum-1 where id = 1;
17,select * from goods where id = 1 for update;
17,update goods set dataNum = dataNum-1 where id = 1;
14,select * from goods where id = 1 for update;
14,update goods set dataNum = dataNum-1 where id = 1;
18,select * from goods where id = 1 for update;
18,update goods set dataNum = dataNum-1 where id = 1;
......
13,begin;
13,select * from goods where id = 1 for update;
13,update goods set dataNum = dataNum-1 where id = 1;
结束,共运行:800次
17,begin;
17,select * from goods where id = 1 for update;
17,update goods set dataNum = dataNum-1 where id = 1;
shutdown status:true
检查数据库的结果
mysql> select * from goods;
+----+------+---------+
| id | name | dataNum |
+----+------+---------+
| 1 | mac | 0 |
+----+------+---------+
1 row in set (0.02 sec)
劣势
讲完了实现,再来说说这个实现的问题吧。
- 锁操作过程中如果没有正确的提交,那将导致数据库表被锁表,其他进程将不能操作锁定的表,直白点,将直接引发生产事故。
- for update将占用数据库连接,如果在操作过程中使用时间过长,就容易造成锁超时。
- 如果并发量过大,会造成数据库直接宕机。
所以此分布式锁使用务必小心,不推荐使用。
适用的场景
使用数据库的for update的加锁机制适用于并发不是特别大的场景,这是因为数据库所能承载的并发是较小的,由于每个锁就占用一个数据库连接,如果操作时间过长,将可能导致锁超时,使用过程也务必要小心,如果某个线程使用了for update,那么在事务运行过程宕机了,那么这个锁将得不到释放,需要手动处理。如果要给出一个参考的数据的话,此并发在几十。