- 作者:老汪软件技巧
- 发表时间:2024-08-29 11:02
- 浏览量:
引言
在前面一文《花2万块买来的BUG!你的系统是不是也埋着相同的BUG?》中因为发短信接口未使用分布式锁导致短信接口在高并发情况下可以被超发,类似的情况在JAVA开发中是非常常见的,最简单解决方案就是给接口添加分布式锁,在上文中也提到了常见实现分布式锁的三种方式,基于数据库、基于Redis、基于Zookeeper,上文中使用了基于Redis的SETNX来实现的,如果项目中没有引用Redis或者Zookeeper也可以基于数据库来实现一个简单的分布式锁了,基于数据库实现分布式锁主要有三种方式,基于数据库唯一索引、基于数据库悲观锁和基于数据库乐观锁,接下来将详细介绍这三种方式实现的具体步骤。
二、基于唯一索引2.1、实现思路
我们知道数据库表中的唯一索引可以确保一张表中相同数据只能插入一次,基于这条规则我们可以创建一张表,然后给锁名字段创建一个唯一索引,当并发插入时如果插入成功就获取到锁,插入失败就未获取到锁,释放锁就是把数据这条数据删除。
创建union_key_lock表:
CREATE TABLE `union_key_lock` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`lock_name` varchar(255) NOT NULL DEFAULT '',
`expire_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '锁过期时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_key_name` (`lock_name`) USING BTREE
) ENGINE=InnoDB COMMENT='唯一键实现分布式锁'
在union_key_lock表中将锁名字段lock_name添加唯一索引,expire_at为锁过期时间,可以在Mysql中或者项目中添加定时任务删除expire_at2.2、代码实现
基于数据库唯一索引代码实现起来是非常简单的,有两个方法,第一个方法是lock(),接收一个锁名参数和锁超时时间参数,第二个方法是unLock()释放锁方法:
@Resource
private JdbcTemplate jdbcTemplate;
@Override
public Boolean lock(String lockName, Integer second) {
try {
String sql = String.format("insert into union_key_lock (lock_name, expire_at) value ('%s','%s')", lockName, DateUtil.formatLocalDateTime(LocalDateTime.now().plusSeconds(second)));
jdbcTemplate.execute(sql);
return true;
} catch (Exception e) {
return false;
}
}
@Override
public void unLock(String lockName) {
String sql = String.format("delete from union_key_lock where lock_name='%s';", lockName);
jdbcTemplate.execute(sql);
}
2.3 、测试代码
我们编写多线程代码压测是否成功实现分布式锁:
@Resource
private UnionKeyLockImpl unionKeyLock;
@Test
void testunionKeyLock() {
String lockName = "赵侠客";
IntStream.range(1, 5).parallel().forEach(x -> {
try {
if (unionKeyLock.lock(lockName, 5)) {
log.info("get lock success");
} else {
log.warn("get lock error");
}
} finally {
unionKeyLock.unLock(lockName);
}
});
}
2.4、小结
基于数据库的分布式锁优点包括实现简单、事务支持、无需额外组件和持久化特性。缺点则包括性能较低、锁粒度受限、死锁风险、资源开销较大以及锁释放问题。
优点:
缺点:
三、基于悲观锁3.1 、实现思路
基于数据库悲观锁实现分布式锁依赖于数据库的行级锁机制,通过 SELECT ... FOR UPDATE 等操作显式地锁定数据库中的某一行,来达到获取分布式锁的目的。在这种方式下,其他事务在尝试修改这行数据时会被阻塞,直到锁被释放。
创建一张锁表,记录需要锁定的资源:
CREATE TABLE `select_for_update_lock` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`lock_name` varchar(255) NOT NULL DEFAULT '',
`lock_status` int(255) NOT NULL DEFAULT '0' COMMENT '0--正常 1--被锁',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_key_name` (`lock_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8 COMMENT='悲观锁'
当获取锁时使用FOR UPDATE阻塞其它查询,任务执行完成后COMMIT提交事务后自动释放锁,在调用锁之前要将锁名信息添加到表中。
BEGIN;
SELECT * FROM select_for_update_lock WHERE lock_name = 'my_lock' AND lock_status = 0 FOR UPDATE;
...执行任务
COMMIT;
其他事务在尝试执行 SELECT ... FOR UPDATE 时会被阻塞,直到COMMIT后锁被释放。
3.2、代码实现
基于数据库悲观锁使用分布式锁代码也是非常简单的,只有一个方法:
@Resource
private JdbcTemplate jdbcTemplate;
@Resource
private PlatformTransactionManager platformTransactionManager;
public void lock(String lockName, Runnable runnable) {
// 定义事务
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// 开启事务
TransactionStatus status = platformTransactionManager.getTransaction(def);
try {
// 尝试获取锁
jdbcTemplate.queryForObject("SELECT lock_name FROM select_for_update_lock WHERE lock_name = ? FOR UPDATE", String.class, lockName);
runnable.run();;
} catch (Exception e) {
// 出现异常时回滚事务
platformTransactionManager.rollback(status);
throw e;
}finally {
// 提交事务,释放锁
platformTransactionManager.commit(status);
}
}
在代码lock()方法中使用PlatformTransactionManager手动开启使用,在finally中手动提交事务
3.3、测试代码
@Resource
private SelectForUpdateLockImpl selectForUpdateLock;
@Test
void testSelectForUpdateLock() {
String lockName = "赵侠客";
IntStream.range(1, 10).parallel().forEach(x -> {
try {
selectForUpdateLock.lock(lockName, () -> {
log.info("get {} lock success", lockName);
});
} catch (Exception e) {
log.error("get {} lock error", lockName);
}
});
}
3.4、小结
基于数据库悲观锁的分布式锁有以下优缺点:优点:
缺点:
四、基于乐观锁4.1 、实现思路
基于数据库的乐观锁实现分布式锁通常利用唯一索引或版本号机制来确保在高并发场景下的锁定操作。乐观锁适合在冲突较少的场景中使用,依赖于更新时的数据状态一致性判断。以下是一个基于数据库乐观锁的分布式锁实现示例。创建一张optimistic_lock表:
CREATE TABLE `optimistic_lock` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`lock_name` varchar(50) DEFAULT NULL,
`expire_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '锁过期时间',
`lock_status` int(255) NOT NULL DEFAULT '0' COMMENT '0--正常 1--被锁',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_lock_name` (`lock_name`) USING BTREE
) ENGINE = InnoDB DEFAULT CHARSET=utf8 COMMENT='乐观锁实现分布式锁'
在锁名字段上增加唯一索引,其实现思路是通过数据库的更新数据是否成功能判断是否获取到锁,所以我们要提前将锁名任务添加到表中,expire_at为锁过期时间,防止未及时释放导致死锁,这里可以通过定时任务删除过期的锁。
4.2 、代码实现
基于数据库乐观锁实现分布锁主要有两个方法:
@Resource
private JdbcTemplate jdbcTemplate;
public boolean lock(String lockName) {
try {
String sql = String.format("update optimistic_lock set lock_status=1, expire_at = NOW() + INTERVAL 1 MINUTE where lock_name ='%s' and lock_status = 0 ;", lockName);
return jdbcTemplate.update(sql) == 1;
} catch (Exception e) {
return false;
}
}
public void unLock(String lockName) {
String sql = String.format("update optimistic_lock set lock_status=0 ,expire_at=now() where lock_name='%s' ;", lockName);
jdbcTemplate.update(sql);
}
4.3 、测试代码
@Resource
private OptimisticLock optimisticLock;
@Test
void testOptimisticLock() {
String lockName = "赵侠客";
IntStream.range(1, 10).parallel().forEach(x -> {
try {
if (optimisticLock.lock(lockName)) {
log.info("get lock success");
} else {
log.warn("get lock error");
}
} finally {
optimisticLock.unLock(lockName);
}
});
}
4.4、小结
基于数据库乐观锁的分布式锁具有以下优缺点:
优点:
缺点:
总结
基于数据库唯一索引、悲观锁、乐观锁实现分布式锁的适用场景可以总结如下:
基于数据库唯一索引的分布式锁:
基于数据库悲观锁的分布式锁:
基于数据库乐观锁的分布式锁: