MySQL中的锁

一、SQL语句使用注意事项

1. alter table 锁表

Mysql在5.6版本之前,直接修改表结构的过程中会锁表,具体的操作步骤如下:
1. 首先创建新的临时表,表结构通过命令ALTAR TABLE新定义的结构
1. 然后把原表中数据导入到临时表
1. 删除原表
1. 最后把临时表重命名为原来的表名

Mysql 5.6 虽然引入了Online DDL,但是并不是修改表结构的时候,一定不会导致锁表,在一些场景下还是会锁表的,比如
- 某个慢SQL或者比较大的结果集的SQL在运行,执行ALTER TABLE时将会导致锁表发生;
- 存在一个事务在操作表的时候,执行ALTER TABLE也会导致修改等待;

根据这次教训,得到注意项:

  1. 尽量选择流量小的事后执行。
  2. 执行时先看一下有没有未提交的事务,注意查看事物information_schema.innodb_trx表
  3. 随时关注服务器日志状况,已有问题要先行解决。
  4. 后续可现在预发环境或测试环境先行模拟,评估风险

二、锁、事务、隔离级别

0. 基础知识

  • 全部的表类型都可以使用锁,但是只有 InnoDB 和 BDB 才有内置的事务功能。
  • 使用 begin 开始事务,使用 commit 结束事务,中间可以使用 rollback 回滚事务。
  • 在默认情况下, InnoDB 表支持一致读。
  • 如果多个事务更新了同一行,就可以通过回滚其中一个事务来解除死锁。
  • MySQL 允许利用 set transaction 来设置隔离级别。
  • 事务只用于 insert 和 update 语句来更新数据表,不能用于对表结构的更改。执行一条更改表结构或 begin 则会立即提交当前的事务。
  • 行级锁(update)在commit之后释放。
  • update语句执行时,如果where语句后的条件不存在索引,则会锁住全表,效率很低;

1. 锁的分类

从对数据操作的类型(读\写)分

在事务中,只有SELECT … FOR UPDATE 或LOCK IN SHARE MODE 同一笔数据时会等待其它事务结束后才执行,一般SELECT … 则不受此影响。

读锁(共享锁):针对同一块数据,多个读操作可以同时进行而不会互相影响。

SELECT * FROM parent WHERE NAME = ‘Jones’ LOCK IN SHARE MODE;

如果事务A获得了先获得了读共享锁,那么事务B之后仍然可以读取加了读共享锁的行数据,但必须等事务A commit或者roll back之后才可以更新或者删除加了读共享锁的行数据。

写锁(排他锁):当当前写操作没有完成前,它会阻断其他写锁和读锁。

SELECT counter_field FROM child_codes FOR UPDATE;
UPDATE child_codes SET counter_field = counter_field + 1;

如果事务A先获得了某行的写共享锁,那么事务B就必须等待事务A commit或者roll back之后才可以访问行数据。

  • 共享锁只用于表级,排他锁用于行级。
  • 加了共享锁的对象,可以继续加共享锁,不能再加排他锁。加了排他锁后,不能再加任何锁。
  • 比如一个DML操作,就要对受影响的行加排他锁,这样就不允许再加别的锁,也就是说别的会话不能修改这些行。同时为了避免在做这个DML操作的时候,有别的会话执行DDL,修改表的定义,所以要在表上加共享锁,这样就阻止了DDL的操作。
  • 当执行DDL操作时,就需要在全表上加排他锁

从锁定的数据范围分

表锁

行锁

为了尽可能提高数据库的并发度,每次锁定的数据范围越小越好,理论上每次只锁定当前操作的数据的方案会得到最大的并发度,但是管理锁是很耗资源的事情(涉及获取,检查,释放锁等动作),因此数据库系统需要在高并发响应和系统性能两方面进行平衡,这样就产生了“锁粒度(Lock granularity)”的概念。


2. 锁粒度(Lock granularity)

表锁:管理锁的开销最小,同时允许的并发量也最小的锁机制。MyIsam存储引擎使用的锁机制。当要写入数据时,把整个表都锁上,此时其他读、写动作一律等待。在MySql中,除了MyIsam存储引擎使用这种锁策略外,MySql本身也使用表锁来执行某些特定动作,比如alter table.

行锁:可以支持最大并发的锁策略。InnoDB和Falcon两张存储引擎都采用这种策略。

Oracle采用的是行锁。


3. 事务(Transaction)

