一 前言
事务是什么?我在想应该怎么记这个答案,用网上的话说:数据库的事务(Transaction)是一种机制、一个操作序列,包含了一组数据库操作命令。事务把所有的命令作为一个整体一起向系统提交或撤销操作请求,即这一组数据库命令要么都执行,要么都不执行,因此事务是一个不可分割的工作逻辑单元。
不好记,那我们都知道事务有四个特性,我想只要符合这四个特性那理解事务就方便多了
二 ACID
原子性:事务是最小的执行单位,不允许分割。
事务的原子性确保动作要么全部完成,要么完全不起作用;
一致性:执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
隔离性:并发访问数据库时,一个用户的事务不被其他事务所干扰,
各并发事务之间数据库是独立的;
持久性:一个事务被提交之后。它对数据库中数据的改变是持久的, 即使数据库发生故障也不应该对其有任何影响。
原子性跟一致性的解释差不多,一个是静,一个是动。隔离性可以理解为你做你的,我做我的,互不影响,比如说张三李四对同一个账户进行存钱,取钱操作,他们之间不应该被对方所干扰,持久性可以看成对数据表进行增删改查操作,提交了操作之后,数据库中的数据不就永久性的发生改变了吗?
三 事务的隔离级别
什么是隔离级别?一说到级别就感觉会有事情发生,就好像地震级别一样,根据级别不同采取不同措施。事务也是如此。
就好比上面那个例子,如果账户里面只有一百元,张三取100,李四不想存钱,他也取100,结果必然有一人取不到钱(现实中),在程序中如果不对其限制,那么就是双方都能取到钱,很明显这样不对。
下面介绍了几种现象
脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据, 由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致, 这可能是两次查询过程中间插入了一个事务更新的原有的数据。
幻读(Phantom Read):在一个事务的两次查询中数据不一致, 例如有一个事务查询了几列(Row)数据, 而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中, 就会发现有几列数据是它先前所没有的。
理解:张三存了100进去,然后李四这里取钱时看到账户里有200,但是张三哪里执行错误,实际上钱没存进去,此时李四哪里看到的还是200,导致前后数据不一致。这就是脏读现象。
解决办法:MySQl定义了四种隔离级别
READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
四 事务的传播行为
指事务A被事务B调用时,事务B应该怎么运行?是继续在事务A中运行还是开一个新的事务
Spring在TransactionDefinition接口中规定了7种类型的事务传播行为。
事务传播行为是Spring框架独有的事务增强特性。
7种:(required / supports / mandatory / requires_new / not supported / never / nested)
- PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,这是最常见的选择,也是Spring默认的事务传播行为。(required需要,没有新建,有加入)
- PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。(supports支持,有则加入,没有就不管了,非事务运行)
- PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。(mandatory强制性,有则加入,没有异常)
- PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。(requires_new需要新的,不管有没有,直接创建新事务)
- PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。(not supported不支持事务,存在就挂起)
- PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。(never不支持事务,存在就异常)
- PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。(nested存在就在嵌套的执行,没有就找是否存在外面的事务,有则加入,没有则新建)
对事务的要求程度可以从大到小排序:mandatory / supports / required / requires_new / nested / not supported / never
后面持续更新
五 分布式事务(重点)
何为分布式事务?
一个业务调用另外一个业务的操作,事务保证操作要么全部执行,要么全部失败,保证数据库前后数据的一致性。
其实在程序中单体架构时可以用@Transactional注解保证数据库的数据,但是在分布式服务中,用这个注解是没用的,为什么呢?因为这时候他们就不在同一个事务中运行了,于是乎,诞生了分布式事务概念,而这里将推荐一款Seata开源工具
简介:Seata(Simple Extensible Autonomous Transaction Architecture) 是 阿里巴巴开源的分布式事务中间件,以高效并且对业务 0 侵入的方式,解决微服务场景下面临的分布式事务问题。
AT工作模式流程
-
Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚
-
Transaction Manager ™: 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议
-
Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚
TM:事务的管理者,知道事务有无异常,并将结果告知TC
TC:协调者,驱动真正干活的人(RM)
RM:提交分支事务
TM发现异常,通知TC,TC通知RM回滚事务
Seata全局事务隔离
写隔离
- 一阶段本地事务提交前,需要确保先拿到 全局锁 。
- 拿不到 全局锁 ,不能提交本地事务。
- 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
以一个示例来说明:
两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
1 tx1 先开始,开启本地事务,拿到本地锁,
2 更新操作 m = 1000 - 100 = 900。
3 本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。
4 tx2 后开始,开启本地事务,拿到本地锁,
5 更新操作 m = 900 - 100 = 800。
6 本地事务提交前,尝试拿该记录的 全局锁 ,
7 tx1 全局提交前,该记录的全局锁被 tx1 持有,
8 tx2 需要重试等待 全局锁 。
tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。
如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。
这方面可以参考官网Seata官网
上述也是官网的一部分
六 分布式锁
问题:多个用户抢同一个商品导致出现超买超卖问题
解决方法:加锁
加synchronized锁,在分布式场景无效
原因:由于分布式系统中的分布性,即多线程和多进程并发 分布在不同机器中,synchronized和lock这两种锁将失去原有锁的效果
用分布式锁
常见分布式锁有三种,主要介绍redis实现分布式锁
基于数据库实现的分布式锁
创建一张表,记录获得锁的主机信息以及线程信息,访问数据库看是否对应
基于Zookeeper实现分布式锁
每个客户端对某个方法加锁时,在Zookeeper上与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。
判断是否获取锁的方式只需要判断有序节点中的序号最小的一个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
基于缓存(redis)来实现分布式锁
采用setnx()和expire()组合实现加锁。
setnx来判断是否获得锁,expire给锁设置过期时间
但是会出现一个问题,设置过期时间长了会导致浪费过多时间
设置过期时间太短还是会导致超买超卖问题
解决办法:每隔一定时间便重置过期时间
这里介绍一款Redisson开源框架
pom依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
配置Redisson
//初始化redisson客户端
@bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
之前实现代码
//获取锁
Boolean lock = stringRedisTemplate.boundValueOps("menhu:miaosha:lock:"+goodsId).setIfAbsent("1");
if(!lock){
return new ResultVo(false,"系统繁忙,请稍后重试");
}
//设置过期时间
stringRedisTemplate.expire("menhu:miaosha:lock:"+goodsId,3, TimeUnit.SECONDS);
//todo ..
//释放锁
stringRedisTemplate.delete("menhu:miaosha:lock:"+goodsId);
现在实现代码
//获取锁
RLock lock = redissonClient.getLock("menhu:miaosha:lock:" + goodsId);
lock.lock(10,TimeUnit.SECONDS);
//todo ...
//释放锁
if(lock != null) {
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
//解锁
lock.unlock();
}
}
面试题:什么是分布式id,为什么使用?
ID是数据的唯一标识,在互联网企业中,大部分公司使用的都是Mysql,并且因为需要事务支持,所以通常会使用Innodb存储引擎,传统的做法是利用UUID和数据库的自增ID,UUID太长以及无序,所以并不适合在Innodb中来作为主键,自增ID比较合适,
但是 但是随着公司的业务发展,数据量将越来越大,需要对数据进行分表,而分表后,每个表中的数据都会按自己的节奏进行自增,很有可能出现ID冲突。
这时就需要一个单独的机制来负责生成唯一ID,生成出来的ID也可以叫做 分布式ID,或全局ID。
实现办法:雪花算法
雪花算法还有个优点:定长,数字类型
记住下面几点就行了,重要有机器id,占10bit,表示有1024位节点,序列号id,占12bit,能产生4096个id,时间戳,占41bit,精确到毫秒,可以容纳约69年时间。总之,其可以在每毫秒内产生1024*4096个不同的id
java实现:
public class IdWorker {
// 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
private final static long twepoch = 1288834974657L;
// 机器标识位数
private final static long workerIdBits = 5L;
// 数据中心标识位数
private final static long datacenterIdBits = 5L;
// 机器ID最大值
private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 数据中心ID最大值
private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
// 毫秒内自增位
private final static long sequenceBits = 12L;
// 机器ID偏左移12位
private final static long workerIdShift = sequenceBits;
// 数据中心ID左移17位
private final static long datacenterIdShift = sequenceBits + workerIdBits;
// 时间毫秒左移22位
private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
/* 上次生产id时间戳 */
private static long lastTimestamp = -1L;
// 0,并发控制
private long sequence = 0L;
private final long workerId;
// 数据标识id部分
private final long datacenterId;
public IdWorker(){
this.datacenterId = getDatacenterId(maxDatacenterId);
this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
}
/**
* @param workerId
* 工作机器ID
* @param datacenterId
* 序列号
*/
public IdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
/**
* 获取下一个ID
*
* @return
*/
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
if (lastTimestamp == timestamp) {
// 当前毫秒内,则+1
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 当前毫秒内计数满了,则等待下一秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
// ID偏移组合生成最终的ID,并返回ID
long nextId = ((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift) | sequence;
return nextId;
}
private long tilNextMillis(final long lastTimestamp) {
long timestamp = this.timeGen();
while (timestamp <= lastTimestamp) {
timestamp = this.timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
/**
* <p>
* 获取 maxWorkerId
* </p>
*/
protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
StringBuffer mpid = new StringBuffer();
mpid.append(datacenterId);
String name = ManagementFactory.getRuntimeMXBean().getName();
if (!name.isEmpty()) {
/*
* GET jvmPid
*/
mpid.append(name.split("@")[0]);
}
/*
* MAC + PID 的 hashcode 获取16个低位
*/
return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
}
/**
* <p>
* 数据标识id部分
* </p>
*/
protected static long getDatacenterId(long maxDatacenterId) {
long id = 0L;
try {
InetAddress ip = InetAddress.getLocalHost();
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
if (network == null) {
id = 1L;
} else {
byte[] mac = network.getHardwareAddress();
id = ((0x000000FF & (long) mac[mac.length - 1])
| (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
id = id % (maxDatacenterId + 1);
}
} catch (Exception e) {
System.out.println(" getDatacenterId: " + e.getMessage());
}
return id;
}
}