解决原子性问题

原子性问题
原子性问题的源头是线程切换,如果能够禁用线程切换就可以解决原子性的问题,而操作系统做线程切换是依赖CPU中断的,所以禁用CPU发送中断就可以禁止线程切换。

在单核时代很容易实现:但是在多核场景下,同一时刻有可能有两个线程同时在执行,一个线程执行在CPU1上,一个线程执行在CPU2上,此时禁止CPU中断,只能保证COU上的线程连续执行,并不能保证同一时刻只有一个线程执行。

同一时刻只有一个线程执行,我们称之为互斥,如果我们能够保证对共享变量的修改是互斥的,那么就可以保证原子性。

Java提供的锁技术:synchronized

class X {
  // 修饰非静态方法
  synchronized void foo() {
    // 临界区
  }
  // 修饰静态方法
  synchronized static void bar() {
    // 临界区
  }
  // 修饰代码块
  Object obj = new Object();
  void baz() {
    synchronized(obj) {
      // 临界区
    }
  }
}  

Java一条隐士规则:
1.当修饰静态方法的时候,锁定的是当前类的Class对象
2.当修饰非静态方法时,锁定的时当前实例对象this

下面分析一下下面的场景:

class SafeCalc {
  long value = 0L;
  long get() {
    return value;
  }
  synchronized void addOne() {
    value += 1;
  }
}

synchronized修饰的的临界区时互斥的,同一个时刻只有一个线程能够执行临界区里的代码,而对一个对象的解锁操作 Happens-Before 于后续对这个对象的加锁操作,综合Happens-Before的传递性原则 可以得出 一个线程在临界区修改共享变量(在解锁之前)对于后续进入临界区的线程(该操作在解锁之后)是可见的,所以使用synchronized关键字可以解决两个线程修改共享变量的值和预期的不一样的问题。但只能get是没有synchronized修饰的,根据管程中锁的原则,解锁的操作总保证对于后续的解锁是可见的,而Get方法并没有加锁操作,所以没法保证可见性。

式列2:

class SafeCalc {
  static long value = 0L;
  synchronized long get() {
    return value;
  }
  synchronized static void addOne() {
    value += 1;
  }
}

这里使用两把锁保护同一资源,一个对象是this,一个对象是Class,由于临界区是两把锁保护的,因而这两个临界区没有互斥性,这样就会导致并发问题了。

式列3

class SafeCalc {
  long value = 0L;
  long get() {
    synchronized (new Object()) {
      return value;
    }
  }
  void addOne() {
    synchronized (new Object()) {
      value += 1;
    }
  }
}

加锁本质就是在锁对象的对象头中写入当前线程id,但是new object每次在内存中都是新对象,所以加锁无效。这样写会被Jvm逃逸分析的优化后synchronized代码会被优化掉,等于没加锁(而且这种new出来只在一个地方使用的对象,其它线程不能对它解锁,这个锁会被编译器优化掉)