从业务角度出发,对数据库的一组操作要求保持4个特征:

Atomicity:原子性

Consistency:一致性

Isolation:隔离性

Durability:持久性

4. 隔离级别(Isolation Level)

Read Uncommitted(读取未提交内容)

在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。

Read Committed(读取提交内容)

这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。

Repeatable Read(可重读)

这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。

Serializable(可串行化)

这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。

脏读:事务 A 读取了事务 B 未提交的数据,并在这个基础上又做了其他操作。

不可重复读:事务 A 读取了事务 B 已提交的更改数据。

幻读:事务 A 读取了事务 B 已提交的新增数据。

5. select… for update

比如select .. for update limit 0,30时,其他服务器执行同样sql语句会自动等待释放锁,等待前一台服务器锁释放后,该台服务器就能查询下一个30条数据。

mysql的innodb存储引擎实务锁虽然是锁行,但它内部是锁索引的,根据where条件和select的值是否只有主键或非主键索引来判断怎么锁,比如只有主键,则锁主键索引,如果只有非主键,则锁非主键索引,如果主键非主键都有,则内部会按照顺序锁。

innodb有索引的话,采用的是行级锁,没有索引或者索引失效后采用的是表级锁。

for update的死锁问题

一条sql语句先锁主键索引,再锁非主键索引;另外一条sql语句先锁非主键索引,再锁主键索引。虽然两个sql语句期望锁的数据行不一样,但两个sql语句查询或更新的条件或结果字段如果有相同列,则可能会导致互相等待对方锁,2个sql语句即引起了死锁。

MySQL select…for update的Row Lock与Table Lock

使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认Row-Level Lock,所以只有「明确」地指定主键,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表单给锁住)。

明确主键/唯一索引:可以使用=和in

for update 使用子查询的情况

SELECT i.* FROM invest i where i.id = (select max(im.id) from invest im WHERE im.id LIKE CONCAT(‘20150717000001’, ‘%’)) for update;

暂时发现无影响。


6. 乐观锁 悲观锁

乐观锁(Optimistic):

每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁.

如何实现乐观锁呢,一般来说有以下2种方式:

1.使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。

2.乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。

举例:

商品goods表中有一个字段status,status为1代表商品未被下单,status为2代表商品已经被下单,那么我们对某个商品下单时必须确保该商品status为1。假设商品的id为1。

下单操作包括3步骤:

1.查询出商品信息
select (status,status,version) from t_goods where id=#{id}

2.根据商品信息生成订单

3.修改商品status为2
update t_goods
set status=2,version=version+1
where id=#{id} and version=#{version};

Spring的乐观锁重试实现:

考虑如果乐观锁update/insert影响行数为0.则可以执行一个do–while()循环,但是由于mysql默认的隔离级别是可重复读(Repeatable read),这时,它不能读取到其它线程更新的数据,所以这个线程读取的数据永远是version=1的,而不是最新的,除非修改显示设置隔离级别为:read commited

@Transactional(isolation=Isolation.READ_COMMITTED)

或者DAO方法不使用Spring提供的声明式事务@Transactional,操作每次都是新开一个事务,而当更新失败,需要再从数据库中查最新的时候,这时是一个新事务了,它就可以看到别人更新的数据了,问题就解决了。

或者考虑使用如下的AOP方式解决:

1.applicationContext.xml要配置:

<aop:aspectj-autoproxy/>

2.自定义注解:

@Retention(RetentionPolicy.RUNTIME)
public @interface IsTryAgain {

}

在需要重试的方法上加@IsTryAgain。

3.定义AOP类:

@Aspect
public class ConcurrentOperationExecutor implements Ordered {

    private static final int DEFAULT_MAX_RETRIES = 1;

    private int maxRetries = DEFAULT_MAX_RETRIES;
    private int order = 100;


    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order)
    {
        this.order = order;
    }

    @Around("@annotation(IsTryAgain)")
    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable
    {
        int numAttempts = 0;
        //InvestException investException;  //自定义异常(如果乐观锁时,判断update/insert=0,则主动抛出自定义异常)
        //Exception investException;   //父类Exception ,回滚时可拦截成功
        DataAccessException investException; //父类Exception ,回滚时可拦截成功
        do
        {
            numAttempts++;
            try {
                return pjp.proceed();
            } catch (DataAccessException ex) {
                investException = ex;
                System.out.println("----投资异常!");
            }
        }
        while (numAttempts <= this.maxRetries);
        throw investException;
    }
}

