关于redis:使用Redis是并发安全的吗

38次阅读

共计 5354 个字符,预计需要花费 14 分钟才能阅读完成。

大家都分明,Redis 是一个开源的高性能键值对存储系统,被开发者广泛应用于缓存、音讯队列、排行榜、计数器等场景。因为其高效的读写性能和丰盛的数据类型,Redis 受到了越来越多开发者的青眼。然而,在并发操作下,Redis 是否可能保证数据的一致性和安全性呢?接下来小岳将跟大家一起来探讨 Redis 并发安全性的问题。

一. Redis 的并发安全性

在 Redis 中,每个客户端都会通过一个独立的连贯与 Redis 服务器进行通信,每个命令的执行都是原子性的。在单线程的 Redis 服务器中,一个客户端的申请会顺次被执行,不会被其余客户端的申请打断,因而不须要思考并发安全性的问题。然而,在多线程或多过程环境中,多个客户端的申请会同时达到 Redis 服务器,这时就须要思考并发安全性的问题了。
Redis 提供了一些并发管制的机制,能够保障并发操作的安全性。其中最罕用的机制是事务和乐观锁,接下来就让咱们一起来看看吧!

1. 事务

Redis 的事务是一组命令的汇合,这些命令会被打包成一个事务块(transaction block),而后一次性执行。在执行事务期间,Redis 不会中断执行事务的客户端,也不会执行其余客户端的命令,这保障了事务的原子性。如果在执行事务的过程中呈现谬误,Redis 会回滚整个事务,保证数据的一致性。

事务的应用形式很简略,只须要应用 MULTI 命令开启事务,而后将须要执行的命令增加到事务块中,最初应用 EXEC 命令提交事务即可。上面是一个简略的事务示例:

Jedis jedis = new Jedis("localhost", 6379);
Transaction tx = jedis.multi();
tx.set("key1", "value1");
tx.set("key2", "value2");
tx.exec();

在下面的示例中,咱们应用 Jedis 客户端开启了一个事务,将两个 SET 命令增加到事务块中,而后应用 EXEC 命令提交事务。如果在执行事务的过程中呈现谬误,能够通过调用 tx.discard() 办法回滚事务。

事务尽管能够保障并发操作的安全性,然而也存在一些限度。首先,事务只能保障事务块内的命令是原子性的,事务块之外的命令不受事务的影响。其次,Redis 的事务是乐观锁机制,即在提交事务时才会查看事务块内的命令是否抵触,因而如果在提交事务前有其余客户端批改了事务块中的数据,就会导致事务提交失败。

2. 乐观锁

在多线程并发操作中,为了保证数据的一致性和可靠性,咱们须要应用锁机制来协调线程之间的拜访。传统的加锁机制是乐观锁,它会在每次拜访数据时都加锁,导致线程之间的竞争和期待。乐观锁则是一种更为轻量级的锁机制,它假设在并发操作中,数据的抵触很少产生,因而不须要每次都加锁,而是在更新数据时检查数据版本号或者工夫戳,如果版本号或工夫戳不统一,则阐明其余线程曾经更新了数据,此时须要回滚操作。

在 Java 中,乐观锁的实现形式有两种:版本号机制和工夫戳机制。上面别离介绍这两种机制的实现形式和代码案例。

2.1 版本号机制的实现形式

版本号机制是指在数据表中新增一个版本号字段,每次更新数据时,将版本号加 1,并且在更新数据时判断版本号是否统一。如果版本号不统一,则阐明其余线程曾经更新了数据,此时须要回滚操作。上面是版本号机制的代码实现:

public void updateWithVersion(int id, String newName, long oldVersion) {
    String sql = "update user set name = ?, version = ? where id = ? and version = ?";
    try {Connection conn = getConnection(); // 获取数据库连贯
        PreparedStatement ps = conn.prepareStatement(sql);
        ps.setString(1, newName);
        ps.setLong(2, oldVersion + 1); // 版本号加 1
        ps.setInt(3, id);
        ps.setLong(4, oldVersion);
        int i = ps.executeUpdate(); // 执行更新操作
        if (i == 0) {System.out.println("更新失败");
        } else {System.out.println("更新胜利");
        }
    } catch (SQLException e) {e.printStackTrace();
    }
}

2.2 工夫戳机制的实现形式

工夫戳机制是指在数据表中新增一个工夫戳字段,每次更新数据时,将工夫戳更新为以后工夫,并且在更新数据时判断工夫戳是否统一。如果工夫戳不统一,则阐明其余线程曾经更新了数据,此时须要回滚操作。上面是工夫戳机制的代码实现:

public void updateWithTimestamp(int id, String newName, Timestamp oldTimestamp) {
    String sql = "update user set name = ?, update_time = ? where id = ? and update_time = ?";
    try {Connection conn = getConnection(); // 获取数据库连贯
        PreparedStatement ps = conn.prepareStatement(sql);
        ps.setString(1, newName);
        ps.setTimestamp(2, new Timestamp(System.currentTimeMillis())); // 更新工夫戳为以后工夫
        ps.setInt(3, id);
        ps.setTimestamp(4, oldTimestamp);
        int i = ps.executeUpdate(); // 执行更新操作
        if (i == 0) {System.out.println("更新失败");
        } else {System.out.println("更新胜利");
        }
    } catch (SQLException e) {e.printStackTrace();
    }
}

