数据一致性问题分析(事务/锁)

环境:JDK17

数据库层

  • 事务管理
    事务的ACID属性(Atomicity原子性、Consistency一致性、Isolation隔离性、Durability持久性)保证了数据的一致性。
    事务将一组操作作为一个工作单元,必须全部成功,否则直接回滚到操作前的状态。
  1. 在Mysql事务中,通过多版本并发控制机制,每个事务在开始的时候获取一个数据版本,事务中的查询操作都在这个版本的数据中查询,这样就保证了数据的隔离性。
  2. 在事务要执行增删改的操作时通过锁机制防止多个事务同时修改相同的数据行。

锁机制:有行锁、表锁、页锁(在数据库存储中通常将固定大小的数据块组成一页,通常是4K)
间隙锁:InnoDB事务隔离级别下的一种锁机制,主要用于防止幻读,既锁定实际存在的记录,也锁定索引记录之间的空间,防止这些间隙间插入数据。
Mysql的InnoDB引擎使用的是行锁,在某些情况下也会使用到页锁,如:Btree索引的合并与分裂、创建或更新全文索引、脏页数据从缓冲池刷新到磁盘等这些可能会涉及到的页数据时。

Mysql 的事务隔离级别:

  1. 读未提交(READ UNCOMMITTED):可以读取其它事务尚未提交的数据,存在脏读、幻读、不可重复读问题
  2. 读已提交(READ COMMITTED):可读到其它事务已提交的数据,存在幻读、不可重复读问题
  3. 可重复读(REPEATABLE READ):同一事务内多次读取的结果一致,Mysql默认的事务隔离级别,存在幻读问题。
  4. 可序列化(可串行化)(SERIALIZABLE):强制事务串行化执行,高并发环境下性能降低

幻读:第一次未读取到该行数据,第二次却读取到该数据,在mysql中通过多版本并发控制机制和锁机制防止幻读(行锁和间隙锁结合的机制)
不可重复读即第一次和第二次读取的同一行数据发生了变化

设置事务级别的sql
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

SQL事务中执行

START TRANSACTION --开始事务
SELECT ...
UPDATE ...
COMMIT --提交事务

配合IF语句用ROLLBACK回滚事务
  • 乐观并发控制
    场景:存在多个事务对同一资源数据的修改,比如数据value初始值为0,事务A和事务B都需要对value进行递增,修改前事务A和B的数据快照显示value都为0,那么就会导致A和B修改后value值都是1,然期望的最终结果应该是2。
    为处理这种场景,可采用(Optimistic Concurrency Control, OCC)乐观并发控制机制,更新的sql中加上条件 where value = 快照中的值,这样A、B两个事务就有一个需要回滚重试。常见的就是数据库表设计一个数据版本字段version,更新时带上该条件。
  • Mysql 索引

B-Tree索引,InnoDB和MyISAM引擎中常用的索引,平衡树索引

平衡性:每个节点可存在多个子节点,使得树的高度低,查询效率高。
有序性:数据是按照顺序排列的,所以范围查询、排序、分组的效率高。

Hash索引

将键值转换成Hash码,直接定位到存储位置,因此查询很快。
由于数据的无序,不支持范围查询、分组和排序。
适用于唯一键,因为hash冲突会导致性能降低。
hash索引通常存储在内存中,适合小数据集高性能读取。

-- 创建索引
CREATE INDEX index_name ON table_name (column_1,column_2,...);
-- 查看索引
SHOW INDEX FROM table_name;
-- 删除索引
DROP INDEX index_name ON table_name;

通过查询执行计划可以确定sql中使用到的索引
EXPLAN SELECT …

possible_keys可能用到的索引,key实际用到的索引,key_len使用索引的长度

JAVA客户端层

在数据库的层我们是通过事务来保证数据的一致性,那么在客户端层也可以从这一方面入手。

  • 事务注解:@Transactional
@Transactional(rollbackFor = Exception.class)
public void business(){
   //数据库交互
}

propagation指定事务的传播行为,默认是REQUIRED,即当前存在事务则加入,不存在则创建新的事务,所以当一个被@Transactional标记的方法调用另一个被@Transactional标记的方法时是不会开启新事务的。
SUPPORTS:当前不存在事务则以非事务方式运行
MANDATORY:当前不存在事务则抛出异常
REQUIRES_NEW:创建一个新事务,将当前事务挂起,所以会导致外部事务回滚,新事务依然提交。
NOT_SUPPORTED:当前事务挂起,已非事务方式执行,适用临时脱离事务的操作

Isolation设置事务隔离级别,然后传递给底层对应的数据库,表示该事务在数据库执行时使用何种级别的事务隔离

注意:事务注解依赖AOP,使用的是JDK的动态代理,所以调用被注解标记的方法不能在同一个类中。

JAVA中的锁
通过锁来控制多个线程对同一资源的访问和操作,以确保数据的一致性,这些锁锁定的是java应用程序而不能影响到外部的数据库。

  • synchronized 内置锁或监视锁
public void synchronized methold() {
    锁定该方法执行的代码块,执行结束则释放锁
}

public void methold() {
  synchronized(this) {
    锁定当前对象,没释放锁时其它线程不可访问该对象
  }
}

public void methold() {
  Object lock = new Object();
  synchronized(lock ) {
    锁定这个对象,一般是对这个对象需要同步访问
  }
}

比如:ConcurrentHashMap 一个线程安全的HashMap其保证线程安全的原理就是通过对需要访问修改的Node节点上锁。
在这里插入图片描述

  • 显示锁

