Database implementation of distributed lock
What is a distributed lock
In a single-instance single-process system, when multiple threads modify a shared variable at the same time, in order to ensure thread safety, it is necessary to synchronize the variable or code. This kind of synchronization operation can use synchronized and JUC packages in java. Under the explicit lock, cas+volatile to achieve.
At present, most systems are deployed in a distributed manner. Manual use such as synchronized can only ensure thread safety within a single process. Thread safety under multiple processes and multiple instances requires distributed locks.
There are currently four mainstream distributed lock solutions:
- Based on database implementation (pessimistic + optimistic)
- Based on Redis
- Based on ZooKeeper
- Based on Etcd
Pessimistic lock of database realizes distributed lock
Create table sql:
CREATE TABLE `t_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`amount` int(11) NOT NULL,
`status` int(11) NOT NULL,
`version` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
INSERT INTO `t_order` (`amount`, `status`, `version`) VALUES ('100', '1', '1');
First use sql to simulate the realization of distributed locks:
step | SessionA | SessionB |
---|---|---|
1 | begin; | begin; |
2 | select * from t_order where id=1 for update; | |
3 | select * from t_order where id=1 for update; – 阻塞 | |
4 | update t_order set status=2 where id=1 and status=1; | |
5 | commit; | Return query result |
6 | update t_order set status=2 where id=1 and status=1; – the status has changed but the update is not successful | |
7 | commit; |
Description:
- Client A and client B execute the first two rows of sql at the same time, client A returns data, and client B blocks waiting for the row lock to be acquired.
- Client A executes the next two rows of SQL, commits the transaction, and client B obtains the row lock and immediately returns the data.
- Client B executes the next two lines of SQL, commits the transaction, and releases the row lock.
Note that the
for update
statement must take the primary key index, otherwise the entire table will be locked if the index is not taken, and gap locks will be generated if other indexes are taken, which may lock multiple records.
Java code implementation:
package com.morris.distribute.lock.database.exclusive;
import com.morris.distribute.entity.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class OrderService {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 数据库分布式锁之悲观锁
*
* @param id
*/
@Transactional
public void updateStatus(int id) {
log.info("updateStatus begin, {}", id);
Integer status = jdbcTemplate.queryForObject("select status from t_order where id=? for update", new Object[]{
id}, Integer.class);
if (Order.ORDER_STATUS_NOT_PAY == status) {
try {
// 模拟耗时操作
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
int update = jdbcTemplate.update("update t_order set status=? where id=? and status=1", new Object[]{
2, id, Order.ORDER_STATUS_NOT_PAY});
if (update > 0) {
log.info("updateStatus success, {}", id);
} else {
log.info("updateStatus failed, {}", id);
}
} else {
log.info("updateStatus status already updated, ignore this request, {}", id);
}
log.info("updateStatus end, {}", id);
}
}
Pay attention to opening the transaction @Transactional
.
Use multiple threads to simulate competing locks:
package com.morris.distribute.lock.database.exclusive;
import com.morris.distribute.config.JdbcConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.stream.IntStream;
public class Demo {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.register(JdbcConfig.class);
applicationContext.register(OrderService.class);
applicationContext.refresh();
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
IntStream.rangeClosed(1, 3).forEach((i) -> new Thread(() -> {
OrderService orderService = applicationContext.getBean(OrderService.class);
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
orderService.updateStatus(1);
}, "t" + i).start());
}
}
The results are as follows:
2020-09-16 14:16:53,248 INFO [t2] (OrderService.java:26) - updateStatus begin, 1
2020-09-16 14:16:53,248 INFO [t1] (OrderService.java:26) - updateStatus begin, 1
2020-09-16 14:16:53,248 INFO [t3] (OrderService.java:26) - updateStatus begin, 1
2020-09-16 14:16:56,289 INFO [t2] (OrderService.java:42) - updateStatus success, 1
2020-09-16 14:16:56,289 INFO [t2] (OrderService.java:49) - updateStatus end, 1
2020-09-16 14:16:56,290 INFO [t3] (OrderService.java:47) - updateStatus status already updated, ignore this request, 1
2020-09-16 14:16:56,290 INFO [t3] (OrderService.java:49) - updateStatus end, 1
2020-09-16 14:16:56,291 INFO [t1] (OrderService.java:47) - updateStatus status already updated, ignore this request, 1
2020-09-16 14:16:56,291 INFO [t1] (OrderService.java:49) - updateStatus end, 1
It can be seen from the running results that only one thread holds the lock at the same time.
Optimistic lock of database realizes distributed lock
Optimistic locking uses the version number to determine whether the record has been updated every time.
package com.morris.distribute.lock.database.share;
import com.morris.distribute.entity.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class OrderService {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 数据库分布式锁之乐观锁
*
* @param id
*/
public void updateStatus(int id) {
log.info("updateStatus begin, {}", id);
for (;;) {
// 自旋,有可能对订单做其他操作,导致version变了,所以需要自旋
Order order = jdbcTemplate.queryForObject("select status, version from t_order where id=?",
new Object[]{
id}, (rs, row) -> {
Order o = new Order();
o.setStatus(rs.getInt(1));
o.setVersion(rs.getInt(2));
return o;
});
if (Order.ORDER_STATUS_NOT_PAY == order.getStatus()) {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
int update = jdbcTemplate.update("update t_order set status=?,version=? where id=? and version=? and status=?",
new Object[]{
Order.ORDER_STATUS_PAY_SUCCESS, order.getVersion() + 1, id, order.getVersion(), Order.ORDER_STATUS_NOT_PAY});
if (update > 0) {
log.info("updateStatus success, {}", id);
break;
} else {
log.info("updateStatus failed, {}", id);
}
} else {
log.info("updateStatus status already updated, ignore this request, {}", id);
break;
}
}
log.info("updateStatus end, {}", id);
}
}
The results are as follows:
2020-09-16 14:21:08,934 INFO [t3] (OrderService.java:25) - updateStatus begin, 1
2020-09-16 14:21:08,934 INFO [t2] (OrderService.java:25) - updateStatus begin, 1
2020-09-16 14:21:08,934 INFO [t1] (OrderService.java:25) - updateStatus begin, 1
2020-09-16 14:21:12,110 INFO [t1] (OrderService.java:50) - updateStatus failed, 1
2020-09-16 14:21:12,110 INFO [t2] (OrderService.java:50) - updateStatus failed, 1
2020-09-16 14:21:12,111 INFO [t3] (OrderService.java:47) - updateStatus success, 1
2020-09-16 14:21:12,111 INFO [t3] (OrderService.java:57) - updateStatus end, 1
2020-09-16 14:21:12,117 INFO [t2] (OrderService.java:53) - updateStatus status already updated, ignore this request, 1
2020-09-16 14:21:12,117 INFO [t1] (OrderService.java:53) - updateStatus status already updated, ignore this request, 1
2020-09-16 14:21:12,117 INFO [t1] (OrderService.java:57) - updateStatus end, 1
2020-09-16 14:21:12,117 INFO [t2] (OrderService.java:57) - updateStatus end, 1
to sum up
Pessimistic lock: It will lock the entire row of records, resulting in the inability to perform other business operations on the data, and it is inefficient. If the sql is not written well, a gap lock may occur, locking multiple records, or even the entire table.
Optimistic locking: Every table needs to add a version field that has nothing to do with business.
Advantages: Directly based on the database implementation, easy to implement.
Disadvantages: IO overhead is large, the number of connections is limited, and it cannot meet the needs of high concurrency.