难度:⭐⭐⭐⭐ | 适合人群:想掌握Redis高级特性的开发者
💥 开场:一次"恐怖"的库存为负数
时间: 周三下午
地点: 办公室
事件: 运营反馈异常
运营妹子: "你们的库存有问题!后台显示库存是-50!"
我: "什么?库存怎么会是负数???" 😱
立即查看Redis:
redis> GET stock:product:123
"-50" # 卧槽,真的是负数
我: "不可能啊,我明明做了判断..." 😰
查看代码:
@Service
public class StockService {
public boolean deductStock(Long productId, int quantity) {
String key = "stock:product:" + productId;
// 1. 获取库存
String stockStr = redisTemplate.opsForValue().get(key);
int stock = Integer.parseInt(stockStr);
// 2. 判断库存
if (stock < quantity) {
return false; // 库存不足
}
// 3. 扣减库存
stock -= quantity;
redisTemplate.opsForValue().set(key, String.valueOf(stock));
return true;
}
}
哈吉米走过来: "这代码有并发问题,不是原子操作!"
我: "哪里不是原子?" 🤔
南北绿豆画了个图:
sequenceDiagram
participant 线程1
participant 线程2
participant Redis
Note over Redis: 当前库存:1
线程1->>Redis: GET stock(返回1)
线程2->>Redis: GET stock(返回1)
线程1->>线程1: 判断:1 >= 1,通过 ✅
线程2->>线程2: 判断:1 >= 1,通过 ✅
线程1->>线程1: 计算:1 - 1 = 0
线程2->>线程2: 计算:1 - 1 = 0
线程1->>Redis: SET stock 0
线程2->>Redis: SET stock 0
Note over Redis: 卖出2件,但库存从1变成0<br/>库存不准确!
阿西噶阿西: "更糟的情况:库存变成负数!"
sequenceDiagram
participant 线程1
participant 线程2
participant 线程3
participant Redis
Note over Redis: 当前库存:1
线程1->>Redis: GET stock(1)
线程2->>Redis: GET stock(1)
线程3->>Redis: GET stock(1)
Note over 线程1,线程3: 三个线程都判断通过
线程1->>Redis: SET stock 0
线程2->>Redis: SET stock 0
线程3->>Redis: SET stock 0
Note over 线程1,线程3: 又来3个请求...
线程1->>Redis: GET stock(0)
Note over 线程1: 判断:0 >= 1,失败
线程2->>Redis: GET stock(0)
Note over 线程2: 判断通过(读到旧值)
线程2->>Redis: SET stock -1
Note over Redis: 库存变成-1!💥
我: "原来是这样!那怎么解决?" 😓
哈吉米: "必须保证操作的原子性!Redis有几种方式..."
🎯 第一问:什么是原子性?
原子性定义
原子性(Atomicity): 一个操作不可分割,要么全部执行,要么全部不执行。
生活中的例子:
转账操作:
1. 从A账户扣款100元
2. 给B账户加款100元
必须原子执行:
- 成功:两步都完成
- 失败:两步都不执行
不能出现:
- A扣了钱,B没收到 ❌
- A没扣钱,B收到了 ❌
Redis中的原子性
南北绿豆: "Redis中的原子性分两个层面。"
1. 单个命令的原子性
Redis是单线程的
↓
单个命令执行时不会被打断
↓
天然具有原子性
例如:
INCR、DECR、LPUSH、SADD等
都是原子操作
2. 多个命令的原子性
多个命令组合:
1. GET stock
2. 判断
3. SET stock
三个步骤之间会被其他命令打断
↓
不是原子操作
↓
需要特殊机制保证原子性
🔧 第二问:实现原子性的4种方式
方式1:使用原子命令(推荐)
哈吉米: "Redis提供了很多原子命令,直接用就行!"
INCR/DECR(自增/自减):
// ❌ 错误:不是原子操作
public void deductStock(String key) {
String stock = redisTemplate.opsForValue().get(key);
int value = Integer.parseInt(stock) - 1;
redisTemplate.opsForValue().set(key, String.valueOf(value));
}
// ✅ 正确:原子操作
public void deductStock(String key) {
redisTemplate.opsForValue().decrement(key); // DECR命令,原子的
}
INCRBY/DECRBY(指定增减量):
// 扣减指定数量
redisTemplate.opsForValue().decrement("stock:product:123", 5); // 减5
// 增加指定数量
redisTemplate.opsForValue().increment("stock:product:123", 10); // 加10
GETSET(获取旧值并设置新值):
// 原子操作:获取旧值 + 设置新值
String oldValue = redisTemplate.opsForValue().getAndSet("counter", "0");
System.out.println("旧值:" + oldValue);
SETNX(不存在才设置):
// 原子操作:检查 + 设置
Boolean success = redisTemplate.opsForValue().setIfAbsent("lock:key", "value");
// 可以加过期时间(Redis 2.6.12+)
Boolean success = redisTemplate.opsForValue().setIfAbsent(
"lock:key", "value", 10, TimeUnit.SECONDS);
方式2:Lua脚本(最强大,推荐)
阿西噶阿西: "Lua脚本是保证原子性的终极方案!"
为什么Lua脚本是原子的?
Redis执行Lua脚本时:
1. 脚本作为一个整体执行
2. 执行期间不会执行其他命令
3. 不会被打断
↓
天然保证原子性
案例1:库存扣减(带判断)
@Service
public class StockService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* Lua脚本:原子扣减库存
*/
private static final String DEDUCT_STOCK_SCRIPT =
"local stock = redis.call('get', KEYS[1]) " +
"if not stock then " +
" return -1 " + // 库存key不存在
"end " +
"if tonumber(stock) < tonumber(ARGV[1]) then " +
" return 0 " + // 库存不足
"end " +
"redis.call('decrby', KEYS[1], ARGV[1]) " +
"return 1"; // 扣减成功
public boolean deductStock(Long productId, int quantity) {
String key = "stock:product:" + productId;
// 执行Lua脚本
Long result = redisTemplate.execute(
new DefaultRedisScript<>(DEDUCT_STOCK_SCRIPT, Long.class),
Collections.singletonList(key),
String.valueOf(quantity)
);
if (result == null) {
return false;
}
switch (result.intValue()) {
case -1:
System.out.println("库存key不存在");
return false;
case 0:
System.out.println("库存不足");
return false;
case 1:
System.out.println("扣减成功");
return true;
default:
return false;
}
}
}
并发测试:
@Test
public void testConcurrentDeduct() throws InterruptedException {
// 初始化库存
redisTemplate.opsForValue().set("stock:product:123", "100");
CountDownLatch latch = new CountDownLatch(1);
AtomicInteger successCount = new AtomicInteger(0);
// 1000个线程并发扣减
ExecutorService executor = Executors.newFixedThreadPool(100);
for (int i = 0; i < 1000; i++) {
executor.submit(() -> {
try {
latch.await();
if (stockService.deductStock(123L, 1)) {
successCount.incrementAndGet();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
latch.countDown(); // 开始
Thread.sleep(5000);
System.out.println("成功扣减:" + successCount.get());
System.out.println("剩余库存:" + redisTemplate.opsForValue().get("stock:product:123"));
executor.shutdown();
}
输出:
成功扣减:100
剩余库存:0
完全准确!没有超卖! ✅
案例2:限流(滑动窗口)
/**
* Lua脚本:滑动窗口限流
*/
private static final String RATE_LIMIT_SCRIPT =
"local key = KEYS[1] " +
"local limit = tonumber(ARGV[1]) " +
"local window = tonumber(ARGV[2]) " +
"local current = tonumber(ARGV[3]) " +
// 删除窗口外的数据
"redis.call('zremrangebyscore', key, 0, current - window) " +
// 统计窗口内的请求数
"local count = redis.call('zcard', key) " +
// 判断是否超过限制
"if count < limit then " +
" redis.call('zadd', key, current, current) " +
" redis.call('expire', key, window) " +
" return 1 " + // 允许请求
"else " +
" return 0 " + // 拒绝请求
"end";
public boolean isAllowed(String userId, int limit, int windowSeconds) {
String key = "rate:limit:" + userId;
long current = System.currentTimeMillis();
Long result = redisTemplate.execute(
new DefaultRedisScript<>(RATE_LIMIT_SCRIPT, Long.class),
Collections.singletonList(key),
String.valueOf(limit),
String.valueOf(windowSeconds * 1000),
String.valueOf(current)
);
return result != null && result == 1;
}
使用:
// 限流:每秒最多10个请求
if (isAllowed("user:123", 10, 1)) {
// 处理请求
} else {
// 拒绝请求:限流
}
方式3:Redis事务(有限制)
南北绿豆: "Redis也有事务,但和MySQL的事务不一样!"
基本使用:
public void transfer() {
// 开启事务
redisTemplate.execute(new SessionCallback<Object>() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
operations.multi(); // MULTI:开启事务
try {
// 一系列命令
operations.opsForValue().decrement("account:A", 100); // A扣100
operations.opsForValue().increment("account:B", 100); // B加100
// 提交事务
return operations.exec(); // EXEC:执行事务
} catch (Exception e) {
operations.discard(); // DISCARD:取消事务
return null;
}
}
});
}
Redis事务的特点:
1. 不支持回滚
- 命令错误会继续执行其他命令
- 不像MySQL那样全部回滚
2. 不保证原子性(有争议)
- MULTI到EXEC之间的命令会被打断吗?不会
- 但部分命令失败不会回滚
3. 使用场景有限
示例:错误不会回滚
redis> MULTI
OK
redis> SET key1 "value1"
QUEUED
redis> INCR key1 # 错误:key1不是数字
QUEUED
redis> SET key2 "value2"
QUEUED
redis> EXEC
1) OK
2) (error) ERR value is not an integer
3) OK
# 结果:
# key1 = "value1" ✅ 执行了
# INCR失败 ❌ 失败了
# key2 = "value2" ✅ 还是执行了
# MySQL的事务:全部回滚
# Redis的事务:部分失败,其他照常执行
阿西噶阿西: "所以Redis事务不适合复杂业务,Lua脚本才是王道!"
方式4:WATCH乐观锁
南北绿豆: "WATCH可以实现乐观锁。"
public boolean deductStockWithWatch(String key, int quantity) {
while (true) {
// 1. WATCH key
redisTemplate.watch(key);
// 2. 获取当前值
String stockStr = redisTemplate.opsForValue().get(key);
int stock = Integer.parseInt(stockStr);
// 3. 判断库存
if (stock < quantity) {
redisTemplate.unwatch();
return false;
}
// 4. 开启事务
redisTemplate.multi();
// 5. 扣减库存
redisTemplate.opsForValue().set(key, String.valueOf(stock - quantity));
// 6. 执行事务
List<Object> results = redisTemplate.exec();
// 7. 检查结果
if (results != null && !results.isEmpty()) {
// 执行成功
return true;
}
// 执行失败(key被修改了),重试
System.out.println("检测到并发修改,重试...");
}
}
WATCH原理:
sequenceDiagram
participant 线程1
participant 线程2
participant Redis
线程1->>Redis: WATCH stock
Note over 线程1: 开始监视stock
线程1->>Redis: GET stock(100)
线程2->>Redis: SET stock 99
Note over Redis: stock被修改了
线程1->>Redis: MULTI
线程1->>Redis: SET stock 99
线程1->>Redis: EXEC
Redis-->>线程1: nil(事务失败)
Note over 线程1: WATCH的key被修改<br/>事务不执行
缺点:
- ❌ 需要重试(失败了要重新执行)
- ❌ 高并发时重试次数多
- ❌ 性能不如Lua脚本
📊 第三问:四种方式对比
对比表格
| 方式 | 原子性 | 性能 | 易用性 | 适用场景 | 推荐度 |
|---|---|---|---|---|---|
| 原子命令 | ✅ 强 | ⚡⚡ 最快 | ⭐⭐⭐ 简单 | 简单操作 | ⭐⭐⭐⭐⭐ |
| Lua脚本 | ✅ 强 | ⚡⚡ 快 | ⭐⭐ 稍复杂 | 复杂业务逻辑 | ⭐⭐⭐⭐⭐ |
| MULTI/EXEC | ⚠️ 弱 | ⚡ 一般 | ⭐⭐⭐ 简单 | 简单批量操作 | ⭐⭐ |
| WATCH | ✅ 强 | 🐢 慢(需重试) | ⭐ 复杂 | 乐观锁场景 | ⭐⭐ |
选择建议
阿西噶阿西: "选择决策树。"
你的需求是?
│
├─ 简单的自增/自减
│ └─ 使用:INCR/DECR原子命令 ⭐⭐⭐⭐⭐
│
├─ 需要判断 + 操作
│ └─ 使用:Lua脚本 ⭐⭐⭐⭐⭐
│
├─ 简单的批量操作(不需要判断)
│ └─ 使用:MULTI/EXEC
│
├─ 乐观锁场景
│ └─ 使用:WATCH
│
└─ 复杂业务逻辑
└─ 使用:Lua脚本 ⭐⭐⭐⭐⭐
💻 第四问:Lua脚本实战案例
案例1:秒杀扣库存(完整版)
@Service
public class SeckillService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* Lua脚本:秒杀扣库存
* 功能:
* 1. 检查库存
* 2. 检查用户是否已购买
* 3. 扣减库存
* 4. 记录用户购买
*/
private static final String SECKILL_SCRIPT =
"local stockKey = KEYS[1] " + // 库存key
"local userKey = KEYS[2] " + // 用户购买记录key
"local quantity = tonumber(ARGV[1]) " + // 购买数量
"local userId = ARGV[2] " + // 用户ID
// 1. 检查库存是否存在
"local stock = redis.call('get', stockKey) " +
"if not stock then " +
" return -1 " + // 库存key不存在
"end " +
// 2. 检查库存是否足够
"if tonumber(stock) < quantity then " +
" return 0 " + // 库存不足
"end " +
// 3. 检查用户是否已购买
"if redis.call('sismember', userKey, userId) == 1 then " +
" return -2 " + // 已经购买过了
"end " +
// 4. 扣减库存
"redis.call('decrby', stockKey, quantity) " +
// 5. 记录用户购买
"redis.call('sadd', userKey, userId) " +
"return 1"; // 成功
public SeckillResult seckill(Long productId, Long userId, int quantity) {
String stockKey = "stock:product:" + productId;
String userKey = "seckill:users:product:" + productId;
// 执行Lua脚本
Long result = redisTemplate.execute(
new DefaultRedisScript<>(SECKILL_SCRIPT, Long.class),
Arrays.asList(stockKey, userKey),
String.valueOf(quantity),
String.valueOf(userId)
);
if (result == null) {
return SeckillResult.fail("系统错误");
}
switch (result.intValue()) {
case -1:
return SeckillResult.fail("商品不存在");
case -2:
return SeckillResult.fail("您已经购买过了");
case 0:
return SeckillResult.fail("库存不足");
case 1:
return SeckillResult.success("秒杀成功");
default:
return SeckillResult.fail("未知错误");
}
}
}
案例2:分布式锁(带过期时间)
/**
* Lua脚本:加锁
*/
private static final String LOCK_SCRIPT =
"if redis.call('exists', KEYS[1]) == 0 then " +
" redis.call('hset', KEYS[1], ARGV[1], 1) " +
" redis.call('expire', KEYS[1], ARGV[2]) " +
" return 1 " +
"elseif redis.call('hexists', KEYS[1], ARGV[1]) == 1 then " +
" redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
" redis.call('expire', KEYS[1], ARGV[2]) " +
" return 1 " +
"else " +
" return 0 " +
"end";
/**
* Lua脚本:解锁
*/
private static final String UNLOCK_SCRIPT =
"if redis.call('hexists', KEYS[1], ARGV[1]) == 0 then " +
" return nil " +
"end " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1) " +
"if counter > 0 then " +
" return 0 " +
"else " +
" redis.call('del', KEYS[1]) " +
" return 1 " +
"end";
案例3:批量操作 + 限流
/**
* Lua脚本:批量获取 + 限流
*/
private static final String BATCH_GET_SCRIPT =
"local limit = tonumber(ARGV[1]) " +
"if #KEYS > limit then " +
" return redis.error_reply('Too many keys') " +
"end " +
"local result = {} " +
"for i, key in ipairs(KEYS) do " +
" local value = redis.call('get', key) " +
" if value then " +
" table.insert(result, value) " +
" else " +
" table.insert(result, false) " +
" end " +
"end " +
"return result";
public List<String> batchGet(List<String> keys, int limit) {
List<Object> results = redisTemplate.execute(
new DefaultRedisScript<>(BATCH_GET_SCRIPT, List.class),
keys,
String.valueOf(limit)
);
return results.stream()
.map(obj -> obj instanceof String ? (String) obj : null)
.collect(Collectors.toList());
}
💡 最佳实践
1. 优先使用原子命令
// ✅ 推荐:直接用原子命令
redisTemplate.opsForValue().increment("counter");
redisTemplate.opsForValue().decrement("stock");
// ❌ 不推荐:自己实现(不是原子的)
String value = redisTemplate.opsForValue().get("counter");
int newValue = Integer.parseInt(value) + 1;
redisTemplate.opsForValue().set("counter", String.valueOf(newValue));
2. Lua脚本优化
// ✅ 推荐:脚本预编译,复用DefaultRedisScript对象
private static final DefaultRedisScript<Long> DEDUCT_SCRIPT;
static {
DEDUCT_SCRIPT = new DefaultRedisScript<>();
DEDUCT_SCRIPT.setScriptText(DEDUCT_STOCK_SCRIPT);
DEDUCT_SCRIPT.setResultType(Long.class);
}
public boolean deductStock(String key, int quantity) {
return redisTemplate.execute(DEDUCT_SCRIPT,
Collections.singletonList(key),
String.valueOf(quantity)) == 1;
}
// ❌ 不推荐:每次都new DefaultRedisScript
public boolean deductStock(String key, int quantity) {
return redisTemplate.execute(
new DefaultRedisScript<>(DEDUCT_STOCK_SCRIPT, Long.class), // 每次new
Collections.singletonList(key),
String.valueOf(quantity)) == 1;
}
3. 错误处理
try {
Long result = redisTemplate.execute(script, keys, args);
if (result == null) {
log.error("Lua脚本执行失败");
return false;
}
return result == 1;
} catch (Exception e) {
log.error("Redis操作异常", e);
// 降级方案
return fallbackMethod();
}
4. 脚本管理
// 推荐:单独管理Lua脚本
// 1. 脚本文件:resources/lua/deduct_stock.lua
local stock = redis.call('get', KEYS[1])
if not stock then
return -1
end
if tonumber(stock) < tonumber(ARGV[1]) then
return 0
end
redis.call('decrby', KEYS[1], ARGV[1])
return 1
// 2. 加载脚本
@Configuration
public class LuaScriptConfig {
@Bean
public DefaultRedisScript<Long> deductStockScript() {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setLocation(new ClassPathResource("lua/deduct_stock.lua"));
script.setResultType(Long.class);
return script;
}
}
// 3. 使用
@Autowired
private DefaultRedisScript<Long> deductStockScript;
public boolean deductStock(String key, int quantity) {
return redisTemplate.execute(deductStockScript,
Collections.singletonList(key),
String.valueOf(quantity)) == 1;
}
💡 知识点总结
原子性与事务核心要点
✅ 原子性概念
- 操作不可分割
- 要么全部成功,要么全部失败
- Redis单命令天然原子
✅ 4种实现方式
- 原子命令(INCR/DECR等)- 最简单
- Lua脚本 - 最强大(推荐)
- MULTI/EXEC事务 - 有限制
- WATCH乐观锁 - 需重试
✅ Lua脚本优势
- 在Redis中原子执行
- 支持复杂逻辑
- 性能好(减少网络开销)
- 可复用
✅ Redis事务特点
- 不支持回滚
- 部分命令失败不影响其他命令
- 和MySQL事务不同
✅ 实战场景
- 库存扣减:Lua脚本
- 秒杀:Lua脚本 + 判断
- 限流:Lua脚本 + 滑动窗口
- 分布式锁:Lua脚本
✅ 最佳实践
- 优先使用原子命令
- 复杂逻辑用Lua脚本
- 脚本要复用
- 做好错误处理
记忆口诀
Redis单命令原子性,
多命令需要特殊技。
INCR、DECR最简单,
简单场景直接用。
Lua脚本最强大,
复杂逻辑全搞定。
MULTI、EXEC有事务,
不支持回滚要记住。
WATCH实现乐观锁,
并发高时要重试。
生产推荐Lua脚本,
原子性能两兼顾。
🤔 常见面试题
Q1: 如何实现Redis原子性?
A:
4种方式:
1. 使用原子命令(最简单)
- INCR、DECR、INCRBY、DECRBY
- SETNX、GETSET
- 适合简单操作
2. Lua脚本(推荐)
- 脚本整体原子执行
- 支持复杂逻辑
- 性能好
3. MULTI/EXEC事务
- 批量命令一起执行
- 不支持回滚
- 使用场景有限
4. WATCH乐观锁
- 监视key变化
- 需要重试
- 性能较差
生产推荐:简单用原子命令,复杂用Lua脚本
Q2: 除了Lua脚本,还有什么能保证原子性?
A:
1. 原子命令
- INCR、DECR等
- 天然原子
2. MULTI/EXEC事务
- 有限的原子性
- 不支持回滚
3. WATCH乐观锁
- 结合事务使用
- 需要重试逻辑
4. Redis Module(扩展模块)
- 自定义原子命令
- 需要编译安装
实际:Lua脚本是最佳方案
Q3: Redis事务和MySQL事务的区别?
A:
MySQL事务:
- 支持回滚(ROLLBACK)
- 满足ACID
- 部分失败全部回滚
- 支持隔离级别
Redis事务:
- 不支持回滚
- 不满足ACID
- 部分失败继续执行
- 不支持隔离级别
示例:
MySQL:命令2失败 → 命令1回滚
Redis:命令2失败 → 命令1和3照常执行
结论:Redis事务更像"批量执行"
💬 写在最后
从原子性到Lua脚本,我们深入学习了Redis的原子操作:
- ⚛️ 理解了原子性的重要性
- 🔧 掌握了4种实现方式
- 📜 学会了Lua脚本编写
- 💻 完成了秒杀等实战案例
这篇文章,希望能让你彻底掌握Redis原子操作!
如果这篇文章对你有帮助,请:
- 👍 点赞支持
- ⭐ 收藏备用
- 🔄 转发分享
- 💬 评论交流
感谢阅读,期待下次再见! 👋