最全的开发中遇到的《锁》(多种分类)

共享锁(S锁)

又称为读锁,可以查看但无法修改和删除的一种数据锁。如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排它锁。获准共享锁的事务只能读数据,不能修改数据。共享锁下其它用户可以并发读取,查询数据。但不能修改,增加,删除数据。资源共享.

在Java中实现共享锁通常是通过java.util.concurrent.locks.ReentrantReadWriteLock类的读锁来完成的。读锁允许多个线程同时持有读锁,从而实现共享资源的并发读取。

以下是一个使用ReentrantReadWriteLock实现共享锁的简单示例:

import java.util.concurrent.locks.ReentrantReadWriteLock;
 
public class SharedLockExample {
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
 
    public void read() {
        rwLock.readLock().lock();
        try {
            // 执行读操作
        } finally {
            rwLock.readLock().unlock();
        }
    }
 
    public void write() {
        rwLock.writeLock().lock();
        try {
            // 执行写操作
        } finally {
            rwLock.writeLock().unlock();
        }
    }
 
    public static void main(String[] args) {
        SharedLockExample example = new SharedLockExample();
 
        // 多个读操作可以并发执行
        new Thread(() -> example.read()).start();
        new Thread(() -> example.read()).start();
 
        // 写操作将会阻塞,直到所有的读操作完成
        new Thread(() -> example.write()).start();
    }
}

排它锁(X锁)

又称为写锁、独占锁,若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A

排它锁(也称为独占锁)在Java中通常是通过synchronized关键字或ReentrantLock类实现的。以下是使用这两种方式实现排它锁的简单示例:

  1. 使用synchronized关键字:
public class MutexExample {
 
    private Object mutex = new Object();
 
    public void synchronizedMethod() {
        synchronized (mutex) {
            // 临界区代码
        }
    }
}
  1. 使用ReentrantLock类:
import java.util.concurrent.locks.ReentrantLock;
 
public class ReentrantLockExample {
 
    private ReentrantLock lock = new ReentrantLock();
 
    public void lockMethod() {
        lock.lock();
        try {
            // 临界区代码
        } finally {
            lock.unlock();
        }
    }
}

在这两种情况下,当一个线程进入同步方法或同步块时,其他线程必须等待该线程退出同步块才能执行。这确保了在同一时刻只有一个线程可以执行临界区代码。

互斥锁

在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

在Java中,互斥锁通常是通过java.util.concurrent.locks.Lock接口实现的。Lock接口提供了比使用synchronized方法和语句可得到的更广泛的锁操作。

以下是使用Lock接口实现互斥锁的一个简单例子:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class MutexExample {
    private final Lock lock = new ReentrantLock();
 
    public void doSynchronizedWork() {
        lock.lock(); // 获取锁
        try {
            // 在此处添加需要同步的代码
            // ...
        } finally {
            lock.unlock(); // 释放锁,确保释放锁,即使发生异常也不能遗漏
        }
    }
}

在这个例子中,ReentrantLock类实现了Lock接口。通过调用lock()方法获取锁,并在完成同步代码后调用unlock()方法释放锁。使用try...finally块确保即使同步代码抛出异常,锁也能被释放,防止死锁的发生。

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。

在Java中,悲观锁通常通过synchronized关键字或者ReentrantLock类实现。以下是使用ReentrantLock实现悲观锁的示例:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class PessimisticLockExample {
    private final Lock lock = new ReentrantLock();
 
    public void performOptimisticOperation() {
        // 尝试获取锁
        lock.lock();
        try {
            // 在这个区域内执行需要乐观控制的代码
            // ... 你的代码 ...
        } finally {
            // 释放锁,以避免死锁
            lock.unlock();
        }
    }
 
    public static void main(String[] args) {
        PessimisticLockExample example = new PessimisticLockExample();
        example.performOptimisticOperation();
    }
}

在这个例子中,ReentrantLock实例是悲观锁,因为它尝试通过lock()方法获取对共享资源的独占访问权限。在获取锁之后,代码在try块内执行,并且确保在finally块中释放锁,以防止发生异常时造成死锁。

乐观锁

顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

乐观锁在Java中通常通过使用版本号或者时间戳来实现。以下是一个简单的Java实现示例:

public class OptimisticLockingExample {
 
    // 假设有一个带有版本号字段的实体类
    public static class EntityWithVersion {
        private int id;
        private String data;
        private int version;
 
        // 构造函数、getter和setter省略
 
        // 更新数据的方法,带有乐观锁的实现
        public void updateData(String newData, int currentVersion) {
            if (this.version != currentVersion) {
                throw new OptimisticLockingException("Version mismatch");
            }
            this.data = newData;
            this.version++; // 更新版本号
        }
    }
 
