Mysql锁最全面知识点
MySql特性:
- **原子性:**事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
- **一致性:**执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
- **隔离性:**并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
- **持久性:**一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
1.并发事务带来哪些问题?
脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
不可重复度和幻读区别:
不可重复读的重点是修改,幻读的重点在于新增或者删除。
例1(同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 ):事务1中的A先生读取自己的工资为 1000的操作还没完成,事务2中的B先生就修改了A的工资为2000,导 致A再读自己的工资时工资变为 2000;这就是不可重复读。
例2(同样的条件, 第1次和第2次读出来的记录数不一样 ):假某工资单表中工资大于3000的有4人,事务1读取了所有工资大于3000的人,共查到4条记录,这时事务2 又插入了一条工资大于3000的记录,事务1再次读取时查到的记录就变为了5条,这样就导致了幻读。
2.Mysql锁
2.1行锁(不常用,容易造成死锁)
在并发处理能力上面有较大的优势,但是行级锁定也因此带来了不少弊端。由于锁定资源的颗粒度很小,所以每次获取锁和释放锁需要做的事情也更多,带来的消耗自然也就更大了。此外,行级锁定也最容易发生死锁。使用行级锁定的主要是InnoDB存储引擎。
语句为:
select ... for update;
mysql-plus写法
GoodsInfo goodsInfo = baseMapper.selectOne(new LambdaQueryWrapper<GoodsInfo>().eq(GoodsInfo::getId,1)
.last(" for update"));
特别注意的事情,innodb 的行锁是在有索引的情况下,没有索引的表是锁定全表的
2.2表锁
使用表级锁定的主要是MyISAM,MEMORY,CSV等一些非事务性存储引擎。
如果我们想对学生表(t_student)加表锁,可以使用下面的命令:
//表级别的共享锁,也就是读锁;
lock tables t_student read;
//表级别的独占锁,也就是写锁;
lock tables t_stuent wirte;
2.3共享锁?
共享锁又称读锁 (read lock),是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。当如果事务对读锁进行修改操作,很可能会造成死锁。
2.4排它锁?
排他锁 exclusive lock(也叫 writer lock)又称写锁,排它锁是悲观锁的一种实现。
3.乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS(Compare and swap)算法实现。
3.1版本号机制(常用)
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
**举一个简单的例子:**假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。
- 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 100-$50 )。
- 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 100-$20 )。
- 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
- 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。
eg:(Java代码)
这里博主建议version用redis来控制,如果用数据库字段会有update功能,在并发情况下,会造成一些错乱。
/***
* 版本号机制 更新库存(加库存)
* @param product
* @param lambdaProduct
* @param count 商品库存
*/
public Boolean versionLockAddStock(Product product, LambdaQueryWrapper<Product> lambdaProduct, Long count) {
/*乐观锁 版本号机制*/
/*用redis存版本号*/
Long version = getVersion(product.getId());
Product newProduct = productMapper.selectOne(lambdaProduct);
Long newVersion = getVersion(newProduct.getId());
/*redis获取库存*/
Long stock = 0L;
stock = getStock(product.getId());
/*版本号一致就更新*/
if (version.equals(newVersion)) {
Long totalStock = stock + count;
log.info("-----版本号一致,商品id为:" + product.getId() + "--- 版本号为:" + version);
newProduct.setStock(totalStock);
redisUtil.set(RedisConstants.MODULES_ORDER_GENERATE_VERSION_KEY + String.valueOf(newProduct.getId()), newVersion + 1);
/*redis修改库存*/
if (redisUtil.set(RedisConstants.MODULES_ORDER_GENERATE_STOCK_KEY + newProduct.getId(), totalStock)) {
/*直接更新数据库库存*/
newProduct.setStock(totalStock);
LambdaQueryWrapper<Product> eq = lambdaProduct.eq(Product::getId, newProduct.getId());
productMapper.update(newProduct, eq);
log.info("--------商品id为" + newProduct.getId() + "库存成功回加库存:" + totalStock);
return true;
} else {
return false;
}
}
/*版本号不一致*/
else {
log.error("-----版本号不一致,商品id为:" + product.getId() + "--- 版本号为:" + version);
/**睡眠2s 有3次机会*/
for (int i = 0; i < 3; i++) {
log.error("第:" + 1 + "次机会重试");
ThreadUtil.safeSleep(5 * 100);
this.versionLockReduceStock(product, lambdaProduct, count);
}
}
return false;
}
3.2 Redisson分布式锁
public boolean tryLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime) {
RLock lock = this.redissonClient.getLock(lockKey);
try {
return lock.tryLock(waitTime, leaseTime, unit);
} catch (InterruptedException var9) {
return false;
}
}
4.出现死锁的原因及排查方式
打个比方:
如果线程A锁住了记录1并等待记录2,而线程B锁住了记录2并等待记录1,这样两个线程就发生了死锁现象。
排查方式
- 查看正在进行中的事务
SELECT * FROM information_schema.INNODB_TRX
-
查看正在锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
-
查看等待锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
-
查询是否锁表
SHOW OPEN TABLES where In_use > 0;
-
在发生死锁时,这几种方式都可以查询到和当前死锁相关的信息,查看最近死锁的日志
show engine innodb status
-
解除死锁,如果需要解除死锁,有一种最简单粗暴的方式,那就是找到进程id之后,直接干掉。
-
查看当前正在进行中的进程
// 也可以使用
SELECT * FROM information_schema.INNODB_TRX;show processlist
// 也可以使用
SELECT * FROM information_schema.INNODB_TRX;
-
这两个命令找出来的进程id 是同一个。
杀掉进程对应的进程 id
kill id
9.验证(kill后再看是否还有锁)
SHOW OPEN TABLES where In_use > 0;
个人搭建项目代码地址:
https://github.com/hongjiatao/spring-boot-anyDemo
欢迎收藏点赞三连。谢谢!