在前端开发中,我们经常会遇到并发同步的问题。比如在一个高并发的场景下,多个用户同时对同一资源进行访问,会导致数据的不一致性和安全性问题。为了解决这个问题,我们可以使用 Redis。
Redis 是一个高性能的键值对存储系统,支持多种数据结构,如字符串、哈希表、列表、集合等。它的特点是快速、稳定、可靠,可以用来做缓存、消息队列、计数器、分布式锁等。
在本文中,我们将介绍 Redis 如何解决并发同步问题,包括分布式锁、乐观锁和悲观锁三种方法。
分布式锁
分布式锁是一种常见的并发控制方法,它可以保证在分布式环境下的并发安全。Redis 可以通过 SETNX 命令实现分布式锁。
SETNX 命令可以将一个键值对设置为一个锁,并且只有在该键值对不存在时才能成功设置。当多个客户端同时尝试设置同一个键值对时,只有一个客户端能够成功设置,其他客户端会失败。
当客户端需要释放锁时,可以使用 DEL 命令来删除该键值对。为了避免死锁,可以给锁设置一个过期时间,如果超过该时间仍未释放锁,则自动释放。
下面是一个使用分布式锁的示例代码:
// www.javascriptcn.com code example
async function doTask(taskId) {
const lockKey = `task:${taskId}:lock`;
const lockExpire = 60; // 锁的过期时间为 60 秒
const lockValue = uuidv4(); // 生成一个随机值作为锁的值
const acquiredLock = await redis.setnx(lockKey, lockValue);
if (!acquiredLock) {
// 获取锁失败,表示有其他客户端正在处理该任务
return;
}
await redis.expire(lockKey, lockExpire); // 给锁设置过期时间
try {
// 执行任务
await processTask(taskId);
} finally {
// 释放锁
const currentLockValue = await redis.get(lockKey);
if (currentLockValue === lockValue) {
// 只有锁的值与当前客户端设置的值相同才能释放锁
await redis.del(lockKey);
}
}
}乐观锁
乐观锁是一种基于版本号的并发控制方法,它不会阻塞其他客户端的操作。Redis 可以通过 WATCH 命令和 MULTI/EXEC 命令实现乐观锁。
WATCH 命令可以监听一个或多个键,当这些键的值发生变化时,会取消客户端在事务中的操作。MULTI/EXEC 命令可以将多个操作组合成一个事务,只有当所有操作都成功执行时才会提交事务。
在使用乐观锁时,我们可以将数据的版本号保存在键值对中,每次操作时先检查版本号是否与当前版本一致,如果一致则执行操作并将版本号加一,否则放弃操作。
下面是一个使用乐观锁的示例代码:
// www.javascriptcn.com code example
async function transfer(from, to, amount) {
const fromKey = `account:${from}`;
const toKey = `account:${to}`;
while (true) {
await redis.watch(fromKey, toKey);
const fromBalance = await redis.get(fromKey);
const toBalance = await redis.get(toKey);
if (fromBalance < amount) {
// 余额不足,放弃转账
await redis.unwatch();
return false;
}
const multi = redis.multi();
multi.decrby(fromKey, amount);
multi.incrby(toKey, amount);
const execResult = await multi.exec();
if (execResult) {
// 转账成功
return true;
}
// 事务执行失败,说明数据已被其他客户端修改,重新尝试
}
}悲观锁
悲观锁是一种基于锁的并发控制方法,它会阻塞其他客户端的操作。Redis 可以通过 SETEX 命令和 GETSET 命令实现悲观锁。
SETEX 命令可以将一个键值对设置为一个锁,并且在一定时间后自动释放。GETSET 命令可以获取一个键的值并设置一个新值,如果该键不存在则返回 null。
在使用悲观锁时,我们可以将数据的锁保存在键值对中,每次操作时先获取锁,执行操作后再释放锁。如果获取锁失败,则等待一段时间后重新尝试。
下面是一个使用悲观锁的示例代码:
// www.javascriptcn.com code example
async function reserveSeat(showId, seatId) {
const lockKey = `show:${showId}:seat:${seatId}:lock`;
const lockExpire = 60; // 锁的过期时间为 60 秒
while (true) {
const acquiredLock = await redis.setex(lockKey, lockExpire, 'locked');
if (!acquiredLock) {
// 获取锁失败,等待 100 毫秒后重新尝试
await new Promise(resolve => setTimeout(resolve, 100));
continue;
}
const seatKey = `show:${showId}:seat:${seatId}`;
const seatStatus = await redis.get(seatKey);
if (seatStatus !== 'available') {
// 座位已被预订或锁已过期,放弃预订
await redis.del(lockKey);
return false;
}
await redis.set(seatKey, 'reserved');
await redis.del(lockKey);
// 预订成功
return true;
}
}通过以上三种方法,我们可以有效地解决并发同步问题,提高应用程序的并发能力和性能。但是需要注意的是,不同的场景需要选择不同的并发控制方法,否则可能会出现死锁、性能下降等问题。在实际应用中,需要根据具体情况选择合适的方法,并进行充分测试和优化。
Source: FunTeaLearn,Please indicate the source for reprints https://funteas.com/post/67d0e192a941bf71341d924d