Author | tech-bus.71
Source | Programmer Bus
Speaking of locks, in our usual work, we mainly use the synchronized keyword or some related class libraries to achieve synchronization, but this is all based on stand-alone applications. When our application is deployed with multiple instances, this time Distributed locks need to be used. The commonly used distributed locks are mainly distributed locks based on redis, distributed locks based on zookeeper, and distributed locks based on the base database. The first two are mainly based on the characteristics of middleware. Let's take a look at the implementation of database-based distributed locks, which are more suitable in some scenarios with low concurrency.
First, you need to create a data table in the database, and the relevant fields are as follows:
CREATE TABLE IF NOT EXISTS `lock_tbl`(
`lock_id` INT NOT NULL, -- 主键且主要字段不可少
`des_one` VARCHAR(20), -- 可有可无
`des_two` VARCHAR(20), -- 可有可无
PRIMARY KEY ( `lock_id` )
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
Then we use a single instance application to write an interface to buy a product in the table. The general idea is: read the inventory, reduce the inventory by one, write back to the database, and return successfully. The core code is as follows:
public class StockServiceImpl implements StockService{
@Autowired
StockMapper stockMapper;
@Override
public Stock selectByPrimaryKey(Integer goodsId) {
return stockMapper.selectByPrimaryKey(goodsId);
}
// 加锁也只能保证单个实例线程安全性
public synchronized void byGoods() throws InterruptedException {
// 这里写死,数据库里就一条记录且ID为1,拿到数据
Stock stock = selectByPrimaryKey(1);
// 获取到商品的库存
Long goodsStock = stock.getGoodsStock();
// 减库存
goodsStock -= 1;
stock.setGoodsStock(goodsStock);
// 为了将问题放大这里睡上几秒 拉长查库存和更新库存的之间的时间间隔
Thread.sleep(3000);
// 更新
updateByPrimaryKeySelective(stock);
// 输出
System.out.println("更新后库存为:" + goodsStock);
}
@Override
public int updateByPrimaryKeySelective(Stock record) {
return stockMapper.updateByPrimaryKeySelective(record);
}
}
After adding a synchronized in a single instance, it is completely normal to reduce the inventory. Then we start two instances and use postman to test the interface. The following situation occurs:
It can be seen from the screenshot that the above program has been oversold. Next, we will transform it and use the lock at the database level. We know that inserting two pieces of data with the same primary key into a table can only succeed in one, because the primary key is binding, so use this feature , when we insert into the database successfully, it means that the lock is acquired, so as to run our business code. When our business code is finished running, we delete the record in the database, which means that the lock is released, so that other threads That is, there is a chance to obtain the lock and then run the business code, so that even if two instances are running, only one thread can run the business code at the same time, and there will be no oversold situation. The codes for locking and unlocking are given below:
// 上锁。由于上锁失败的话会直接返回失败,并不会再次获取
// 是非阻塞的,这里利用循环实现阻塞。
@Override
public boolean tryLock() {
// 这里的Lock就是简单的一个POJO对象映射到数据库中一张表的字段
Lock lock = new Lock();
lock.setLockId(1);
// 通过while循环来实现阻塞
while (true) {
try {
// 首先查询一下主键为1的数据是否存在,如果存在则说明锁已经被占用了
if (lockMapper.selectByPrimaryKey(1) == null) {
// 不存在则尝试加锁即向数据库中插入数据
int i = lockMapper.insert(lock);
if (i == 1) {
return true;
}
}
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
// 解锁代码
@Override
public void unLock() {
deleteByPrimaryKey(1);
}
The code of the purchased product at the service layer is locked
// 买商品
public void byWithLock() throws InterruptedException {
// 上锁
lockService.tryLock();
// 业务代码
byGoods();
// 释放锁并跳出循环
lockService.unLock();
}
Code for the controller layer
@RestController
public class LoadBalance {
@Autowired
StockServiceImpl stockService;
@RequestMapping("/balance")
public String balance() {
try {
stockService.byWithLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
return "success";
}
}
Start the program again, use postman to do a simple pressure test, and find that the inventory reduction has been carried out normally. The result is shown below
existing problems
If one instance crashes after getting the lock, and the lock is not released in time, other instances will never be able to acquire the lock.
Non-reentrant, after an instance gets the lock, it will fail when trying to acquire the lock again
How to solve
For the problem that the lock cannot be released due to instance downtime, you can insert a current timestamp into the database when inserting data, then start a scheduled task, scan the table regularly, and set a lock timeout (the The timeout time must be greater than the normal interface call time), and the timeout record will be deleted.
For non-reentrant, you can add instance and thread-related information when inserting data into the table, judge when acquiring the lock, and acquire the lock directly if it matches.
pessimistic lock
A simple understanding of pessimistic locking is that in any case, it is pessimistic that when a critical resource is requested, it will conflict with other threads, so every time a pessimistic lock is added, which is strongly invasive and exclusive. The lock added in the above example is a pessimistic lock, that is, the lock is taken first and then accessed. The pessimistic lock that comes with MySql is For Update. Using For Update can display the increase of row locks, but the pessimistic lock will increase the database overhead and increase the deadlock. risk of locking.
optimistic locking
A simple understanding of optimistic locking is that every time a thread requests a critical resource, it thinks that no other thread will compete with it, and only competes when data is submitted. When detecting data conflicts, it does not rely on the locking mechanism of the database itself and does not affect the request. performance. In the above example, we can add a Version version number to the database table. For the data to be modified, first find out the version number of the modified Version from the database, and then modify it together with the version number when modifying.
SELECT VERSION FROM TABLE_A -- 假设这里查出来version的值是OldVersion
UPDATE TABLE_A SET COUNT = COUNT -1, VERSION = VERSION + 1 WHERE VERSION = OldVersion
Summarize
When concurrency is not particularly high, you can consider using database-based distributed locks, and try to use optimistic locking to improve application throughput.
Recommended in the past
Gartner Announces Five Technology Trends for the Automotive Industry in 2022
Stop using Redis List to implement message queues, Stream is designed for queues
How OpenStack can be upgraded across versions
point to share
Favorites
Like
click to watch