Fork me on GitHub

【分布式锁】通过MySQL数据库的表来实现-V1

一、来源

  之所以要写这篇文章是因为想对自己当前的分布式知识做一个归纳。今天就先推出一篇MySQL实现的分布式锁,后续会继续推出其他版本的分布式锁,比如通过Zookeeper、Redis实现等。

 

二、正题

  要想通过MySQL来实现分布式锁,那么必定是需要一个唯一的特性才可以实现,比如主键、唯一索引这类。因为锁是为了限制资源的同步访问,也就是一个瞬间只能有一个线程去访问该资源。分布式锁就是为了解决这个资源的访问竞争问题。

  那么,主键这个方式是不建议使用的,因为我们的锁有可能是各种字符串,虽然字符串也可以当作主键来使用,但是这样会让插入变得完全随机,进而触发页分裂。所以站在性能角度,MySQL分布式锁这一块不使用主键来实现,而采用唯一索引的方式来实现。

  直接上数据库脚本:

DROP TABLE IF EXISTS `distribute_lock_info`;

CREATE TABLE `distribute_lock_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `lock_key` varchar(100) NOT NULL COMMENT '加锁Key',
  `lock_value` varchar(100) NOT NULL COMMENT '加锁Value',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `expire_time` datetime DEFAULT NULL COMMENT '过期时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_lock_key` (`lock_key`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=21884 DEFAULT CHARSET=utf8mb4;

  这里主要的是3个字段:加锁key(唯一索引),加锁value,过期时间。id采用自增,保证顺序性。两个时间主要是作为一个补充信息,非必需字段。

  ok,那么到这里,一张基本的分布式锁的表设计就已经完成了。这里的唯一索引是实现互斥的一个重点。

  接着就开始代码的编写了,先来一份依赖文件。

    <dependencies>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>false</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <!--mybatis-generator-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.2.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>

  分布式锁有个过期时间,那么就需要定时清除这些过期的锁信息,这也是预防死锁的一个手段。所以,我们可以编写一个清除的定时任务,来帮助我们清除过期的锁信息。代码如下:

package cn.lxw.task;

import cn.lxw.configdb.DistributeLockInfoMapper;
import cn.lxw.entity.DistributeLockInfo;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

import javax.annotation.Resource;
import java.time.LocalDateTime;

@Configuration
@EnableScheduling
public class LockCleanTask {

    @Resource
    private DistributeLockInfoMapper lockInfoMapper;

    /**
     * 功能描述: <br>
     * 〈Clean the lock which is expired.〉
     * @Param: []
     * @Return: {@link Void}
     * @Author: luoxw
     * @Date: 2021/7/26 20:13
     */
    @Scheduled(cron = "0/6 * * * * *")
    public void cleanExpireLock() {
        int deleteResult = lockInfoMapper.delete(new UpdateWrapper<DistributeLockInfo>() {
            {
                le("expire_time", LocalDateTime.now());
            }
        });
        System.out.println("[LockCleanTask]The count of expired lock is " + deleteResult + "!");
    }
}

  清除任务搞定,那么我们可以开始建立数据库的分布式锁那张表对应的实体类,方便后面操作表数据。

package cn.lxw.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * 功能描述: <br>
 * 〈The entity of ditribute_lock_info table in database.〉
 * @Param:
 * @Return: {@link }
 * @Author: luoxw
 * @Date: 2021/7/26 20:19
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("distribute_lock_info")
public class DistributeLockInfo implements Serializable {

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @TableField("lock_key")
    private String lockKey;

    @TableField("lock_value")
    private String lockValue;

    @TableField("create_time")
    private LocalDateTime createTime;

    @TableField("update_time")
    private LocalDateTime updateTime;

    @TableField("expire_time")
    private LocalDateTime expireTime;

    @Override
    public String toString() {
        return "DistributeLockInfo{" +
                "id=" + id +
                ", lockKey='" + lockKey + '\'' +
                ", lockValue='" + lockValue + '\'' +
                ", createTime=" + createTime +
                ", updateTime=" + updateTime +
                ", expireTime=" + expireTime +
                '}';
    }
}

  接着,就是编写这张表对应的增删改查操作了。这里我采用的是Mybatis-Plus,这样比较快捷一些。

package cn.lxw.configdb;


import cn.lxw.entity.DistributeLockInfo;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

/**
 * 功能描述: <br>
 * 〈Judge by whether the record insert success or not to prove that lock-operation is whether success or not.So I need to define a method which can ignore insert when failed.〉
 * @Param:
 * @Return: {@link }
 * @Author: luoxw
 * @Date: 2021/7/26 20:19
 */
public interface DistributeLockInfoMapper extends BaseMapper<DistributeLockInfo> {

    int insertIgnore(DistributeLockInfo entity);

}

  这时,应该声明一个分布式锁相关的API操作,比如这些,加锁,解锁,获取锁信息等。

package cn.lxw.service;

import cn.lxw.entity.DistributeLockInfo;

import java.util.concurrent.TimeUnit;

public interface ILockService {

    /**
     * 功能描述: <br>
     * 〈Lock until success!〉
     * @Param: [lockKey, lockValue]
     * @Return: {@link Void}
     * @Author: luoxw
     * @Date: 2021/7/26 20:14
     */
    void lock(String lockKey,String lockValue);

    /**
     * 功能描述: <br>
     * 〈Lock method, return the result immediately if failed .〉
     * @Param: [lockKey, lockValue]
     * @Return: {@link boolean}
     * @Author: luoxw
     * @Date: 2021/7/26 20:14
     */
    boolean tryLock(String lockKey,String lockValue);

    /**
     * 功能描述: <br>
     * 〈Lock with a timeout param, return the result immediately if failed.If lock success and it is expired,will be freed by LockCleanTask {@link cn.lxw.task.LockCleanTask}〉
     * @Param: [lockKey, lockValue, expireTime, unit]
     * @Return: {@link boolean}
     * @Author: luoxw
     * @Date: 2021/7/26 20:14
     */
    boolean tryLock(String lockKey,String lockValue,long expireTime, TimeUnit unit);

    /**
     * 功能描述: <br>
     * 〈Unlock with lockKey & lockValue.If doesn't matched,will be lock failed.〉
     * @Param: [lockKey, lockValue]
     * @Return: {@link boolean}
     * @Author: luoxw
     * @Date: 2021/7/26 20:14
     */
    boolean unlock(String lockKey,String lockValue);

    /**
     * 功能描述: <br>
     * 〈Get lock info by lockKey!〉
     * @Param: [lockKey]
     * @Return: {@link DistributeLockInfo}
     * @Author: luoxw
     * @Date: 2021/7/26 20:14
     */
    DistributeLockInfo getLock(String lockKey);
}

   接着就是通过数据库的增删改查操作去实现这些API。

package cn.lxw.service.impl;

import cn.lxw.configdb.DistributeLockInfoMapper;
import cn.lxw.entity.DistributeLockInfo;
import cn.lxw.service.ILockService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

@Service
@Primary
public class MysqlDistributeLockServiceImpl implements ILockService {

    @Resource
    private DistributeLockInfoMapper lockInfoMapper;

    /**
     * 功能描述: <br>
     * 〈Lock until success!〉
     * @Param: [lockKey, lockValue]
     * @Return: {@link Void}
     * @Author: luoxw
     * @Date: 2021/7/26 20:14
     */
    @Override
    public void lock(String lockKey, String lockValue) {
        int insertResult = 0;
        // trying until success
        while(insertResult < 1) {
            insertResult = lockInfoMapper.insertIgnore(new DistributeLockInfo() {
                {
                    setLockKey(lockKey);
                    setLockValue(lockValue);
                }
            });
        }
    }

    /**
     * 功能描述: <br>
     * 〈Lock method, return the result immediately if failed .〉
     * @Param: [lockKey, lockValue]
     * @Return: {@link boolean}
     * @Author: luoxw
     * @Date: 2021/7/26 20:14
     */
    @Override
    public boolean tryLock(String lockKey, String lockValue) {
        int insertResult = lockInfoMapper.insertIgnore(new DistributeLockInfo() {
            {
                setLockKey(lockKey);
                setLockValue(lockValue);
            }
        });
        return insertResult == 1;
    }

    /**
     * 功能描述: <br>
     * 〈Lock with a timeout param, return the result immediately if failed.If lock success and it is expired,will be freed by LockCleanTask {@link cn.lxw.task.LockCleanTask}〉
     * @Param: [lockKey, lockValue, expireTime, unit]
     * @Return: {@link boolean}
     * @Author: luoxw
     * @Date: 2021/7/26 20:14
     */
    @Override
    public boolean tryLock(String lockKey, String lockValue, long expireTime, TimeUnit unit) {
        long expireNanos = unit.toNanos(expireTime);
        LocalDateTime expireDateTime = LocalDateTime.now().plusNanos(expireNanos);
        int insertResult = lockInfoMapper.insertIgnore(new DistributeLockInfo() {
            {
                setLockKey(lockKey);
                setLockValue(lockValue);
                setExpireTime(expireDateTime);
            }
        });
        return insertResult == 1;
    }

    /**
     * 功能描述: <br>
     * 〈Unlock with lockKey & lockValue.If doesn't matched,will be lock failed.〉
     * @Param: [lockKey, lockValue]
     * @Return: {@link boolean}
     * @Author: luoxw
     * @Date: 2021/7/26 20:14
     */
    @Override
    public boolean unlock(String lockKey, String lockValue) {
        int deleteResult = lockInfoMapper.delete(new UpdateWrapper<DistributeLockInfo>() {
            {
                eq("lock_key", lockKey);
                eq("lock_value", lockValue);

            }
        });
        return deleteResult == 1;
    }

    /**
     * 功能描述: <br>
     * 〈Get lock info by lockKey!〉
     * @Param: [lockKey]
     * @Return: {@link DistributeLockInfo}
     * @Author: luoxw
     * @Date: 2021/7/26 20:14
     */
    @Override
    public DistributeLockInfo getLock(String lockKey) {
        return lockInfoMapper.selectOne(new QueryWrapper<DistributeLockInfo>(){
            {
                eq("lock_key",lockKey);
            }
        });
    }
}

  理解起来没有那么困难,【加锁】实际就是添加一条记录,【解锁】就是删除这条记录,【获取锁信息】就是查询出这条记录,【加过期时间的锁】就是添加记录的时候多设置一个过期时间。

  这样的话,就可以进行测试工作了。测试之前,需要先准备一个锁信息的生成工具类,帮助我们生成统一格式的锁信息。主要的锁信息有:IP+节点ID+线程ID+线程名称+时间戳。这个锁信息一方面是为了解锁的时候是唯一值,不会误解掉别人的锁,还有一方面是可以提供有效的信息帮助你排查问题。

package cn.lxw.util;

/**
 * 功能描述: <br>
 * 〈A string util of lock.〉
 * @Param:
 * @Return: {@link }
 * @Author: luoxw
 * @Date: 2021/7/26 20:09
 */
public class LockInfoUtil {

    private static final String LOCAL_IP = "192.168.8.8";
    private static final String NODE_ID = "node1";
    private static final String STR_SPILT = "-";
    private static final String STR_LEFT = "[";
    private static final String STR_RIGHT = "]";
    
    /**
     * 功能描述: <br>
     * 〈Return the unique String value of lock info.〉
     * "[192.168.8.8]-[node1]-[37]-[pool-1-thread-1]-[1627301265325]"
     * @Param: []
     * @Return: {@link String}
     * @Author: luoxw
     * @Date: 2021/7/26 20:08
     */
    public static String createLockValue(){
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder
                .append(STR_LEFT)
                .append(LOCAL_IP)
                .append(STR_RIGHT)
                .append(STR_SPILT)
                .append(STR_LEFT)
                .append(NODE_ID)
                .append(STR_RIGHT)
                .append(STR_SPILT)
                .append(STR_LEFT)
                .append(Thread.currentThread().getId())
                .append(STR_RIGHT)
                .append(STR_SPILT)
                .append(STR_LEFT)
                .append(Thread.currentThread().getName())
                .append(STR_RIGHT)
                .append(STR_SPILT)
                .append(STR_LEFT)
                .append(System.currentTimeMillis())
                .append(STR_RIGHT);
        return stringBuilder.toString();
    }
}

  测试开始,我这边直接通过main函数进行测试工作。大家有觉得不妥的,可以写个test类,效果是一样的。

package cn.lxw;

import cn.lxw.entity.DistributeLockInfo;
import cn.lxw.service.ILockService;
import cn.lxw.util.LockInfoUtil;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@SpringBootApplication
@EnableTransactionManagement
@MapperScan("cn.lxw")
@EnableScheduling
public class MainApp {

    /**
     * 功能描述: <br>
     * 〈DistributeLock testing start here!〉
     *
     * @Param: [args]
     * @Return: {@link Void}
     * @Author: luoxw
     * @Date: 2021/7/26 18:20
     */
    public static void main(String[] args) {
        // run the springboot app
        ConfigurableApplicationContext context = SpringApplication.run(MainApp.class, args);
        // define some lock infomation
        final String lockKey = "lock_test";
        ILockService lockService = context.getBean(ILockService.class);
        // create a ThreadPoolExecutor
        ThreadPoolExecutor tpe = new ThreadPoolExecutor(5,
                5,
                60,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(10));
        // execute the simulator
        for (int i = 0; i < 3; i++) {
            tpe.execute(() -> {
                while (true) {
                    // get the unique lock value of current thread
                    String lockValue = LockInfoUtil.createLockValue();
                    // start lock the lockKey
                    boolean tryLockResult = lockService.tryLock(lockKey, lockValue, 10L, TimeUnit.SECONDS);
                    // get the most new lock info with lockKey
                    DistributeLockInfo currentLockInfo = lockService.getLock(lockKey);
                    System.out.println("[LockThread]Thread[" + Thread.currentThread().getId() + "] lock result:" + tryLockResult + ",current lock info:" + (currentLockInfo==null?"null":currentLockInfo.toString()));
                    // here do some business opearation
                    try {
                        TimeUnit.SECONDS.sleep((int) (Math.random() * 10));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // continue to fight for lock if failed
                    if(!tryLockResult){
                        continue;
                    }
                    // start unlock the lockKey with lockKey & lockValue
                    lockService.unlock(lockKey, lockValue);
                }
            });
        }
    }
}

 查看日志,是否满足分布式锁的要求:同一个瞬间,必然只有一个线程可以争抢到对应资源的锁。

2021-07-27 20:33:40.764  INFO 14128 --- [pool-1-thread-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2021-07-27 20:33:40.972  INFO 14128 --- [pool-1-thread-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
[LockThread]Thread[39] lock result:false,current lock info:DistributeLockInfo{id=22354, lockKey='lock_test', lockValue='[192.168.8.8]-[node1]-[38]-[pool-1-thread-2]-[1627389195666]', createTime=2021-07-27T20:33:15, updateTime=2021-07-27T20:33:15, expireTime=2021-07-27T20:33:26}
[LockThread]Thread[37] lock result:false,current lock info:DistributeLockInfo{id=22354, lockKey='lock_test', lockValue='[192.168.8.8]-[node1]-[38]-[pool-1-thread-2]-[1627389195666]', createTime=2021-07-27T20:33:15, updateTime=2021-07-27T20:33:15, expireTime=2021-07-27T20:33:26}
[LockThread]Thread[38] lock result:false,current lock info:DistributeLockInfo{id=22354, lockKey='lock_test', lockValue='[192.168.8.8]-[node1]-[38]-[pool-1-thread-2]-[1627389195666]', createTime=2021-07-27T20:33:15, updateTime=2021-07-27T20:33:15, expireTime=2021-07-27T20:33:26}
[LockThread]Thread[38] lock result:false,current lock info:DistributeLockInfo{id=22354, lockKey='lock_test', lockValue='[192.168.8.8]-[node1]-[38]-[pool-1-thread-2]-[1627389195666]', createTime=2021-07-27T20:33:15, updateTime=2021-07-27T20:33:15, expireTime=2021-07-27T20:33:26}
[LockCleanTask]The count of expired lock is 1!
[LockThread]Thread[37] lock result:true,current lock info:DistributeLockInfo{id=22362, lockKey='lock_test', lockValue='[192.168.8.8]-[node1]-[37]-[pool-1-thread-1]-[1627389224033]', createTime=2021-07-27T20:33:44, updateTime=2021-07-27T20:33:44, expireTime=2021-07-27T20:33:54}
[LockThread]Thread[38] lock result:false,current lock info:DistributeLockInfo{id=22362, lockKey='lock_test', lockValue='[192.168.8.8]-[node1]-[37]-[pool-1-thread-1]-[1627389224033]', createTime=2021-07-27T20:33:44, updateTime=2021-07-27T20:33:44, expireTime=2021-07-27T20:33:54}
[LockThread]Thread[37] lock result:true,current lock info:DistributeLockInfo{id=22364, lockKey='lock_test', lockValue='[192.168.8.8]-[node1]-[37]-[pool-1-thread-1]-[1627389224067]', createTime=2021-07-27T20:33:44, updateTime=2021-07-27T20:33:44, expireTime=2021-07-27T20:33:54}
[LockThread]Thread[37] lock result:true,current lock info:DistributeLockInfo{id=22365, lockKey='lock_test', lockValue='[192.168.8.8]-[node1]-[37]-[pool-1-thread-1]-[1627389225085]', createTime=2021-07-27T20:33:45, updateTime=2021-07-27T20:33:45, expireTime=2021-07-27T20:33:55}
[LockThread]Thread[39] lock result:false,current lock info:DistributeLockInfo{id=22365, lockKey='lock_test', lockValue='[192.168.8.8]-[node1]-[37]-[pool-1-thread-1]-[1627389225085]', createTime=2021-07-27T20:33:45, updateTime=2021-07-27T20:33:45, expireTime=2021-07-27T20:33:55}
[LockCleanTask]The count of expired lock is 0!
[LockThread]Thread[38] lock result:false,current lock info:DistributeLockInfo{id=22365, lockKey='lock_test', lockValue='[192.168.8.8]-[node1]-[37]-[pool-1-thread-1]-[1627389225085]', createTime=2021-07-27T20:33:45, updateTime=2021-07-27T20:33:45, expireTime=2021-07-27T20:33:55}
[LockThread]Thread[38] lock result:false,current lock info:DistributeLockInfo{id=22365, lockKey='lock_test', lockValue='[192.168.8.8]-[node1]-[37]-[pool-1-thread-1]-[1627389225085]', createTime=2021-07-27T20:33:45, updateTime=2021-07-27T20:33:45, expireTime=2021-07-27T20:33:55}
[LockThread]Thread[39] lock result:false,current lock info:DistributeLockInfo{id=22365, lockKey='lock_test', lockValue='[192.168.8.8]-[node1]-[37]-[pool-1-thread-1]-[1627389225085]', createTime=2021-07-27T20:33:45, updateTime=2021-07-27T20:33:45, expireTime=2021-07-27T20:33:55}
[LockThread]Thread[37] lock result:true,current lock info:DistributeLockInfo{id=22370, lockKey='lock_test', lockValue='[192.168.8.8]-[node1]-[37]-[pool-1-thread-1]-[1627389232106]', createTime=2021-07-27T20:33:52, updateTime=2021-07-27T20:33:52, expireTime=2021-07-27T20:34:02}
[LockCleanTask]The count of expired lock is 0!
[LockThread]Thread[38] lock result:false,current lock info:DistributeLockInfo{id=22370, lockKey='lock_test', lockValue='[192.168.8.8]-[node1]-[37]-[pool-1-thread-1]-[1627389232106]', createTime=2021-07-27T20:33:52, updateTime=2021-07-27T20:33:52, expireTime=2021-07-27T20:34:02}
[LockThread]Thread[38] lock result:false,current lock info:DistributeLockInfo{id=22370, lockKey='lock_test', lockValue='[192.168.8.8]-[node1]-[37]-[pool-1-thread-1]-[1627389232106]', createTime=2021-07-27T20:33:52, updateTime=2021-07-27T20:33:52, expireTime=2021-07-27T20:34:02}
[LockThread]Thread[38] lock result:false,current lock info:DistributeLockInfo{id=22370, lockKey='lock_test', lockValue='[192.168.8.8]-[node1]-[37]-[pool-1-thread-1]-[1627389232106]', createTime=2021-07-27T20:33:52, updateTime=2021-07-27T20:33:52, expireTime=2021-07-27T20:34:02}

  大家可以自己按照这个思路去调整一下代码验证一下。

三、结论

  结论是通过MySQL我们是可以实现分布式锁的,而且十分简单,一张表,一点代码就可以搞定。但是,它的本质是通过数据库的锁来实现的,所以这么做会增加数据库的负担。而且数据库实现的锁性能是有瓶颈的,不能满足性能高的业务场景。所以,性能低的业务下玩玩是可以的。

  

  OK,本篇MySQL实现分布式锁介绍结束,欢迎关注下一篇分布式锁V2的实现。

 

  项目地址:https://github.com/telephone6/java-collection/tree/main/distribute/distribute-lock/mysql-lock

posted @ 2021-07-27 20:49  罗西施  阅读(514)  评论(0编辑  收藏  举报