pinterest网站怎么进,上海网站定制费用,网站的风格主要包括,微信api文档一、MySQL 排它锁和共享锁
在进行实验前#xff0c;先来了解下MySQL 的排它锁和共享锁#xff0c;在 MySQL 中的锁分为表锁和行锁#xff0c;在行锁中锁又分成了排它锁和共享锁两种类型。
1. 排它锁
排他锁又称为写锁#xff0c;简称X锁#xff0c;是一种悲观锁#x…一、MySQL 排它锁和共享锁
在进行实验前先来了解下MySQL 的排它锁和共享锁在 MySQL 中的锁分为表锁和行锁在行锁中锁又分成了排它锁和共享锁两种类型。
1. 排它锁
排他锁又称为写锁简称X锁是一种悲观锁具有悲观锁的特征如一个事务获取了一个数据行的X锁其他事务尝试获取锁时就会等待另一个事务的释放。其中在 InnoDB 引擎下做写操作时 UPDATE、DELETE、INSERT都会自动给涉及到的数据加上 X 锁因此当多线程情况下对同一条数据进行更新在MySQL中不会出现线程安全问题。
其中 SELECT 语句默认不会加锁如果查询的数据已经存在 X 锁则会返回其最近提交的数据如果希望每次获取的数据都是更新后最新的数据当存在有更新时则等待更新完成后获取新的值这种情况下就需要对 SELECT 语句也要存在 X 锁其中 SELECT 语句加 X 锁的话需要使用 FOR UPDATE 语句。
比如当前有一张表结构如下
CREATE TABLE lock (id int NOT NULL AUTO_INCREMENT,name varchar(255) DEFAULT NULL,PRIMARY KEY (id)
) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci;写入一条测试数据
INSERT INTO testdb.lock(id, name) VALUES (1, lock1);下面我使用 Navicat 开启了两个对话框我在第一个对话框中使用手动提交事务的方式执行更新语句并且既不提交也不回滚事务
BEGIN;
UPDATE lock SET name lock2 WHERE id 1; 下面在另一个对话框中查询 id 1 的数据
SELECT * FROM lock where id 1可以看到并没有拿到最新的内容因为此时 X 锁还没有释放那此时对查询语句进行调整下加上 FOR UPDATE 语句
SELECT * FROM lock where id 1 FOR UPDATE此时会发现查询语句一直在等待因为这个查询语句在等待 X 锁的释放下面对第一个对话框中执行提交事务
COMMIT;在回到第二个对话框中查看 已经拿到最新的值。这里需要注意下你的是不是出现了超时报错这是因为 Innodb 引擎对等待锁有个等待超时时间默认情况下是 50s 可以通过下面指令查看
SHOW VARIABLES LIKE Innodb_lock_wait_timeout如果感觉太小可以通过下面指令调整
SET innodb_lock_wait_timeout 100上面的操作已经感觉出来 X 锁的效果那当两个 SELECT 语句都加上 FOR UPDATE 呢比如在第一个回话框中使用手动事务执行 SELECT 语句同样不提交事务:
BEGIN;
SELECT * FROM lock where id 1 FOR UPDATE;在第二个对话框同样执行相同的代码可以发现被阻塞掉了。 当第一个提交事务后第二个紧接着也查出了信息这也正符合排他锁的特征。
2. 共享锁
共享锁可以理解为读锁简称S锁可以对多个事务SELECT情况下读取同一数据时不会阻塞但是如果存在写操作时 UPDATE、DELETE、INSERTSELECT语句也会被阻塞在MySQL中使用 S 锁需要使用 LOCK IN SHARE MODE。
例如还是开启两个对话框在第两个对话框中都查询 id 1 的数据并加上 S 锁最后同样不提交事务
BEGIN;
SELECT * FROM lock where id 1 LOCK IN SHARE MODE;可以发现两个都拿到了数据对两个都提交事务后假如第一个对话框中是更新操作最后同样不提交事务
BEGIN;
UPDATE lock SET name lock3 WHERE id 1 ;在第二个对话框中还是加上 S 锁的查询操作
BEGIN;
SELECT * FROM lock where id 1 LOCK IN SHARE MODE;可以看到查询被阻塞了当第一个对话框中提交了事务这里才会返回结果 读到这里相信大家已经对 MySQL 的排它锁和共享锁有了一定的了解下面我们基于 排它锁 实现分布式锁的场景。
二、基于 MySQL 排它锁实现分布式可重入锁
根据上面的实例可以看到排它锁具有阻塞等待的效果和我们 JVM 中普通的锁的效果是一致的但普通的锁通常只能在单个 JVM 中但现在的服务动则都要多台集群部署对于不同的 JVM 普通的锁实在心有余而力不足此时就要考虑使用分布式锁目前分布式锁的解决方案也比较多例如基于 Redis 的 setNx 实现的分布式锁相关框架有 Redissson 还有基于 Zookeeper 的临时节点实现的分布式锁相关框架有 Curator 等等而且这些都有方案实现锁的可重入性。
本文我们再介绍一种基于 MySQL 的方案毕竟现在再小的项目基本都会引入数据库我们在此基础上延伸也少了其他框架的学习。
实现的思路
数据库中创建一个lock 表 里面根据场景添加数据一行就代表一个分布式锁的句柄。在项目中在需要锁的方法中首先开启事务保证下面的操作在事务中事务可借助 Spring 的 Transactional 注解。在获取锁时使用 SELECT * FROM lock WHERE id #{id} FOR UPDATE 排它锁语句执行。如果正常查询到则获取锁成功此时如果其他事务也在获取锁则因为排他锁的原因会阻塞等待。此时如果还要获取锁也就是对于锁的可重入性设计可以利用同一个事务中对于同一条数据 FOR UPDATE 不会阻塞的特征只需在同一个事务中再次获取锁的操作即可实现 。方法执行完如果是手动事务一定要提交或回滚事务即表示释放锁如果是 Spring 的 Transactional 注解则会自动提交或回滚。
开始实施
首先新建一个 SpringBoot 项目在 pom 中引入 mybatis-plus 依赖
dependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-boot-starter/artifactIdversion3.3.2/version
/dependencydependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactId
/dependencydependencygroupIdcom.alibaba/groupIdartifactIddruid/artifactIdversion1.1.6/version
/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactId
/dependency
下面在配置文件中增加 MySQL 的配置
server:port: 8081spring:datasource:url: jdbc:mysql://127.0.0.1:3306/testdb?useUnicodetruecharacterEncodingutf8serverTimezoneUTCusername: rootpassword: roottype: com.alibaba.druid.pool.DruidDataSource下面获取锁的逻辑其实就是一个 Mapper 中的 Select 操作
Mapper
public interface LockMapper {/*** 尝试获取锁*/Select(SELECT id FROM lock where id #{id} FOR UPDATE;)Long tryLock(Param(id) Long id);
}下面编写一个线程安全的例子使用 10 个线程去对一个全局 int 变量做 1 操作这里为了方便测试直接声明成 Controller
RestController
public class LockService {private volatile int count 0;GetMapping(/test)public void test() {for (int i 0; i 10; i) {new Thread(() - {testLock();}).start();}}public void testLock() {count;System.out.print(count , );}
}运行后访问测试接口查看控制台打印的效果 可以看到已经出现线程安全问题了下面我们改造成使用 MySQL 的排他锁进行协调这里需要注意下这里事务使用的是 Spring 的 Transactional 注解是基于 AOP 实现的因此 LockService 需要从 Spring 容器中获取 另外对于锁的超时可以捕获 CannotAcquireLockException 异常。
RestController
public class LockService {ResourceLockService lockService;ResourceLockMapper lockMapper;private final Long LOCK_ID 1L;private volatile int count 0;GetMapping(/test)public void test() {for (int i 0; i 10; i) {new Thread(() - {lockService.testLock();}).start();}}Transactional(rollbackFor Exception.class)public void testLock() {try {//获取锁如果获取不到则阻塞if (Objects.nonNull(lockMapper.tryLock(LOCK_ID))){count;System.out.print(count , );}} catch (CannotAcquireLockException e) {System.out.println(获取锁超时);}}
}执行后查看日志 细心地话可以明显感觉执行速度比之前慢了因为出现了阻塞情况通过数据可以看到已经解决了线程安全问题但是锁的可重入性呢我们在获取到锁后再次获取锁看看是否正常注意可重入锁表示锁中锁锁的对象一定要是一致的也就是这里的锁的 ID 要是一致的
RestController
public class LockService {ResourceLockService lockService;ResourceLockMapper lockMapper;private final Long LOCK_ID 1L;private volatile int count 0;GetMapping(/test)public void test() {for (int i 0; i 10; i) {new Thread(() - {lockService.testLock();}).start();}}Transactional(rollbackFor Exception.class)public void testLock() {try {//获取锁如果获取不到则阻塞if (Objects.nonNull(lockMapper.tryLock(LOCK_ID))){// 重入锁if (Objects.nonNull(lockMapper.tryLock(LOCK_ID))){count;System.out.print(count , );}}} catch (CannotAcquireLockException e) {System.out.println(获取锁超时);}}
}运行后查看日志 可以看到可重入锁场景下也是可以正常获取到锁。
三、总结
本文基于 MySQL 实现的一种分布式可重入锁的效果由于锁是使用的 MySQL 的排他锁因此在多个 JVM 中也是可以实现锁的效果。这里主要讲解了实现思路对于模块的封装没有做过多的设计如果有想法的小伙伴也可以发动想法封装一下。另外由于是使用了 MySQL 如果是大量并发的情况下可能会对 MySQL 造成一些压力。另外可能由于某些原因造成一端持有锁的时间过长其余等待锁发生超时现象超时情况这里未做处理后续可以根据实际情况进行重试或错误处理。