• 使用MySQL实现分布式锁


    image

    分布式锁开发中经常使用,在项目多节点部署或者微服务项目中,JAVA提供的线程锁已经不能满足安全的需求,需要使用全局的分布式锁来保证安全;分布式锁的实现的方式有很多种,最常见的有zookeeper,Redis,数据库等;zookeeper和redis都需要我们单独部署甚至搭建集群去提高可用性。这对于服务资源本身不够的机器来说更是雪上加霜,不过mysql这种作为一个储存功能应用,我们离不开它,所以用它来实现分布式锁,不需要额外的去维护一个应用,实现起来也比较简单,对并发不高项目而言是一种比较好的实现方式;

    • 优点:简单高效可靠
    • 缺点:并发性能较低,功能相对来说比较单一

    本次演示使用的框架为 SpringBoot+MybatisPlus

    1.创建数据库表

    这里我的主键并未使用自增,因为解锁时会利用主键去做唯一判断,而且锁在释放的时候会删除数据,并且数据库可能会做集群,使用自增主键意义不大,所以采用了雪花算法实现主键;

    CREATE TABLE `lock_info` (
      `id` bigint(20) unsigned NOT NULL,
      `expiration_time` datetime DEFAULT NULL COMMENT '过期时间',
      `status` tinyint(1) DEFAULT NULL COMMENT '锁状态,0,未锁,1,已经上锁',
      `tag` varchar(255) DEFAULT NULL COMMENT '锁的标识,如项目id',
      `create_time` datetime DEFAULT NULL,
      `update_time` datetime DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE,
      KEY `uni_tag` (`tag`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库分布式锁表';
    

    2.代码逻辑

    实体对象,id采用雪花算法

    @Data
    @TableName(value = "lock_info")
    public class LockInfo implements Serializable {
        private static final long serialVersionUID = 1L;
    
        public static final Integer LOCKED_STATUS = 1;
        public static final Integer UNLOCKED_STATUS = 0;
    
        /**
         * 最大超时时间,超过将删除
         */
        public static final Integer MAX_TIMEOUT_SECONDS = 120;
    
        @TableId(value = "id", type = IdType.ASSIGN_ID)
        private Long id;
    
        /**
         * 锁过期时间
         */
        private Date expirationTime;
    
        /**
         * 锁状态,0,未锁,1,已经上锁
         */
        private Integer status = LOCKED_STATUS;
    
        /**
         * 锁的标识,如项目id
         */
        private String tag;
    
        private Date createTime;
    
        private Date updateTime;
    }
    

    锁的数据视图对象

    public class LockVo implements Serializable {
    
        /**
         * 锁的id
         */
        private Long lockId;
    
        /**
         * 锁过期时间
         */
        private Date expirationTime;
    
        /**
         * 锁的标识,如项目id
         */
        private final String tag;
    
        public LockVo(String tag) {
            this.tag = tag;
        }
    
        public void setLockId(Long lockId) {
            this.lockId = lockId;
        }
    
        public void setExpirationTime(Date expirationTime) {
            this.expirationTime = expirationTime;
        }
    
        public Long getLockId() {
            return lockId;
        }
    
        public Date getExpirationTime() {
            return expirationTime;
        }
    
        public String getTag() {
            return tag;
        }
    
        @Override
        public String toString() {
            return "LockVo{" +
                    "lockId=" + lockId +
                    ", expirationTime=" + expirationTime +
                    ", tag='" + tag + '\'' +
                    '}';
        }
    }
    

    mapper对象

    public interface LockInfoMapper extends BaseMapper<LockInfo> {
    
    }
    

    service接口

    public interface ILockInfoService extends IService<LockInfo> {
    
        /**
         * 根据锁标识获取锁信息
         *
         * @param tag 锁标识
         * @return com.chinaunicom.deliver.api.model.eo.LockInfo
         */
        LockInfo findByTag(String tag);
    
        /**
         * 尝试获取锁
         *
         * @param lockVo         锁的数据信息
         * @param expiredSeconds 锁的过期时间(单位:秒),默认10s
         * @return boolean
         */
        boolean tryLock(LockVo lockVo, Integer expiredSeconds);
    
        /**
         * 尝试获取锁,默认锁定10秒
         *
         * @param lockVo 锁的数据信息
         * @return boolean
         */
        boolean tryLock(LockVo lockVo);
    
        /**
         * 释放锁
         *
         * @param lockVo 锁的数据对象
         */
        void unlock(LockVo lockVo);
    }
    

    service实现类

    @Service
    public class LockInfoServiceImpl extends ServiceImpl<LockInfoMapper, LockInfo> implements ILockInfoService {
    
        private static final Integer DEFAULT_EXPIRED_SECONDS = 10;
    
        @Autowired
        private PlatformTransactionManager platformTransactionManager;
    
        @Autowired
        private TransactionDefinition transactionDefinition;
    
    
        @Transactional(propagation = Propagation.NOT_SUPPORTED)
        @Override
        public boolean tryLock(LockVo lockVo, Integer expiredSeconds) {
            if (lockVo == null || StringUtils.isEmpty(lockVo.getTag())) {
                throw new NullPointerException();
            }
    
            Date now = new Date();
    
            LockInfo lock = findByTag(lockVo.getTag());
    
            TransactionStatus transaction = null;
            try {
                if (Objects.isNull(lock)) {
                    transaction = platformTransactionManager.getTransaction(transactionDefinition);
                    lock = new LockInfo(lockVo.getTag(), this.getExpiredSeconds(new Date(), expiredSeconds));
                    this.save(lock);
                    platformTransactionManager.commit(transaction);
                    lockVo.setLockId(lock.getId());
                    return true;
                } else {
                    Date expiredTime = lock.getExpirationTime();
                    if (expiredTime.before(now)) {
                        transaction = platformTransactionManager.getTransaction(transactionDefinition);
                        // 如果过期并且超过过期时间120秒之后,将删除锁数据
                        if (expiredTime.before(getExpiredSeconds(expiredTime, LockInfo.MAX_TIMEOUT_SECONDS))) {
                            this.removeById(lock.getId());
                        }
                        lock.setExpirationTime(this.getExpiredSeconds(now, expiredSeconds));
                        lock.setId(null);
                        this.save(lock);
                        platformTransactionManager.commit(transaction);
                        lockVo.setLockId(lock.getId());
                        return true;
                    }
                }
            } catch (Exception e) {
                if (transaction != null) {
                    platformTransactionManager.rollback(transaction);
                }
            }
            return false;
        }
    
        @Override
        public boolean tryLock(LockVo lockVo) {
            return this.tryLock(lockVo, DEFAULT_EXPIRED_SECONDS);
        }
    
        @Override
        @Transactional(rollbackFor = Throwable.class)
        public void unlock(LockVo lockVo) {
            if (lockVo == null || StringUtils.isEmpty(lockVo.getTag()) || lockVo.getLockId() == null) {
                throw new NullPointerException();
            }
            LockInfo info = getById(lockVo.getLockId());
            if (info == null || !lockVo.getTag().equals(info.getTag())) {
                return;
            }
            this.removeById(info.getId());
        }
    
        private Date getExpiredSeconds(Date date, Integer seconds) {
            Calendar calendar = Calendar.getInstance();
            calendar.setTime(date);
            calendar.add(Calendar.SECOND, seconds);
            return calendar.getTime();
        }
    
        @Override
        public LockInfo findByTag(String tag) {
            return this.lambdaQuery().eq(LockInfo::getTag, tag)
                    .orderByDesc(LockInfo::getExpirationTime)
                    .last("limit 1").one();
        }
    }
    
    

    1.注意事项

    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    
    • 可以看到加锁的方法上加了事务注解并且配置传播规则为Propagation.NOT_SUPPORTED(以非事务的方式运行,如果当前存在事务,则挂起当前事务),这样配置是为了使用手动事务,如果不加上该注解,SpringBoot会自动帮我们加入当前事务,这样就没办法手动提交事务;这样会导致在并发时,我们的加锁事务在会等待外部事务一起提交,在默认的隔离级别下面,其他线程的事务是没办法读取未提交的事务,也就是说我们加的锁数据没有保存进数据库,其他线程一样可以加锁,这样就导致加锁失败了;

    • 加锁的时候会查询当前锁对象tag在表中过期时间最长的那个数据,避免锁过期没有释放,一个tag对应多个值的问题。并且在解锁的时候需要用主键id和tag对应唯一值,删除了其他加了锁的数据。

    什么时候会出现删除其他锁对象的数据:当一个操作执行的时间过长,获取的锁已经过期,此时其他同样需要这个锁的任务是能够获取锁的,那么此时表中相同的tag数据至少是2条以上,如果不使用主键id和tag做唯一标识,那么在释放锁的时候就会把别的任务加的锁一起删除了,导致其他任务释放锁失败!

  • 相关阅读:
    面对缓存,有哪些问题需要思考?
    .NET 文件格式相关开源项目
    (转)谈谈用ASP.NET开发的大型网站有哪些架构方式(成本)
    (转)基于微软平台IIS/ASP.NET开发的大型网站有哪些?
    sql查询优化策略
    初入linux系统
    Npoi操作Excel
    List GroupBy真实用法,Reflection(反射)用法,Enum用法,正则,搜索下拉布局
    3.2.2.4 文本匹配锚点
    3.2.2.3 单个表达式匹配多字符
  • 原文地址:https://www.cnblogs.com/lwjQAQ/p/16145573.html
Copyright © 2020-2023  润新知