悲观锁(Pessimistic Lock):

就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

要使用悲观锁,我们必须关闭mysql数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。

我们可以使用命令设置MySQL为非autocommit模式:
set autocommit=0;

例如:

设置完autocommit后,我们就可以执行我们的正常业务了。具体如下:

//0.开始事务
begin;/begin work;/start transaction; (三者选一就可以)

//1.查询出商品信息
select status from t_goods where id=1 for update;

//2.根据商品信息生成订单
insert into t_orders (id,goods_id) values (null,1);

//3.修改商品status为2
update t_goods set status=2;

//4.提交事务
commit;/commit work;


7. 具体问题分析

7.1 解决并发更新同一数据的问题(先select后update)
  • 把SELECT和UPDATE合成一条SQL

或:

  • 使用写共享锁select for update查询,再update(悲观锁)

或:

  • 乐观锁
    首先SELECT SQL不作任何修改,然后在UPDATE SQL的WHERE条件中加上SELECT出来的vip_memer的end_at条件。

如下:

vipMember = SELECT * FROM vip_member WHERE uid=1001 LIMIT 1 # 查uid为1001的会员
cur_end_at = vipMember.end_at
if vipMember.end_at < NOW():
    UPDATE vip_member SET start_at=NOW(), end_at=DATE_ADD(NOW(), INTERVAL 1 MONTH), active_status=1, updated_at=NOW() WHERE uid=1001 AND end_at=cur_end_at
else:
    UPDATE vip_member SET end_at=DATE_ADD(end_at, INTERVAL 1 MONTH), active_status=1, updated_at=NOW() WHERE uid=1001 AND end_at=cur_end_at
7.2 平台转账设计(两人互相转账/多人向同一个人转账)
  • 使用写共享锁select for update查询,再update(悲观锁)
7.3 关于余额类数据的增减操作

如果有一个表有条数据其中一个字段money,然后所有人都可以获取这个Money字段的值进行更新,然后新添加一条新的记录数据(money为修改后的数据),以后每个人都需要获取最新的数据的money。并发情况下,如何保证最新的一条数据的money值是正确的??

新添加一个version字段(并添加唯一索引),每次更改money的同时,执行version=version+1,并发时导致获取version相同,都执行version=version+1时,后提交的数据会由于唯一索引的关系更新失败。

此种情况下获取最新的一条记录不需要加悲观锁(悲观锁应该可以防止money的不同)。

7.4 ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction 当锁等待超过设置时间的时候,就会报这个的错误;

1、锁等待超时。是当前事务在等待其它事务释放锁资源造成的。可以找出锁资源竞争的表和语句,优化你的SQL,创建索引等,如果还是不行,可以适当减少并发线程数。

2、你的事务在等待给某个表加锁时超时了,估计是表正被另的进程锁住一直没有释放。
可以用 SHOW INNODB STATUS/G; 看一下锁的情况。

3、修改innodb_lock_wait_timeout

指的是事务等待获取资源等待的最长时间,超过这个时间还未分配到资源则会返回应用失败;
参数的时间单位是秒,最小可设置为1s(一般不会设置得这么小),最大可设置1073741824秒(34年,一条语句锁等待超过30分钟估计业务该有反馈了)
默认安装时这个值是50s

通过语句修改

set innodb_lock_wait_timeout=100;

set global innodb_lock_wait_timeout=100;

注意global的修改对当前线程是不生效的,只有建立新的连接才生效

或者修改参数文件/etc/my.cnf
innodb_lock_wait_timeout = 50

7.5 查询锁

监控innodb锁的状态:

use information_schema ;
SELECT * FROM INNODB_TRX;

trx_state: RUNNING —- trx_id: 只是持有锁,但并没有产生锁等待;

trx_state: LOCK WAIT —- trx_id: 状态为锁等待

锁等待和持有锁的相互关系

use information_schema ;
SELECT * FROM INNODB_LOCK_WAITS;

requesting_trx_id:锁等待;

blocking_trx_id:持有锁。

锁等待的原因

SELECT * FROM INNODB_LOCKS;

在进程里查看状态

SHOW PROCESSLIST;

KILL id号。

7.6 联合主键的自增长

ALTER TABLE user_bill_1 DROP PRIMARY KEY ,ADD PRIMARY KEY ( id , seq_num ) ;
ALTER TABLE user_bill_1 CHANGE COLUMN seq_num seq_num bigint(21) NOT NULL AUTO_INCREMENT ;

