在redis++中使用事务和watch应对hash并发

并发

这里针对Redis操作中的并发,有两个层面的意思:

  • 在单进程中,同时多个线程执行Redis写操作
    • 在同一个连接中
    • 在不同的连接中
  • 在多个进程中,同时执行Redis写操作,相当于上述场景同时存在多个

假设以下应用,对 hash 表执行以下操作(更新值):

  1. hget 读取指定 field 的 value
  2. 修改 value
  3. hset 重新写回

在单线程中执行上述操作是没有任何问题的,下面分析一下多线程的情况。

事务

在多线程环境中,当只有一个连接时,可能会出现以下问题:

  • 线程A执行,步骤1完成,开始执行步骤2
  • 线程B执行步骤1,此时读取的值仍然是旧值
  • 线程A步骤2执行完成,使用hset更新了值
  • 线程B执行步骤2和步骤3

如此一来,线程B更新的就不是最新的值,已经产生了数据竞争。

这种情况,可以使用事务来解决。

Redis中事务使用 multi…exec 指令来完成,在事务中的指令:

  • 事务是一个被隔离的操作,事务中的方法都会被 Redis 进行序列化并按顺序执行,事务在执行的过程中不会被其他客户端发生的命令所打断
  • 事务是一个原子性的操作,它要么全部执行,要么就什么都不执行

于是,使用了事务,线程之间的指令就会被序列化执行,不会再出现竞争的情况。

示例代码如下:

auto tx = redis.transaction();
tx.set("key", "1").incr("2").exec();

这样,set设置key和incr增加值的操作就是原子的。

但由于事务的所有操作都是在调用exec后执行的,所以读取操作不能放在事务中。

而如果先读取,再执行事务的话,可能会有其他线程在执行事务中再次读取,又造成数据一致性问题。

所以还要使用 WATCH。

乐观锁

想要原子地完成获取值、修改值、更新值操作,需要借助WATCH指令实现一个乐观锁的功能。

WATCH用于监视指定的key是否被改变,如果被改变,事务会执行失败,否则才执行成功。

这样就保证了,在当前线程更新值时,如果有其他线程更新了值,就拒绝执行,一般应用层重试即可。

示例代码如下:

auto redis = Redis(opts, pool_opts);

// If the watched key has been modified by other clients, the transaction might fail.
// So we need to retry the transaction in a loop.
while (true) {
    try {
        // Create a transaction without creating a new connection.
        auto tx = redis.transaction(false, false);

        // Create a Redis object from the Transaction object. Both objects share the same connection.
        auto r = tx.redis();

        // Watch a key.
        r.watch("key");

        // Get the old value.
        auto val = r.hget("key", "field");
        auto num = 0;
        if (val) {
            num = std::stoi(*val);
        } // else use default value, i.e. 0.

        // Incr value.
        ++num;

        // Execute the transaction.
        auto replies = tx.hset("key", "field", std::to_string(num)).exec();

        // Transaction has been executed successfully. Check the result and break.

        assert(replies.size() == 1 && replies.get<bool>(0) == false);

        break;
    } catch (const WatchError &err) {
        // Key has been modified by other clients, retry.
        continue;
    } catch (const Error &err) {
        // Something bad happens, and the Transaction object is no longer valid.
        throw;
    }
}

这样,就保证了线程间数据安全。一般情况下,乐观锁应用于发生数据碰撞概率较低的场景下,所以重试次数一般不会太多。

使用这种模式,即使是多个进程同时操作一个Redis实例,也可以保证各客户端的数据一致性。

针对hash中的field

Redis 支持的WATCH指令只能针对key。

对于hash表来说,一个key下可能有成千上万的field,当更改其中一条记录时,其他记录的更改并不影响。

如果直接WATCH key,那么任何一个field的变动都会导致事务执行失败,这会影响执行效率。

这里就提供一个变通的思路,只要规定任何修改hash数据的操作都遵循以下步骤:

  1. WATCH key:field:lock,其中key:field:lock为根据hash的key和field组建的唯一字符串,由监听key变通为监听这个字符串
  2. HGET KEY FIELD, 并修改当前值为updated_value
  3. MULTI,开始事务
  4. SET key:field:lock “”,在事务中修改监听的字符串,如果当前事务执行成功,其他修改该值的事务必然失败,以此达到同步的目的
  5. HSET KEY FIELD updated_value
  6. EXEC

以上,就把监听整个hash key的需求转变成了监听由hash的key-field组成的唯一字符串了,这样,修改hash value的操作和修改string的操作绑定在了一起。

这样就不会key下任意field改动都会导致事务失败了。

参考 redis-py watch hash key

小结

涉及到并发的情况,在应用程序开发中都要特别注意。

一旦出现数据一致性问题,会比较难调试。

在分布式系统中,操作数据库时的数据一致性,也是需要特别关注的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值