<think>我们使用Redis的SET命令的扩展参数来实现原子操作。具体来说,使用SET命令的NX选项(当键不存在时设置)和EX选项(设置过期时间,单位为秒)来一次性完成设置键和过期时间,从而保证原子性。 在Java代码中,我们可以使用Jedis等客户端来执行这样的命令。假设我们使用的是Jedis,那么代码可以这样写: 原始代码中使用了setnx和expire两个命令,现在替换为: String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime); 这个命令的含义是:当lockKey不存在时(NX),设置lockKey的值为requestId,并设置过期时间为expireTime秒(EX)。这个操作是原子的。 因此,在优化后的代码中,我们不再分开使用setnx和expire,而是使用一条SET命令。 但是注意,在问题中给出的方法参数中,使用的是LkmRedisRemote这个自定义的Redis操作接口。因此,我们需要确保该接口支持set命令的扩展参数(即支持NX和EX选项)。如果该接口不支持,则需要扩展接口或者使用其他方式(如使用eval命令执行lua脚本)来保证原子性。 下面我们给出使用SET命令优化后的代码片段: 原代码: Long result = redisRemote.setnx(getRedisLock(lockKey), requestId); if (result != null && result == 1L) { redisRemote.expire(getRedisLock(lockKey), expireTime); return true; } 优化为: // 使用原子操作SET key value NX EX expireTime String result = redisRemote.set(lockKey, requestId, "NX", "EX", expireTime); if ("OK".equals(result)) { return true; } 注意:这里假设redisRemote.set方法返回的是String类型,并且当设置成功时返回"OK"。 如果LkmRedisRemote接口没有提供这样的set方法,那么我们可以通过执行Lua脚本来保证原子性。下面给出Lua脚本的方式: 使用Lua脚本: String script = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then " + "redis.call('expire', KEYS[1], ARGV[2]) " + "return 1 " + "else " + "return 0 " + "end"; Object result = redisRemote.eval(script, 1, lockKey, requestId, String.valueOf(expireTime)); if (result != null && result.equals(1L)) { return true; } 但是,使用Lua脚本的方式需要确保Redis服务器支持Lua脚本,并且注意参数传递。 然而,在Redis2.6.12及以上版本,直接使用SET命令是更推荐的方式,因为它更简单、高效。 因此,我们优先推荐使用SET命令的扩展参数。如果LkmRedisRemote接口不支持,我们可以扩展接口或者使用Lua脚本。 下面,我们按照要求,在回答后提出相关问题。</think>### 解决 SETNX+EXPIRE 非原子问题的方案 #### 1. Redis 原生原子方案(推荐) **使用 Redis 2.6.12+ 的扩展 SET 命令** ```java // 优化后的原子操作代码 String result = redisRemote.set( lockKey, requestId, "NX", // 仅当键不存在时设置 "EX", // 设置过期时间单位(秒) expireTime // 过期时间值 ); if ("OK".equals(result)) { return true; // 原子获取锁成功 } ``` **优势**: - 单条命令原子执行 - 无需额外操作 - 高性能(仅需1次网络往返) **参数说明**: - `NX`:等价于 SETNX 的原子实现 - `EX`:设置过期时间(秒),也可用 `PX` 表示毫秒 #### 2. Lua 脚本方案(兼容旧版本) ```java // Lua 脚本实现原子操作 String luaScript = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then\n" + " redis.call('expire', KEYS[1], ARGV[2])\n" + " return 1\n" + "else\n" + " return 0\n" + "end"; Object result = redisRemote.eval( luaScript, 1, // KEYS 数量 lockKey, // KEYS[1] requestId, // ARGV[1] expireTime // ARGV[2] ); if (result != null && result.equals(1L)) { return true; } ``` **优势**: - 兼容 Redis 2.6 以下版本 - 保证多个命令的原子执行 - 避免网络分区导致的不一致 #### 3. 事务方案(不推荐) ```java // 使用 WATCH/MULTI/EXEC(存在局限性) redisRemote.watch(lockKey); boolean exists = redisRemote.exists(lockKey); if (!exists) { Transaction tx = redisRemote.multi(); tx.setnx(lockKey, requestId); tx.expire(lockKey, expireTime); List<Object> results = tx.exec(); if (results != null && results.size() == 2) { return true; } } redisRemote.unwatch(); ``` **缺点**: - 事务可能被中断(WATCH失败) - 性能较低(多次网络往返) - 实现复杂,不推荐用于分布式锁 ### 关键注意事项 1. **版本兼容性** - SET NX EX 需要 Redis ≥ 2.6.12 - 生产环境应检查 Redis 版本:`redisRemote.info("server")` 2. **错误处理** ```java try { // 原子操作代码 } catch (RedisException e) { // 处理Redis连接/执行异常 log.error("Redis lock failed: {}", e.getMessage()); return false; } ``` 3. **时间单位一致性** - 确保 `expireTime` 单位与参数一致(EX=秒,PX=毫秒) - 推荐使用明确的时间单位常量 ### 最佳实践建议 1. 优先使用 `SET key value NX EX timeout` 方案 2. 在基础架构层强制 Redis 版本 ≥ 3.0(2015年发布) 3. 添加版本检查逻辑: ```java if (redisVersion < 2.6) { throw new RuntimeException("Redis 2.6+ required for atomic locking"); } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值