Redis 单线程 为何却需要事务处理并发问题

本文涉及的产品
云原生内存数据库 Tair,内存型 2GB
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Redis 版,经济版 1GB 1个月
简介: Redis 单线程 为何却需要事务处理并发问题

Redis 是单线程处理,也就是命令会顺序执行。那么为什么会存在并发问题呢?

个人理解是,虽然 redis 是单线程,但是可以同时有多个客户端访问,每个客户端会有
一个线程。客户端访问之间存在竞争

简单的总结下,其实 redis 本事是不会存在并发问题的,因为他是单进程的,再多的 command 都是 one by one 执行的。我们使用的时候,可能会出现并发问题,比如 getset 这一对。

因为存在多客户端并发,所以必须保证操作的原子性。比如银行卡扣款问题,获取余额,判断,扣款,写回就必须构成事务,否则就可能出错。

redis 中的并发问题

使用 redis 作为缓存已经很久了,redis 是以单进程的形式运行的,命令是一个接着一个执行的,一直以为不会存在并发的问题,直到今天看到相关的资料,才恍然大悟~~

具体问题实例

有个键,假设名称为 myNum,里面保存的是阿拉伯数字,假设现在值为 1,存在多个连接对 myNum 进行操作的情况,这个时候就会有并发的问题。假设有两个连接 linkAlinkB,这两个连接都执行下面的操作,取出 myNum 的值,+1,然后再存回去,看看下面的交互:

linkA get myNum => 1
linkB get myNum => 1
linkA set muNum => 2
linkB set myNum => 2

执行完操作之后,结果可能是 2,这和我们预期的 3 不一致。

再看一个具体的例子:

<?php
require "vendor/autoload.php";
$client = new Predis\Client([
    'scheme' => 'tcp',
    'host' => '127.0.0.1',
    'port' => 6379,
]);
for ($i = 0; $i < 1000; $i++) {
    $num = intval($client->get("name"));
    $num = $num + 1;
    $client->setex("name", $num, 10080);
    usleep(10000);
}

设置 name 初始值为 0,然后同时用两个终端执行上面的程序,最后 name 的值可能不是 2000,而是一个 < 2000 的值,这也就证明了我们上面的并发问题的存在,这个该怎么解决呢?

redis 中的事务

redis 中也是有事务的,不过这个事务没有 mysql 中的完善,只保证了一致性和隔离性,不满足原子性和持久性。

redis 事务使用 multi、exec 命令

原子性,redis 会将事务中的所有命令执行一遍,哪怕是中间有执行失败也不会回滚。kill 信号、宿主机宕机等导致事务执行失败,redis 也不会进行重试或者回滚。

持久性,redis 事务的持久性依赖于 redis 所使用的持久化模式,遗憾的是各种持久化模式也都不是持久化的。

隔离性,redis 是单进程,开启事务之后,会执行完当前连接的所有命令直到遇到 exec 命令,才处理其他连接的命令。

一致性,看了文档,觉得挺扯的,但是貌似说的没有问题。

redis 中的事务不支持原子性,所以解决不了上面的问题。

当然了 redis 还有一个 watch 命令,这个命令可以解决这个问题,看下面的例子,对一个键执行 watch,然后执行事务,由于 watch 的存在,他会监测键 a,当 a 被修该之后,后面的事务就会执行失败,这就确保了多个连接同时来了,都监测着 a,只有一个能执行成功,其他都返回失败。

127.0.0.1:6379> set a 1
OK
127.0.0.1:6379> watch a
OK
127.0.0.1:6379> multi 
OK
127.0.0.1:6379> incr a
QUEUED
127.0.0.1:6379> exec
1) (integer) 2
127.0.0.1:6379> get a
"2"

失败时候的例子,从最后可以看出,test 的值被其他连接修改了:

127.0.0.1:6379> set test 1
OK
127.0.0.1:6379> watch test
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incrby test 11
QUEUED
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get test
"100"

我的问题如何解决

redis 中命令是满足原子性的,因此在值为阿拉伯数字的时候,我可以将 getset 命令修改为 incr 或者 incrby 来解决这个问题,下面的代码开启两个终端同时执行,得到的结果是满足我们预期的 2000

<?php
require "vendor/autoload.php";
$client = new Predis\Client([
    'scheme' => 'tcp',
    'host'   => '127.0.0.1',
    'port'   => 6379,
]);
for ($i = 0; $i < 1000; $i++) {
    $client->incr("name");
    $client->expire("name", 10800);
    usleep(10000);
}

@manzilu 提到的方法

评论中 manzilu 提到的方法查了下资料,确实可行,效果还不错,这里写了个例子

