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

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 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
高德地图爬虫实践:Java多线程并发处理策略
高德地图爬虫实践:Java多线程并发处理策略
|
2天前
|
Java 测试技术 Python
Python的多线程允许在同一进程中并发执行任务
【5月更文挑战第17天】Python的多线程允许在同一进程中并发执行任务。示例1展示了创建5个线程打印&quot;Hello World&quot;,每个线程调用同一函数并使用`join()`等待所有线程完成。示例2使用`ThreadPoolExecutor`下载网页,创建线程池处理多个URL,打印出每个网页的大小。Python多线程还可用于线程间通信和同步,如使用Queue和Lock。
14 1
|
3天前
|
缓存 NoSQL Redis
Redis经典问题:数据并发竞争
在大流量系统中,数据并发竞争可能导致系统性能下降和崩溃。为解决此问题,可以采取加写回操作和互斥锁,确保数据一致性并减少写操作对缓存的影响。另外,保持缓存数据多个备份能降低并发竞争概率。通过实例展示了如何在电商网站中应用这些策略,从而提高系统稳定性和性能。关注微信公众号“软件求生”获取更多技术分享。
233 1
|
3天前
|
存储 NoSQL Redis
深入浅出Redis(二):Redis单线程模型与通信流程
深入浅出Redis(二):Redis单线程模型与通信流程
|
3天前
|
安全 C++
C++多线程编程:并发与同步
C++多线程编程:并发与同步
10 0
|
3天前
|
安全 Java
Java中的并发编程:理解并发性与线程安全
Java作为一种广泛应用的编程语言,在并发编程方面具有显著的优势和特点。本文将探讨Java中的并发编程概念,重点关注并发性与线程安全,并提供一些实用的技巧和建议,帮助开发人员更好地理解和应用Java中的并发机制。
|
3天前
|
NoSQL Redis
Redis 线程模型
Redis 线程模型
|
3天前
|
算法 安全
AtomicInteger使用非阻塞算法,实现并发控制多线程实现售票
AtomicInteger使用非阻塞算法,实现并发控制多线程实现售票
|
3天前
|
存储 安全 Java
【亮剑】`ConcurrentHashMap`是Java中线程安全的哈希表,采用锁定分离技术提高并发性能
【4月更文挑战第30天】`ConcurrentHashMap`是Java中线程安全的哈希表,采用锁定分离技术提高并发性能。数据被分割成多个Segment,每个拥有独立锁,允许多线程并发访问不同Segment。当写操作发生时,计算键的哈希值定位Segment并获取其锁;读操作通常无需锁定。内部会根据负载动态调整Segment,减少锁竞争。虽然使用不公平锁,但Java 8及以上版本提供了公平锁选项。理解其工作原理对开发高性能并发应用至关重要。
|
3天前
|
存储 缓存 NoSQL
为什么Redis使用单线程 性能会优于多线程?
在计算机领域,性能一直都是一个关键的话题。无论是应用开发还是系统优化,我们都需要关注如何在有限的资源下,实现最大程度的性能提升。Redis,作为一款高性能的开源内存数据库,因其出色的单线程性能而备受瞩目。那么,为什么Redis使用单线程性能会优于多线程呢?
25 1