可重入锁:ReenTrantLock实现Lock
读写锁:ReentrantReadWriteLock实现ReadWriteLock,写锁获取后其它线程不能获取写锁和读锁

都是锁定的当前对象不同线程对当前对象所访问的共享资源的一个锁控制。
比如:A对象有个共享属性a,B对象有个共享属性b,C对象中申明了一个显示锁,且锁定的代码块中有对A.a的访问没有对B.b的访问,那么线程获取的锁只控制的共享资源只是A.a,如果这时候有对象C中又申明了一个显示锁,在其所锁定的代码块中也有对A.a的访问,这个时候两个显示锁对A.a的访问锁控制是独立的,也就是可能会出现数据不一致问题。

ReentrantLock基于AQS(AbstractQueuedSynchronizer)构建,使用了一个FIFO队列管理等待获取锁的线程,
volatile 修饰的state状态控制锁状态,0(锁未被任何线程持有)大于0(被某个线程持有),同一线程可重复获取同一锁,state状态递增,释放锁时state递减,直到状态为0
状态值的第一次增加时即设置state为1时使用的方法:

 private static final long STATE = U.objectFieldOffset(AbstractQueuedSynchronizer.class, "state");
 protected final boolean compareAndSetState(int expect, int update) {
    return U.compareAndSetInt(this, STATE, expect, update);
 }
 //CAS 先比较后设值
 public final native boolean compareAndSetInt(Object o, long offset, int expected,int x);

volatile 可见性:当某个线程修改了变量值时,会立即更新到主内存,其它线程可以看到新值。有序性:读写指令不会被重排,即执行了前面的命令后后面依次执行

ReentrantLock和ReentrantReadWriteLock 获取锁有公平模式和非公平模式默认都是非公平模式
AQS中的队列用Node的数据结构来实现

abstract boolean initialTryLock();
//公平锁
FairSync.initialTryLock() {按照Node节点顺序来遍历}
//非公平锁
NonfairSync.initialTryLock(){可以插队,不需要按照Node节点顺序来}
  • 分布式锁

在分布式系统中控制多个节点对共享资源的访问锁控制
Redis分布式锁

Java中的原子操作类
如:AtomicInteger AtomicBoolean AtomicReference 其属性value值通过volatile修饰,利用底层提供的CAS原子操作指令来保证数据的操作的原子性,避免了锁机制带来的开销。

AtomicInteger

jdk.internal.misc.Unsafe

@IntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,int expected,int x);

AtomicReference

java.lang.invoke.VarHandle

@IntrinsicCandidate
boolean compareAndSet(Object... args);

Unsafe和VarHandle都是进行低级别内存操作工具,VarHandle是JDK9之后的产物,相较于Unsafe更加安全,避免了直接暴露内存操作接口,覆盖了Unsafe的大部分接口。

分布式事务管理

  • AT(Automatic Transaction)
    AT 是 Seata的一种事务模式,通过拦截SQL生成反向回滚SQL确保分布式事务的一致性。
  • 低侵入性:无需额外编写补偿机制或确认提交取消等方法(相较于TCC和SAGA)。
  • 事务开始执行更新SQL时会先检查lock_table表,判断是否有事务在执行当前数据的更新操作(也就是获取全局锁),获取锁成功的同时会在lock_table中插入一条锁定记录,然后Seata会将变更前的数据快照记录到undo_log中(既是在事务提交时与最新的数据快照做对比,也是做为回滚数据的依照)
  • 当然还是会存在快照比对也就是冲突核实期间,其它事物对同一数据修做了修改,导致数据一致性问题,这中情况可以结合上述事务管理中提到的乐观并发控制机制。
  • 自动回滚:由框架自动完成,由于是通过生成的反向SQL来回滚,所以对于复杂的SQL可能不是很友好
  • 性能方面:由于需要记录快照数据,所以存在一定的存储空间开销,且在高并发场景下可能会存在频繁的冲突检测和重试影响性能。
  • TCC(Try-Confirm-Cancel)
    通过预操作(try)、确认提交(confirm)、取消即回滚(cancel)确保事务的一致性,适用于金融支付问题。
    在这里插入图片描述
    如上图服务A中开启了一个事务,调用了服务B和服务C的程序,那么相应的B和C服务也需要手动实现TCC,服务A中的try在执行时手动调用服务B和C的Try,同理服务A中的confirm和cancel也需要调用服务B和C的confirm和cancel。
    根据try和confirm是否发生异常或超时(也就是确认其中末一环节失败)确定是否需要取消本次事务操作。
  • 隔离性:通过try阶段锁定资源,避免其它事务对同一资源做操作,常见的就是我们在余额变更时所采用的冻结余额(也就是预留资源做预操作,方便cancel时释放预留资源)
  • 性能开销:由于TCC三个阶段都需要对数据库做操作,所以存在一定的数据库压力
  • 幂等性:由于confirm和cancel可能会存在重复调用,所以一定要确保这两个阶段的幂等性操作。
  • 问题:当Try阶段成功,confirm或cancel失败可能会导致资源一直被锁定,就如余额一直被冻结,这个时候我们需要有额外的机制来处理。
  • SAGA
    通过一系列本地事务的组合来完成最终的一致性问题,适用于事务较长的,如:订单处理、工作流等。
    在这里插入图片描述
    如图:整个事务分别在ABC三个服务中执行三个流程,每个流程执行成功之后可以通过消息中间间的方式告知其它服务流程,如果某一个流程执行失败了,那么其它两个服务流程需要执行相应的补偿机制。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值