使用数据库进行分布式加锁实现

本文通过实例演示了如何在MySQL中使用forupdate实现行级锁,以及如何在分布式场景下利用行锁实现简单的分布式锁。详细介绍了forupdate的用法,包括在事务中开始、查询和更新数据,以及锁的释放。同时,文章也指出了使用forupdate作为分布式锁可能带来的问题,如锁表风险、锁超时和高并发下的性能问题,并建议只在并发不高的场景下谨慎使用。

1.使用数据库进行分布式加锁

行锁以mysql为例,进行举例 :

1.1 for update

在mysql中使用for update获得行锁。

for update是一种行级锁,又叫排它锁,一旦用户对某个行施加了行级锁,则该用户可以更新也可以查询也可以更新被加锁的数据行,其他用户只能查询,不能更新被加锁的数据行,如果其他用户想更新该表中的数据行,则也必须对表施加行级锁。

释放行级:

1.执行提交commit语句.

2.退出数据库

3.程序停止运行.

通常情况下,select语句是不会对数据加锁的,妨碍影响其他DDL或者DML操作。

select…for update是我们经常使用的手工加锁语句。如果查询条件带有主键,会锁行数据,没有则进行锁表操作。针对innodb引擎。

举个例子:

锁行示例:

drop table if exists data_user;


create table data_user(
id bigint(15) not null primary key,
name varchar(50),
code varchar(50)
)engine=innodb  comment='测试用户表';


insert into data_user (id,name,code) values(1,'name1','code1');
insert into data_user (id,name,code) values(2,'name2','code2');
insert into data_user (id,name,code) values(3,'name3','code3');

在命令行1中输入:

mysql> begin;

select * from data_user where  id = 2 for update;
Query OK, 0 rows affected (0.00 sec)

+----+-------+-------+
| id | name  | code  |
+----+-------+-------+
|  2 | name2 | code2 |
+----+-------+-------+
1 row in set (0.01 sec)

mysql> 

在命令行2中输入

mysql> begin;

select * from data_user where  id = 2 for update;
Query OK, 0 rows affected (0.00 sec)

会发现SQL卡住了,这就是锁行了.

然后再命令行1中操作

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

这时命令行2中就可以看到:

mysql> begin;

select * from data_user where  id = 2 for update;
Query OK, 0 rows affected (0.00 sec)

+----+-------+-------+
| id | name  | code  |
+----+-------+-------+
|  2 | name2 | code2 |
+----+-------+-------+
1 row in set (46.07 sec)

之前卡住的信息又恢复了。

这就是行锁最简单的一个演示,如果在使用for update不加主键那将是锁表。

for update实现分布式锁

使用for update实现一个分布式场景下的锁:

先给出数据库的SQL:

drop table if exists goods;

create table goods(
id bigint(15) not null primary key,
name varchar(50),
dataNum int(5)
)engine=innodb  comment='商品信息';

insert into goods values(1,'mac',800);

先看数据库操作:

public class JdbcOperator {
  private static final String DRIVER_CLASS = "com.mysql.jdbc.Driver";
  private static final String MYSQL_URL =
      "jdbc:mysql://localhost:3306/zsc?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true";
  private static final String USER_NAME = "root";
  private static final String PASSWORD = "123456";

  static {
    // 加载驱动
    try {
      Class.forName(DRIVER_CLASS);
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    }
  }

  /**
   * 批量SQL操作
   *
   * @param sqlList
   * @return
   */
  public static int executeSql(List<String> sqlList) {

    int updateRsp = 0;
    try (Connection connection = DriverManager.getConnection(MYSQL_URL, USER_NAME, PASSWORD); ) {
      // 关闭自动提交
      connection.setAutoCommit(false);

      for (int i = 0; i < sqlList.size(); i++) {
        runStatementSql(sqlList.get(i), connection);
      }

      // 完成后提交事务
      connection.commit();
    } catch (SQLException e) {
      e.printStackTrace();
    }

    return updateRsp;
  }

