分布式锁开发中经常使用,在项目多节点部署或者微服务项目中,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做唯一标识,那么在释放锁的时候就会把别的任务加的锁一起删除了,导致其他任务释放锁失败!

使用MySQL实现分布式锁的更多相关文章

  1. Mysql高手系列 - 第26篇:聊聊如何使用mysql实现分布式锁

    Mysql系列的目标是:通过这个系列从入门到全面掌握一个高级开发所需要的全部技能. 欢迎大家加我微信itsoku一起交流java.算法.数据库相关技术. 这是Mysql系列第26篇. 本篇我们使用my ...

  2. 基于Mysql实现分布式锁

    一.分布式锁要解决的问题 可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行. 这把锁要是一把可重入锁(避免死锁) 这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条 ...

  3. 分布式锁(redis/mysql)

    单台机器所能承载的量是有限的,用户的量级上万,基本上服务都会做分布式集群部署.很多时候,会遇到对同一资源的方法.这时候就需要锁,如果是单机版的,可以利用java等语言自带的并发同步处理.如果是多台机器 ...

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

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

  5. Redis的“假事务”与分布式锁

    关注公众号:CoderBuff,回复"redis"获取<Redis5.x入门教程>完整版PDF. <Redis5.x入门教程>目录 第一章 · 准备工作 第 ...

  6. 手撕redis分布式锁,隔壁张小帅都看懂了!

    前言 上一篇老猫和小伙伴们分享了为什么要使用分布式锁以及分布式锁的实现思路原理,目前我们主要采用第三方的组件作为分布式锁的工具.上一篇运用了Mysql中的select ...for update实现了 ...

  7. 死磕 java同步系列之mysql分布式锁

    问题 (1)什么是分布式锁? (2)为什么需要分布式锁? (3)mysql如何实现分布式锁? (4)mysql分布式锁的优点和缺点? 简介 随着并发量的不断增加,单机的服务迟早要向多节点或者微服务进化 ...

  8. 二十六、聊聊mysql如何实现分布式锁

    分布式锁的功能 分布式锁使用者位于不同的机器中,锁获取成功之后,才可以对共享资源进行操作 锁具有重入的功能:即一个使用者可以多次获取某个锁 获取锁有超时的功能:即在指定的时间内去尝试获取锁,超过了超时 ...

  9. 论JAVA实现MYSQL 行级锁(分布式锁实现方案一)

    @Override @Transactional public String getCustomerId() { // return String.valueOf(getSequenceId(SEQ_ ...

  10. 【分布式锁的演化】“超卖场景”,MySQL分布式锁篇

    前言 之前的文章中通过电商场景中秒杀的例子和大家分享了单体架构中锁的使用方式,但是现在很多应用系统都是相当庞大的,很多应用系统都是微服务的架构体系,那么在这种跨jvm的场景下,我们又该如何去解决并发. ...

随机推荐

  1. 《最新出炉》系列入门篇-Python+Playwright自动化测试-49-Route类拦截修改请求-下篇

    1.简介 在日常工作和学习中,自动化测试的时候:在加载页面时,可能页面出现很多不是很重要或者不是我们所关注的,这个时候我们就可以选择不加载这些内容,以提高页面加载速度,节省资源.例如:可能页面上图片比 ...

  2. YUV图片旋转

    1.从H264视频流中解析出来的图片格式是YUV YUV旋转可以选择手工旋转,也可以利用libYUV,libYUV开启NEON指令之后可以加快处理速度 在iOS系统上,利用Accelcerate库进行 ...

  3. Python爬虫 | 批量爬取今日头条街拍美图

    01 前言 上篇文章我们爬取了今日头条街拍美图,心情相当愉悦,今天这篇文章我们使用Selenium来爬取当当网的畅销图书排行.正所谓书中自有黄金屋,书中自有颜如玉,我们通过读书学习来提高自身的才华,自 ...

  4. 使用nvm安装以及管理多版本node教程

    安装nvm.node.npm 下载nvm安装包,推荐使用1.1.7,我个人使用1.1.8会有中文乱码的报错 点击exe文件,注意修改nvm的安装根目录以及node的安装根目录,后者是以后管理多版本no ...

  5. TypeScript keyof

    keyof 是 TypeScript 中的一个关键字,用于获取一个类型的所有键(属性名)构成的联合类型.它主要用于在类型系统中引用对象类型的键. 以下是一些 keyof 的用法和示例: 1. 获取对象 ...

  6. SELinux(一) 简介

    首发公号:Rand_cs 前段时间的工作遇到了一些关于 SELinux 的问题,初次接触不熟悉此概念,导致当时配置策略时束手束脚,焦头烂额,为此去系统的学习了下 SELinux 的东西.聊 SELin ...

  7. Prime Solutions

    Prime Solutions 以下是一段中学时代的惨痛回忆-每当学到排列组合的单元时,最痛苦的不是分析题目,也不是带错公式或计算错误,而是所谓的「苦工题」,以下这题是个例子:给定正整数N与S,求出方 ...

  8. python 方法调用另一个方法报错,捕获的异常只有message,优化为trackback捕获详细的报错信息

    A方法加了try...expect... B方法也加了try....expect... B方法调用了A方法,A方法查找元素,找不到报超时异常,实际B捕获到的异常,只有message\n,没有办法看出是 ...

  9. SQLServer统计采集数据库相关信息

    在MS Sql Server中可以能过以下的方法查询出磁盘空间的使用情况及各数据库数据文件及日志文件的大小及使用利用率: 1.查询各个磁盘分区的剩余空间:Exec master.dbo.xp_fixe ...

  10. AT_joisc2019_j 题解

    先考虑这个式子: \[\sum_{j=1}^{M} |C_{k_{j}} - C_{k_{j+1}}| \] 一定是在 \(C\) 有序时取到,具体证明很简单各位读者自己证明. 那么现在式子变成: \ ...