7.7 实现从一批数据中找到最后一条,实现锁定,并添加一条

场景:比方说一个用户的账户历史记录表中存有当前的用户余额,每次用户操作时都会往表中插入一条记录,这条记录的余额为最新值,那么如何保证多线程同时操作获取的最后一条数据是最新的?

首先,使用 LOCK IN SHARE MODE读锁查询出最后一条数据,

SELECT u.* FROM user_bill u WHERE u.user_id=#{userId} ORDER BY u.seq_num DESC LIMIT 1 LOCK IN SHARE MODE

当一个用户需要累加seq_num+1,然后插入记录,直接操作即可。


8.InnoDB锁类型

8.1 基本锁

基本锁:共享锁( Shared Locks:S锁)与排他锁(Exclusive Locks:X锁)

mysql允许拿到S锁的事务读一行,允许拿到X锁的事务更新或删除一行。

加了S锁的记录,允许其他事务再加S锁,不允许其他事务再加X锁;

加了X锁的记录,不允许其他事务再加S锁或者X锁。

mysql对外提供加这两种锁的语法如下:

  • 加S锁:select…lock in share mode
  • 加X锁:select…for update
8.2 意向锁(Intention Locks)

InnoDB为了支持多粒度(表锁与行锁)的锁并存,引入意向锁。
意向锁是表级锁,可分为意向共享锁(IS锁)和意向排他锁(IX锁)。

事务在请求S锁和X锁前,需要先获得对应的IS、IX锁。

意向锁产生的主要目的是为了处理行锁和表锁之间的冲突,用于表明“某个事务正在某一行上持有了锁,或者准备去持有锁”。

意向锁为了方便检测表级锁和行级锁之间的冲突,故在给一行记录加锁前,首先给该表加意向锁。也就是同时加意向锁和行级锁。

共享锁、排他锁与意向锁的兼容矩阵如下:

XIXSIS
X冲突冲突冲突冲突
IX冲突兼容冲突兼容
S冲突冲突兼容兼容
IS冲突兼容兼容兼容

8.3 行锁

【记录锁(Record Locks)

记录锁, 仅仅锁住索引记录的一行。

单条索引记录上加锁,record lock锁住的永远是索引,而非记录本身,即使该表上没有任何索引,那么innodb会在后台创建一个隐藏的聚集主键索引,那么锁住的就是这个隐藏的聚集主键索引。所以说当一条sql没有走任何索引时,那么将会在每一条聚集索引(主键)后面加X锁,这个类似于表锁,但原理上和表锁应该是完全不同的。

记录锁是索引记录上的锁。例如,SELECT c1 FOR UPDATE FROM t WHERE c1 = 10;可以防止任何其它事务插入、 更新或删除t.c1等于10的行。

【间隙锁(Gap Locks)】

区间锁, 仅仅锁住一个索引区间(开区间)。

在索引记录之间的间隙中加锁,或者是在某一条索引记录之前或者之后加锁,并不包括该索引记录本身。

间隙锁会锁定一个范围内的索引,或者是某索引记录之前或者之后的索引。例如,SELECT c1 FOR UPDATE FROM t WHERE c1 BETWEEN 10 and 20;可以防止其他事务将一个t.c1等于15的值插入到表中,无论列中是否有该记录,因为该范围内所有可能存在的值都被锁定。

例如:

create table test(id int,v1 int,v2 int,primary key(id),key idx_v1(v1))Engine=InnoDB DEFAULT CHARSET=UTF8;

该表的记录如下:

idv1v2
110
231
342
553
774
1095

间隙锁(Gap Lock)一般是针对非唯一索引而言的,test表中的v1(非唯一索引)字段值可以划分的区间为:

(-∞,1)
(1,3)
(3,4)
(4,5)
(5,7)
(7,9)
(9, +∞)

假如要更新v1=7的数据行,那么此时会在索引idx_v1对应的值,也就是v1的值上加间隙锁,锁定的区间是(5,7)和(7,9)。同时找到v1=7的数据行的主键索引和非唯一索引,对key加上锁。

select * from t where b = 6 for update;

如果上面查询不存在,则会锁住离6最近的前后两条数据之间的数据行X Gap锁。

【next-key锁(Next-Key Locks)】

record lock + gap lock, 左开右闭区间。