通过以上两种形式的实现,咱们就能够实现 Java 乐观锁的机制,并且在多线程并发操作中保证数据的一致性和可靠性。

3.WATCH 命令

WATCH 命令能够监督一个或多个键,如果这些键在事务执行期间被批改,事务就会被回滚。WATCH 命令的应用形式如下:

Jedis jedis = new Jedis("localhost", 6379);
jedis.watch("key1", "key2");
Transaction tx = jedis.multi();
tx.set("key1", "value1");
tx.set("key2", "value2");
tx.exec();

在下面的示例中,咱们应用 WATCH 命令监督了 key1 和 key2 两个键,如果这两个键在事务执行期间被批改,事务就会被回滚。在执行事务之前,咱们须要应用 jedis.watch() 办法监督须要监督的键,而后应用 jedis.multi() 办法开启事务,将须要执行的命令增加到事务块中,最初应用 tx.exec() 办法提交事务。

4.CAS 命令

CAS 命令是 Redis 4.0 中新增的命令,它能够将一个键的值与指定的旧值进行比拟,如果相等,则将键的值设置为新值。CAS 命令的应用形式如下:

Jedis jedis = new Jedis("localhost", 6379);
jedis.set("key1", "old value");
String oldValue = jedis.get("key1");
if(oldValue.equals("old value")){jedis.set("key1", "new value");
}

在下面的示例中,咱们首先将 key1 的值设置为 old value,而后通过 jedis.get() 办法获取 key1 的值,并将其赋值给 oldValue 变量。如果 oldValue 等于 old value,则将 key1 的值设置为 new value。因为 CAS 命令是原子性的,因而能够保障并发操作的安全性。

二. 案例剖析

为了更好地阐明 Redis 的并发安全性,咱们接下来将联合公司实在我的项目案例进行剖析。
咱们公司有一个在线游戏我的项目,其中蕴含排行榜和计数器等性能,须要应用 Redis 进行数据存储和解决。在并发拜访排行榜和计数器时,如果没有并发管制机制,就会导致数据不统一的问题。

为了解决这个问题,咱们应用了 Redis 的事务和乐观锁机制。首先,咱们应用 Redis 的事务机制将须要执行的命令打包成一个事务块,而后应用 WATCH 命令监督须要监督的键。如果在执行事务期间有其余客户端批改了监督的键,事务就会被回滚。如果事务执行胜利,Redis 就会主动开释监督的键。

上面是一个示例代码:

public void updateRank(String userId, long score){
    Jedis jedis = null;
    try {jedis = jedisPool.getResource();
        while (true){jedis.watch("rank");
            Transaction tx = jedis.multi();
            tx.zadd("rank", score, userId);
            tx.exec();
            if(tx.exec()!=null){break;}
        }
    }finally {if(jedis!=null){jedis.close();
        }
    }
}

在下面的示例中,咱们定义了一个 updateRank() 办法,用于更新排行榜。在办法中,咱们应用 jedis.watch() 办法监督 rank 键,而后应用 jedis.multi() 办法开启事务,将须要执行的命令增加到事务块中,最初应用 tx.exec() 办法提交事务。在提交事务之前,咱们应用 while 循环不断尝试执行事务,如果事务执行胜利,就退出循环。通过这种形式,咱们能够保障排行榜的数据是统一的。

相似地,咱们还能够应用乐观锁机制保障计数器的并发安全性。上面是一个示例代码:

public long getCount(String key){
    Jedis jedis = null;
    long count = -1;
    try {jedis = jedisPool.getResource();
        jedis.watch(key);
        String value = jedis.get(key);
        count = Long.parseLong(value);
        count++;
        Transaction tx = jedis.multi();
        tx.set(key, Long.toString(count));
        if(tx.exec()!=null){jedis.unwatch();
        }
    }finally {if(jedis!=null){jedis.close();
        }
    }
    return count;
}

在下面的示例中,咱们定义了一个 getCount() 办法,用于获取计数器的值。在办法中,咱们应用 jedis.watch() 办法监督计数器的键,而后通过 jedis.get() 办法获取计数器的值,并将其赋值给 count 变量。接着,咱们将 count 变量加 1,并应用 jedis.multi() 办法开启事务,将 SET 命令增加到事务块中。如果事务执行胜利,就应用 jedis.unwatch() 办法解除监督。

三. 总结

本文次要介绍了 Redis 的并发安全性问题,并联合公司实在我的项目案例进行了详细分析阐明。咱们能够应用 Redis 的事务和乐观锁机制保障并发操作的安全性,从而防止数据的不一致性和安全性问题。在理论开发中,咱们应该依据具体的利用场景抉择适宜的并发管制机制,确保数据的一致性和安全性。

Redis 全套教程学习分享!

https://www.bilibili.com/video/BV1CL411778r/?aid=464667873&ci…

正文完
 0