  private static void runStatementSql(String sql, Connection connection) {
    try (PreparedStatement stat = connection.prepareStatement(sql)) {
      stat.execute();
      System.out.println(Thread.currentThread().getId() + "," + sql);
    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

调用进行扣减库存操作:

public class Goods {

  public Goods() {}

  /** 商品的库存扣减操作 */
  public void minusGoods(int num) {

    List<String> sqlList = new ArrayList<>();
    sqlList.add("begin;");
    sqlList.add("select * from goods where  id = 1 for update;");
    sqlList.add("update goods set dataNum = dataNum-" + num + " where id = 1;");

    // 扣减库存操作
    JdbcOperator.executeSql(sqlList);
  }
}

订单信息:

public class Orders {

  /** 商品服务 */
  private Goods goods;

  public Orders(Goods goods) {
    this.goods = goods;
  }

  /**
   * 创建订单
   *
   * @return
   */
  public boolean createOrder(int num) {
    // 执行扣减库存操作
    goods.minusGoods(num);
    return true;
  }
}

测试分布式场景下的锁:

public class TestOrdersLock {

  @Test
  public void useOrder() throws InterruptedException {
    int orderNumSum = 800;
    Goods goods = new Goods();

    // 并发进行下单操作
    int maxOrder = 4;

    int count = 0;
    for (int i = 0; i < orderNumSum / maxOrder; i++) {
      CountDownLatch startLatch = new CountDownLatch(maxOrder);
      for (int j = 0; j < maxOrder; j++) {
        TaskThreadPool.INSTANCE.submit(
            () -> {
              startLatch.countDown();

              Orders instance = new Orders(goods);
              instance.createOrder(1);
            });

        count++;
      }
      // 执行等待结果
      try {
        startLatch.await();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    System.out.println("结束,共运行:" + count + "次");

    TaskThreadPool.INSTANCE.shutdown();

    Thread.sleep(500);

    System.out.println("shutdown status:" + TaskThreadPool.INSTANCE.getPool().isShutdown());
  }
}

单元测试的结果在控制台可以看到:

15,begin;
12,begin;
13,begin;
16,begin;
19,begin;
17,begin;
14,begin;
18,begin;
13,select * from goods where  id = 1 for update;
13,update goods set dataNum = dataNum-1 where id = 1;
12,select * from goods where  id = 1 for update;
12,update goods set dataNum = dataNum-1 where id = 1;
15,select * from goods where  id = 1 for update;
15,update goods set dataNum = dataNum-1 where id = 1;
19,select * from goods where  id = 1 for update;
19,update goods set dataNum = dataNum-1 where id = 1;
16,select * from goods where  id = 1 for update;
16,update goods set dataNum = dataNum-1 where id = 1;
17,select * from goods where  id = 1 for update;
17,update goods set dataNum = dataNum-1 where id = 1;
14,select * from goods where  id = 1 for update;
14,update goods set dataNum = dataNum-1 where id = 1;
18,select * from goods where  id = 1 for update;
18,update goods set dataNum = dataNum-1 where id = 1;
......
13,begin;
13,select * from goods where  id = 1 for update;
13,update goods set dataNum = dataNum-1 where id = 1;
结束,共运行:800次
17,begin;
17,select * from goods where  id = 1 for update;
17,update goods set dataNum = dataNum-1 where id = 1;
shutdown status:true

检查数据库的结果

mysql> select * from goods;
+----+------+---------+
| id | name | dataNum |
+----+------+---------+
|  1 | mac  |       0 |
+----+------+---------+
1 row in set (0.02 sec)
劣势

讲完了实现,再来说说这个实现的问题吧。

  1. 锁操作过程中如果没有正确的提交,那将导致数据库表被锁表,其他进程将不能操作锁定的表,直白点,将直接引发生产事故。
  2. for update将占用数据库连接,如果在操作过程中使用时间过长,就容易造成锁超时。
  3. 如果并发量过大,会造成数据库直接宕机。

所以此分布式锁使用务必小心,不推荐使用。

适用的场景

使用数据库的for update的加锁机制适用于并发不是特别大的场景,这是因为数据库所能承载的并发是较小的,由于每个锁就占用一个数据库连接,如果操作时间过长,将可能导致锁超时,使用过程也务必要小心,如果某个线程使用了for update,那么在事务运行过程宕机了,那么这个锁将得不到释放,需要手动处理。如果要给出一个参考的数据的话,此并发在几十。

<think>首先,用户的问题是:“我想在分布式系统中对AOP过程进行加锁实现方法 请问如何在分布式环境中对面向切面编程(AOP)操作进行加锁”。这涉及到在分布式系统中使用AOP(面向切面编程)时,如何实现加锁机制。 从系统级指令中,我需要: - 所有行内数学表达式使用$...$格式。 - 独立公式使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识,如[^1]。 用户可见层指令: - 回答结构清晰,帮助用户逐步解决问题。 - 保证回答真实可靠。 参考站内引用提供了相关背景: - [^1]: 使用自定义注解+分布式锁+AOP实现缓存。 - [^2]: AOP通过设置切面拦截调用。 - [^3]: 通过AOP切面方式管理Redis缓存。 - [^4]: 基于SpringBoot AOP实现Redis分布式锁。 - [^5]: AOP用于加锁、cache等通用操作。 这些引用表明,在分布式环境中,AOP常用于实现缓存、加锁等功能,结合Redis等分布式锁机制。 关键点: - AOP用于拦截方法调用并添加额外功能,如加锁。 - 在分布式系统中,加锁需要分布式锁,如使用Redis实现。 - 核心是使用自定义注解和AOP来集成分布式锁。 回答结构: 1. **介绍问题**:解释为什么在分布式系统中需要对AOP操作加锁。 2. **实现方法**:逐步描述如何实现。 - 使用自定义注解定义需要加锁的方法。 - 使用AOP切面拦截这些方法。 - 在切面中集成分布式锁(如Redis锁)。 3. **代码示例**:提供简单的代码示例(如Java/Spring Boot)。 4. **注意事项**:讨论分布式锁的挑战,如死锁、超时等。 5. **生成相关问题**:在末尾添加相关问题。 确保使用中文。 生成相关问题: - 基于用户问题,相关问题可以包括: 1. 如何选择分布式锁实现方式? 2. AOP切面中如何处理锁的异常? 3. 分布式锁的性能优化方法? 4. 如何避免分布式环境中的死锁? 5. AOP在其他分布式场景的应用? 最后,引用标识:在回答中自然地添加引用标识,如解释概念时引用[^2],讨论实现时引用[^4]。 完整回答大纲: 1. **引言**:简述分布式系统和AOP加锁的需求。 2. **实现步骤**: a. 定义自定义注解。 b. 创建AOP切面。 c. 集成分布式锁(如Redis)。 d. 处理锁的获取和释放。 3. **代码示例**:提供伪代码或简化代码。 4. **注意事项**:讨论可靠性和性能。 5. **相关问题**:列出3-5个相关问题。</think>### 分布式环境中对AOP操作进行加锁实现方法 在分布式系统中,面向切面编程(AOP)常用于实现横切关注点(如加锁、缓存、日志等),但分布式环境下的并发控制需要分布式锁来确保操作的原子性和一致性。AOP通过拦截目标方法调用,在切面中集成分布式锁机制(如Redis或ZooKeeper),可以有效防止多个节点同时访问共享资源。以下是一个清晰、逐步的实现方法,基于Spring Boot框架和Redis分布式锁(参考引用[^4]),确保真实可靠。 #### 实现步骤 1. **定义自定义注解**:创建一个注解来标记需要加锁的方法,便于AOP切面识别。 ```java import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) // 标注在方法上 @Retention(RetentionPolicy.RUNTIME) // 运行时生效 public @interface DistributedLock { String lockKey(); // 锁的键名,通常基于方法参数或资源标识 long expireTime() default 30000; // 锁超时时间(毫秒),默认30秒 } ``` 2. **实现分布式锁工具类**:使用Redis实现分布式锁的核心逻辑,确保原子性操作(如SETNX命令)。 ```java import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; @Component public class RedisLockUtil { private final StringRedisTemplate redisTemplate; public RedisLockUtil(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } // 获取锁:使用SETNX和EXPIRE保证原子性 public boolean acquireLock(String lockKey, long expireTime) { Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", expireTime, TimeUnit.MILLISECONDS); return Boolean.TRUE.equals(result); } // 释放锁 public void releaseLock(String lockKey) { redisTemplate.delete(lockKey); } } ``` - **关键点**:使用`setIfAbsent`(Redis的SETNX)并设置过期时间,避免死锁。锁键(`lockKey`)通常基于方法参数或资源ID生成,确保全局唯一性[^4]。 3. **创建AOP切面集成分布式锁**:定义切面拦截被`@DistributedLock`注解标记的方法,在方法执行前加锁,执行后释放锁。 ```java import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Aspect @Component public class DistributedLockAspect { @Autowired private RedisLockUtil redisLockUtil; @Around("@annotation(distributedLock)") // 拦截自定义注解 public Object aroundMethod(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable { String lockKey = distributedLock.lockKey(); long expireTime = distributedLock.expireTime(); try { // 尝试获取锁,如果失败则等待或重试 if (!redisLockUtil.acquireLock(lockKey, expireTime)) { throw new RuntimeException("获取分布式锁失败,资源已被占用"); } // 执行目标方法 return joinPoint.proceed(); } finally { // 确保释放锁 redisLockUtil.releaseLock(lockKey); } } } ``` - **AOP机制**:AOP框架(如Spring AOP)通过动态代理拦截方法调用,在`@Around`通知中织入加锁逻辑。ClassFilter和MethodMatcher用于精确匹配目标方法[^2]。 - **分布式锁集成**:在切面中调用`RedisLockUtil`,确保锁的获取和释放与业务逻辑解耦,提高可维护性[^1][^4]。 4. **在业务方法中使用注解**:将`@DistributedLock`注解应用于需要加锁的方法。 ```java @Service public class OrderService { @DistributedLock(lockKey = "'order_lock_' + #orderId", expireTime = 5000) // 锁键基于订单ID public void updateOrder(String orderId) { // 业务逻辑,如更新数据库 System.out.println("处理订单: " + orderId); } } ``` - **锁键生成**:使用Spring EL表达式(如`#orderId`)动态生成键,避免硬编码。 #### 注意事项 - **原子性与可靠性**:分布式锁必须保证原子操作(Redis的SETNX是原子的),但需处理网络分区或节点故障。推荐使用RedLock法或结合Lua脚本增强可靠性[^4]。 - **超时与死锁**:设置合理的锁过期时间(`expireTime`),防止线程阻塞导致死锁。finally块确保锁释放,避免资源泄漏。 - **性能影响**:加锁会增加延迟,建议仅在共享资源(如数据库行)上使用。测试并发场景下的吞吐量,优化锁粒度(如细粒度锁键)。 - **异常处理**:在切面中捕获异常并重试或回滚,确保业务连续性。参考AOP的异常通知(@AfterThrowing)实现回退逻辑[^2]。 - **依赖配置**:在Spring Boot项目中,添加Redis和AOP依赖(引用[^3]): ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> ``` #### 总结 通过自定义注解+AOP切面+分布式锁(如Redis),可以在分布式环境中高效实现AOP操作的加锁。这种方法提升了代码的可扩展性和可维护性,同时降低开发成本(引用[^1])。核心是确保锁的原子获取/释放,并结合AOP的拦截机制。实际部署时,需监控锁竞争和性能指标。 ### 相关问题 1. 如何选择分布式锁实现方式(如Redis vs ZooKeeper)? 2. AOP切面中如何处理分布式锁的异常和重试机制? 3. 分布式锁的性能优化有哪些常见策略? 4. 如何避免分布式环境中的死锁和活锁问题? 5. AOP在分布式系统中还有哪些典型应用场景(如缓存、日志)?[^5] [^1]: 使用自定义注解结合AOP实现分布式锁和缓存的功能可以提高代码的可维护性、可扩展性和性能。 [^2]: AOP通过设置切面拦截方法调用,基于ClassFilter和MethodMatcher进行匹配。 [^3]: 在Spring Boot项目中引入切面依赖是实现AOP的基础。 [^4]: 基于Redis实现分布式锁的核心是将资源键放入Redis并利用其原子操作。 [^5]: AOP常用于加锁、缓存等横切关注点,提升代码复用性。
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值