    public static void main(String[] args) {
        EntityWithVersion entity = new EntityWithVersion(1, "Initial data", 0);
 
        // 假设这是第一个线程尝试更新数据
        entity.updateData("Updated data", 0); // 成功更新
 
        // 现在有另一个线程尝试同样的操作但是使用旧的版本号
        try {
            entity.updateData("Duplicate update", 0); // 引发异常,因为版本号不匹配
        } catch (OptimisticLockingException e) {
            // 处理乐观锁冲突
        }
    }
}
 
class OptimisticLockingException extends RuntimeException {
    public OptimisticLockingException(String message) {
        super(message);
    }
}

在这个例子中,EntityWithVersion 类有一个版本号字段 version。每次更新数据时,都会检查版本号是否与传入的版本号相同。如果不同,则抛出 OptimisticLockingException 异常,表示有乐观锁冲突。这种方法可以避免使用传统的数据库锁定机制,从而提高系统的并发性能。

行级锁

 行级锁是 MySQL 中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突,其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁和排他锁。开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

在Java中实现行级锁通常涉及到数据库的事务控制和锁机制。如果你使用的是JDBC,可以通过Connection对象来控制事务并获取行级锁。

以下是一个简单的例子,展示了如何在JDBC中使用行级锁来控制并发访问:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
 
public class RowLevelLockingExample {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/yourdatabase";
        String username = "yourusername";
        String password = "yourpassword";
 
        Connection connection = null;
        PreparedStatement preparedStatement = null;
 
        try {
            // 建立连接
            connection = DriverManager.getConnection(url, username, password);
            
            // 设置手动提交
            connection.setAutoCommit(false);
            
            // 查询语句,使用for update来获取行级排他锁
            String sql = "SELECT * FROM your_table WHERE id = ? FOR UPDATE";
            preparedStatement = connection.prepareStatement(sql);
            
            // 设置查询参数
            preparedStatement.setInt(1, 1);
            
            // 执行查询
            preparedStatement.executeQuery();
            
            // 在这里执行你的业务逻辑
            
            // 提交事务
            connection.commit();
            
        } catch (SQLException e) {
            try {
                if (connection != null) {
                    // 发生异常时回滚事务
                    connection.rollback();
                }
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
            e.printStackTrace();
        } finally {
            try {
                if (preparedStatement != null) {
                    preparedStatement.close();
                }
                if (connection != null) {
                    connection.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

在这个例子中,FOR UPDATE语句会在查询时获取行级的排他锁。其他事务将不能在获取锁的行上执行任何类型的DML操作(除非它们也使用FOR UPDATE),直至锁被释放。

请注意,这个例子假设你已经有了一个名为yourdatabase的数据库,其中有一个名为your_table的表,并且表中有一个名为id的字段。确保你的数据库引擎支持事务(如InnoDB),并且已经启用了行级锁。

表级锁

表级锁是 MySQL 中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分 MySQL 引擎支持。最常使用的 MyISAM 与 InnoDB 都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。

在Java中,可以通过JDBC的Connection对象来实现表级锁。表级锁通常用于保证数据的完整性和一致性,在并发环境下防止数据被多个事务同时修改。

以下是使用java.sql.Connection实现表级锁的示例代码:

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
 
public class TableLockExample {
 
    public void lockTable(Connection connection, String tableName) throws SQLException {
        // 使用MySQL的表级锁定语法
        String sql = "LOCK TABLES " + tableName + " WRITE";
        PreparedStatement statement = connection.prepareStatement(sql);
        statement.executeUpdate();
        // 注意:在实际使用中,应该在try-finally块中释放锁
        // 以确保即使在发生异常的情况下也能释放锁
    }
 
    public void unlockTable(Connection connection, String tableName) throws SQLException {
        String sql = "UNLOCK TABLES";
        PreparedStatement statement = connection.prepareStatement(sql);
        statement.executeUpdate();
    }
 
    public static void main(String[] args) {
        // 假设已经有一个数据库连接
        Connection connection = ...;
        TableLockExample tableLock = new TableLockExample();
 
        try {
            // 锁定表
            tableLock.lockTable(connection, "my_table");
 
            // 在这里执行需要表锁的操作
 
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                // 释放表锁
                tableLock.unlockTable(connection, "my_table");
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

请注意,这个例子使用了MySQL的语法来锁定表。不同的数据库系统可能有不同的语法或方法来实现表级锁。另外,在实际应用中,应该总是在finally块中释放锁,确保即使在发生异常的情况下锁也能被释放。

页级锁

页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁,一次锁定相邻的一组记录。BDB 支持页级锁。开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

在Java中,页级锁通常是由底层数据库或文件系统提供支持的,而不是由Java语言本身直接提供。如果你需要在Java中实现类似的功能,你可能需要借助JNI(Java Native Interface)或者直接使用Java Cryptography Extension(JCE)中的一些加密API,这些API可以提供对文件加锁的支持。

如果你想要在Java中实现类似数据库页级锁的功能,你可以使用第三方库,例如Apache Derby的XADataSource,它支持分布式事务和两阶段提交。

以下是一个简单的示例,使用Java Cryptography Extension来锁定文件:

import javax.crypto.Cipher;
import java.io.File;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;
import java.security.Key;
import java.security.SecureRandom;
 
public class FileLockExample {
    public static void main(String[] args) {
        try {
            File file = new File("test.txt");
            RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
 
            // 使用Cipher生成一个密钥
            Key key = Cipher.generateKey(Cipher.TYPE_SECRET_KEY);
 
            // 随机生成一个文件锁
            SecureRandom sr = new SecureRandom();
            byte[] token = new byte[20];
            sr.nextBytes(token);
 
            // 尝试获取文件锁
            FileLock lock = randomAccessFile.getChannel().lock(0, Long.MAX_VALUE, true, token, key);
 
            // 执行文件操作
            // ...
 
            // 释放文件锁
            lock.release();
 
            // 关闭文件
            randomAccessFile.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

请注意,这个例子使用了Cipher.generateKey和SecureRandom来生成一个伪随机的锁,并且使用了一个密钥来尝试锁定整个文件。这不是一个真正的页级锁,而是一个简化的文件锁实现,用于演示如何在Java中实现文件锁的概念。实际的页级锁通常需要操作系统和数据库的支持。

丢失修改

指事务1和事务2同时读入相同的数据并进行修改,事务2提交的结果破坏了事务1提交的结果,导致事务1进行的修改丢失。

"java 丢失修改"这个问题可能是指在使用Java进行数据库操作时,更新数据失败的问题。这种情况通常是由于事务未正确提交导致的。

以下是一个简单的Java代码示例,演示了如何使用JDBC处理事务并实现数据修改:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
 
public class JdbcUpdateExample {
    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
 
        try {
            // 注册JDBC驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
 
            // 打开连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password");
 
            // 设置自动提交为false
            conn.setAutoCommit(false);
 
            // 执行查询
            stmt = conn.createStatement();
            String sql = "UPDATE mytable SET field = 'value' WHERE condition";
            stmt.executeUpdate(sql);
 
            // 提交事务
            conn.commit();
            System.out.println("修改成功");
        } catch (ClassNotFoundException e) {
            // 处理驱动注册错误
            e.printStackTrace();
        } catch (SQLException e) {
            // 处理SQL错误
            try {
                if (conn != null) {
                    // 发生异常时回滚事务
                    conn.rollback();
                    System.out.println("修改失败,已回滚");
                }
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
            e.printStackTrace();
        } finally {
            // 关闭资源
            try {
                if (stmt != null) stmt.close();
            } catch (SQLException se2) {
                se2.printStackTrace();
            }
            try {
                if (conn != null) conn.close();
            } catch (SQLException se) {
                se.printStackTrace();
            }
        }
    }
}

在这个示例中,我们首先设置了数据库连接的自动提交属性为false,这意味着我们将自己负责提交或回滚事务。在执行更新操作后,我们调用commit()方法提交事务。如果在操作过程中发生了异常,我们通过捕获异常来回滚事务,确保数据的一致性。这是处理数据库修改丢失问题的一个基本模板。

不可重复读

一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!

Java中的“不可重复读”通常指的是在数据库事务中,一个事务读取数据后,再次读取数据时,数据发生了改变。为了实现不可重复读,可以将数据库事务隔离级别设置为READ_COMMITTED(读取已提交),这样在事务中间时期其他事务的更新会对当前事务可见,从而导致不可重复读问题。

以下是一个简单的例子,演示如何在Java中使用JDBC处理不可重复读的问题:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
 
public class UnrepeatedReadExample {
    private static final String URL = "jdbc:mysql://localhost:3306/your_database";
    private static final String USERNAME = "your_username";
    private static final String PASSWORD = "your_password";
 
    public static void main(String[] args) {
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
 
        try {
            // 设置数据库连接的隔离级别为READ_COMMITTED
            connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
            connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
 
            connection.setAutoCommit(false); // 开始事务
 
            // 第一次查询
            statement = connection.prepareStatement("SELECT value FROM your_table WHERE id = 1");
            resultSet = statement.executeQuery();
            if (resultSet.next()) {
                System.out.println("First read: " + resultSet.getString("value"));
            }
 
            // 模拟其他事务更新数据
            statement = connection.prepareStatement("UPDATE your_table SET value = 'new_value' WHERE id = 1");
            statement.executeUpdate();
            connection.commit(); // 提交事务
 
            // 第二次查询,数据已经发生改变
            resultSet.beforeFirst(); // 重新定位结果集指针
            if (resultSet.next()) {
                System.out.println("Second read: " + resultSet.getString("value"));
            }
 
            connection.commit(); // 提交事务
        } catch (SQLException e) {
            try {
                if (connection != null) {
                    connection.rollback(); // 回滚事务
                }
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
            e.printStackTrace();
        } finally {
            try {
                if (resultSet != null) {
                    resultSet.close();
                }
                if (statement != null) {
                    statement.close();
                }
                if (connection != null) {
                    connection.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

在这个例子中,我们使用了Connection.setTransactionIsolation()方法来设置事务的隔离级别为READ_COMMITTED。这样在事务中,即使其他事务提交了对数据的更新,也会被我们的事务感知到,导致不可重复读问题的发生。记得在操作完成后,要关闭相关的资源,并处理异常。

读脏数据

事务T1修改某一数据,并将其写回磁盘,事务T2读取同一数据后,T1由于某种原因被撤消,这时T1已修改过的数据恢复原值,T2读到的数据就与数据库中的数据不一致,则T2读到的数据就为"脏"数据,即不正确的数据。

在Java中解决脏读通常涉及到数据库事务的隔离级别。脏读是指一个事务读取了另一个事务尚未提交的数据。为了避免脏读,可以将事务的隔离级别设置为READ_COMMITTED。

以下是一个简单的例子,演示如何在Java中使用JDBC设置事务隔离级别,从而避免脏读:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Savepoint;
 
public class TransactionExample {
    public static void main(String[] args) {
        Connection conn = null;
        Savepoint savepoint = null;
 
        try {
            // 加载并注册JDBC驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
 
            // 建立数据库连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password");
 
            // 设置事务隔离级别为READ_COMMITTED
            conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
 
            // 开启事务
            conn.setAutoCommit(false);
 
            // 执行数据库操作...
 
            // 保存保点
            savepoint = conn.setSavepoint();
 
            // 提交事务
            conn.commit();
 
        } catch (ClassNotFoundException | SQLException e) {
            try {
                if (conn != null && !conn.isClosed()) {
                    if (savepoint != null) {
                        conn.rollback(savepoint);
                    } else {
                        conn.rollback();
                    }
                    conn.close();
                }
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
            e.printStackTrace();
        }
    }
}

在这个例子中,我们首先设置了数据库连接的事务隔离级别为TRANSACTION_READ_COMMITTED,然后开始一个事务,在事务中执行数据库操作。如果操作成功,我们提交事务;如果操作失败,我们回滚事务。这样可以避免脏读问题。

死锁

 两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
死锁四个产生条件:

1)互斥条件:  指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。

2)请求和保持条件: 指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

3)不剥夺条件: 指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

4)环路等待条件: 指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

预防死锁打破上述之一的条件。


java 解决死锁问题

死锁是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种僵局,无一个进程或线程能够继续执行。在Java中,死锁问题通常是由于两个或多个线程相互等待对方释放资源,导致它们都无法继续执行。

为了解决死锁问题,可以采取以下策略:

  1. 避免嵌套锁:尽量不在持有一个锁的情况下去申请另一个锁。
  2. 锁顺序统一:当你需要获取多个锁时,确保所有线程以相同的顺序获取锁。
  3. 使用定时锁:对于那些不能一次就立即获取到所有锁的情况,可以使用Lock.tryLock(timeout, unit)方法,它可以在等待一定时间后放弃锁。
  4. 检测死锁:JVM提供了工具来检测死锁,例如使用jstack工具。
  5. 使用锁层次结构:通过定义锁的层次结构,确保按层次顺序获取锁。

下面是一个简单的Java代码示例,演示了如何使用ReentrantLock和Condition来避免死锁,并提供了一个锁顺序的例子:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class DeadlockExample {
 
    private Lock lockA = new ReentrantLock();
    private Lock lockB = new ReentrantLock();
 
    public void deadlock() {
        lockA.lock();
        try {
            // 假设这里做了一些操作
            lockB.lock();
            try {
                // 操作B
            } finally {
                lockB.unlock();
            }
        } finally {
            lockA.unlock();
        }
    }
 
    public void correctOrder() {
        Lock locks[] = new Lock[]{lockA, lockB};
        // 对锁进行排序,确保获取锁的顺序一致
        Arrays.sort(locks, (o1, o2) -> Integer.compare(System.identityHashCode(o1), System.identityHashCode(o2)));
 
        locks[0].lock();
        try {
            // 操作A
            locks[1].lock();
            try {
                // 操作B
            } finally {
                locks[1].unlock();
            }
        } finally {
            locks[0].unlock();
        }
    }
}

在这个例子中,deadlock方法可能会导致死锁,因为线程在获取lockA后,尝试获取lockB,同时没有释放lockA。而correctOrder方法通过排序锁,确保了在获取下一个锁之前,先释放当前持有的锁,从而避免了死锁的发生。

  • 47
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值