关于事务那件小事

一 前言

事务是什么?我在想应该怎么记这个答案,用网上的话说:数据库的事务(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工作模式流程

  1. Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚

  2. Transaction Manager ™: 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议

  3. 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;
    }


}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值