T O P

[资源分享]     【分布式锁】通过MySQL数据库的表来实现-V1

  • By - 楼主

  • 2021-07-27 22:00:34
  • 一、来源

      之所以要写这篇文章是因为想对自己当前的分布式知识做一个归纳。今天就先推出一篇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

    本帖子中包含资源

    您需要 登录 才可以下载,没有帐号?立即注册