<?php
require "vendor/autoload.php";
$client = new Predis\Client([
    'scheme' => 'tcp',
    'host'   => '127.0.0.1',
    'port'   => 6379,
]);
class RedisLock
{
    public $objRedis = null;
    public $timeout = 3;
    /**
     * @desc 设置redis实例
     *
     * @param obj object | redis实例
     */
    public function __construct($obj)
    {
        $this->objRedis = $obj;
    }
    /**
     * @desc 获取锁键名
     */
    public function getLockCacheKey($key)
    {
        return "lock_{$key}";
    }
    /**
     * @desc 获取锁
     *
     * @param key string | 要上锁的键名
     * @param timeout int | 上锁时间
     */
    public function getLock($key, $timeout = NULL)
    {
        $timeout = $timeout ? $timeout : $this->timeout;
        $lockCacheKey = $this->getLockCacheKey($key);
        $expireAt = time() + $timeout;
        $isGet = (bool)$this->objRedis->setnx($lockCacheKey, $expireAt);
        if ($isGet) {
            return $expireAt;
        }
        while (1) {
            usleep(10);
            $time = time();
            $oldExpire = $this->objRedis->get($lockCacheKey);
            if ($oldExpire >= $time) {
                continue;
            }
            $newExpire = $time + $timeout;
            $expireAt = $this->objRedis->getset($lockCacheKey, $newExpire);
            if ($oldExpire != $expireAt) {
                continue;
            }
            $isGet = $newExpire;
            break;
        }
        return $isGet;
    }
    /**
     * @desc 释放锁
     *
     * @param key string | 加锁的字段
     * @param newExpire int | 加锁的截止时间
     *
     * @return bool | 是否释放成功
     */
    public function releaseLock($key, $newExpire)
    {
        $lockCacheKey = $this->getLockCacheKey($key);
        if ($newExpire >= time()) {
            return $this->objRedis->del($lockCacheKey);
        }
        return true;
    }
}
$start_time = microtime(true);
$lock = new RedisLock($client);
$key = "name";
for ($i = 0; $i < 10000; $i++) {
    $newExpire = $lock->getLock($key);
    $num = $client->get($key);
    $num++;
    $client->set($key, $num);
    $lock->releaseLock($key, $newExpire);
}
$end_time = microtime(true);
echo "花费时间 : ". ($end_time - $start_time) . "\n";

执行 shellphp setnx.php & php setnx.php&,最后会得到结果:

$ 花费时间 : 4.3004920482635
[2]  + 72356 done       php setnx.php
# root @ ritoyan-virtual-pc in ~/PHP/redis-high-concurrency [20:23:41] 
$ 花费时间 : 4.4319710731506
[1]  + 72355 done       php setnx.php

同样循环 1w 次,去掉 usleep,使用 incr 直接进行增加,耗时在 2s 左右。

而获取所得时候取消 usleep,时间不但没减少,反而增加了,这个 usleep 的设置要合理,免得进程做无用的循环

总结

看了这么多,简单的总结下,其实 redis 本事是不会存在并发问题的,因为他是单进程的,再多的 command 都是 one by one 执行的。我们使用的时候,可能会出现并发问题,比如 getset 这一对。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
3天前
|
Java 测试技术 容器
多线程编程基础与并发问题解决方案
多线程编程基础与并发问题解决方案
|
2天前
|
调度
【浅入浅出】Qt多线程机制解析:提升程序响应性与并发处理能力
在学习QT线程的时候我们首先要知道的是QT的主线程,也叫GUI线程,意如其名,也就是我们程序的最主要的一个线程,主要负责初始化界面并监听事件循环,并根据事件处理做出界面上的反馈。但是当我们只限于在一个主线程上书写逻辑时碰到了需要一直等待的事件该怎么办?它的加载必定会带着主界面的卡顿,这时候我们就要去使用多线程。
|
4天前
|
存储 安全 Java
使用Java实现高效的多线程并发控制策略
使用Java实现高效的多线程并发控制策略
|
9天前
|
Java 调度
Java多线程编程与并发控制策略
Java多线程编程与并发控制策略
|
9天前
|
安全 Java 开发者
Java并发编程:理解并发与多线程
在当今软件开发领域,Java作为一种广泛应用的编程语言,其并发编程能力显得尤为重要。本文将深入探讨Java中的并发编程概念,包括多线程基础、线程安全、并发工具类等内容,帮助开发者更好地理解和应用Java中的并发特性。
7 1
|
2天前
|
Java 调度 Windows
Java面试之程序、进程、线程、管程和并发、并行的概念
Java面试之程序、进程、线程、管程和并发、并行的概念
8 0
|
2天前
|
Java Redis 数据安全/隐私保护
Redis14----Redis的java客户端-jedis的连接池,jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,最好用jedis连接池代替jedis,配置端口,密码
Redis14----Redis的java客户端-jedis的连接池,jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,最好用jedis连接池代替jedis,配置端口,密码
|
2天前
|
安全 NoSQL Java
网络安全-----Redis12的Java客户端----客户端对比12,Jedis介绍,使用简单安全性不足,lettuce(官方默认)是基于Netty,支持同步,异步和响应式,并且线程是安全的,支持R
网络安全-----Redis12的Java客户端----客户端对比12,Jedis介绍,使用简单安全性不足,lettuce(官方默认)是基于Netty,支持同步,异步和响应式,并且线程是安全的,支持R
|
5天前
|
Java 测试技术 容器
多线程编程基础与并发问题解决方案
多线程编程基础与并发问题解决方案
|
9天前
|
NoSQL Redis
Redis系列学习文章分享---第五篇(Redis实战篇--优惠券秒杀,全局唯一id 添加优惠券 实现秒杀下单 库存超卖问题分析 乐观锁解决超卖 实现一人一单功能 集群下的线程并发安全问题)
Redis系列学习文章分享---第五篇(Redis实战篇--优惠券秒杀,全局唯一id 添加优惠券 实现秒杀下单 库存超卖问题分析 乐观锁解决超卖 实现一人一单功能 集群下的线程并发安全问题)
11 0