默认情况下,innodb使用next-key locks来锁定记录。
但当查询的索引含有唯一属性的时候,Next-Key Lock 会进行优化,将其降级为Record Lock,即仅锁住索引本身,不是范围。

记录锁和间隙锁的结合,对于InnoDB中,更新非唯一索引对应的记录,会加上Next-Key Lock。如果更新记录为空,就不能加记录锁,只能加间隙锁。

【插入意向锁(Insert Intention Locks)】

Gap Lock中存在一种插入意向锁(Insert Intention Lock),在insert操作时产生。在多事务同时写入不同数据至同一索引间隙的时候,并不需要等待其他事务完成,不会发生锁等待。

假设有一个记录索引包含键值4和7,不同的事务分别插入5和6,每个事务都会产生一个加在4-7之间的插入意向锁,获取在插入行上的排它锁,但是不会被互相锁住,因为数据行并不冲突。

insert加锁过程

简单的insert会在insert的行对应的索引记录上加一个排它锁,这是一个record lock,并没有gap,所以并不会阻塞其他session在gap间隙里插入记录。

不过在insert操作之前,还会加一种锁,官方文档称它为insertion intention gap lock,也就是意向的gap锁。这个意向gap锁的作用就是预示着当多事务并发插入相同的gap空隙时,只要插入的记录不是gap间隙中的相同位置,则无需等待其他session就可完成,这样就使得insert操作无须加真正的gap lock。

假设有一个记录索引包含键值4和7,不同的事务分别插入5和6,每个事务都会产生一个加在4-7之间的插入意向锁,获取在插入行上的排它锁,但是不会被互相锁住,因为数据行并不冲突。

假设发生了一个唯一键冲突错误,那么将会在重复的索引记录上加读锁。当有多个session同时插入相同的行记录时,如果另外一个session已经获得该行的排它锁,那么将会导致死锁。

从mysql的insert 加锁的源码可以看出,insert 插入的时候是用的是LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION (这就是插入意向锁)去检查插入的gap,这个锁模式是与LOCK_S | LOCK_GAP,LOCK_X | LOCK_GAP锁模式冲突的,但对于相同的gap,两个锁模式为LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION,是兼容的。

—行锁的矩阵—

GapInsert IntentionRecordNext-Key
Gap兼容兼容兼容兼容
Insert Intention冲突兼容兼容冲突
Record兼容兼容冲突
Next-Key兼容兼容冲突冲突

表注:横向是已经持有的锁,纵向是正在请求的锁。

由于S锁和S锁是完全兼容的,因此在判别兼容性时只考虑持有的锁与请求的锁是这三种组合情形:X、S和S、X和X、X。

另外,需要提醒注意的是进行兼容判断也只是针对于加锁涉及的行有交集的情形。

  • INSERT操作之间不会有冲突。
  • GAP,Next-Key会阻止Insert。
  • GAP和Record,Next-Key不会冲突
  • Record和Record、Next-Key之间相互冲突。
  • 已有的Insert锁不阻止任何准备加的锁。

8.4 自增(AUTO-INC Locks)

AUTO-INC锁是一种特殊的表级锁,发生涉及AUTO_INCREMENT列的事务性插入操作时产生。

8.5 关于update锁

第一种情况

事务1:

set autocommit=0;
update t_plan_submit 
set t.n_state=4,t.b_valid=0
where id in (''); //或者id =''; 或者id IS NULL;或者id='一个不存在的值'
commit;

事务1不会出现锁表,事务2可以正常执行:

set autocommit=0;
select * from t_plan_submit where id='201705081930438753' for update;
commit;
第二种情况

事务1:

set autocommit=0;
UPDATE t_plan_submit t
set t.n_state=4,t.b_valid=0
where id in (
    select b.id from 
    (
        select a.* from t_plan_submit a where a.id=100
        //或者where 条件后是非主键的列条件
    ) b
);
commit;

事务1(子查询是本身)会出现锁表,事务2要执行等待:

set autocommit=0;
select * from t_plan_submit where id='201705081930438753' for update;
commit;
第三种情况

事务1:

set autocommit=0;

UPDATE t_plan_submit t
set t.n_state=4,t.b_valid=0 
where t.b_valid=1 and t.n_state=1  and  DATEDIFF(t.t_due_tm,NOW())<0
commit;

事务1会出现锁表(即使影响行数是0),事务2要执行等待:

set autocommit=0;
select * from t_plan_submit where id='201705081930438753' for update;
commit;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值