【无标题】
每个服务器都有自己的锁监视器,服务器 a 的互斥锁无法对服务器 b 的锁起到互斥作用,多个服务器同时用一个账号购买的话仍会造成一人多单,所以需要加分布式锁。认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。实现:根据用户 id 以及商品 id 判断是否购买过,如果购买过就直接返回。多台服务器抢同一个共享资源时,让多个服务器使用一个锁监视器,保证。认为线程安全问题不一定会发生
电商秒杀实现
流程

解决超卖问题
原因: 多用户同时抢购,并发查询库存都显示有货,一起下单扣库存,扣多了。
悲观锁: 认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。效率差
乐观锁
认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改。
版本号法:
优化一:以判断库存是否改变 来判断是否被修改
- 问题:多个线程同时访问的时候会导致只有一个线程成功购买,导致少卖
优化二:直接判断库存是否大于 0,大于就卖
一人一单
单服务器
实现:根据用户 id 以及商品 id 判断是否购买过,如果购买过就直接返回
此时连续请求可能会导致多次下单,所以要对用户 id 加互斥锁保证串行执行
多服务器(集群):
每个服务器都有自己的锁监视器,服务器 a 的互斥锁无法对服务器 b 的锁起到互斥作用,多个服务器同时用一个账号购买的话仍会造成一人多单,所以需要加分布式锁
分布式锁
多台服务器抢同一个共享资源时,让多个服务器使用一个锁监视器,保证同一时间只有一台机器能操作,避免数据错乱。
常用 redis、mysql、zookeeper 实现
reids setnx 实现分布式锁

- 加锁:
// redis单行操作是原子性的
// NX:键不存在才创建,保证互斥
// EX:设置过期时间,规避宕机死锁
// unique_id:唯一标识,防止误删锁
SET lock_key unique_id NX EX expire_time
// 老写法加锁和设置过期时间是分开写的,如果设置过期时间之前服务器就挂了
// 就会导致死锁
// java实现:
public boolean tryLock(String lockKey, String lockId, long expireSec) {
return redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockId, expireSec, TimeUnit.SECONDS);
}
- 释放锁
// 错误!非原子!
if (redis.get(lockKey).equals(myLockId)) {
// 在这里卡顿1毫秒,锁就可能过期被别人抢走,就会误删
redis.del(lockKey);
}
// 解锁
public boolean unLock(String lockKey, String lockId) {
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
lockId
);
return result == 1;
}
- 注意:不可以直接删除 key(会误删)
- 线程 A 加锁成功
- 线程 A 业务很慢,执行了 4 秒
- Redis 自动过期释放锁(3 秒到了)
- 线程 B 马上抢到锁 → 开始执行业务
- 线程 A 终于执行完了
- 线程 A 直接执行 del key → 把线程 B 的锁删了!
问题:
- 不可重入(同一个进程无法多次获取锁)
methodA() {
lock(); // 加锁成功
methodB(); // 再次尝试加锁
}
methodB() {
lock(); // 阻塞/加锁失败
}
- 不可重试,只能手动写循环重试会占用 cpu、增加 redis 压力
- 主从一致性
1.主节点加锁成功
2.主节点立刻宕机,锁还没同步到从节点
3.从节点升级为主节点
4.锁丢失,其他客户端可以加锁
根本原因:
Redis 异步复制,不保证强一致性。
redisson 实现
- 引入依赖
- 配置类中配置 redis
- 使用
@RestController
public class RedissonDemoController {
@Autowired
private RedissonClient redissonClient;
public String lockDemo() {
// 锁名称
String lockKey = "order:lock:1001";
// 获取锁
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁:等待3秒,锁自动释放10秒
boolean success = lock.tryLock(3, 10, java.util.concurrent.TimeUnit.SECONDS);
if (success) {
System.out.println("获取锁成功,执行业务逻辑");
Thread.sleep(5000); // 模拟业务
return "执行成功";
} else {
return "获取锁失败";
}
} catch (InterruptedException e) {
return "异常";
} finally {
// 释放锁(必须)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
lua 脚本
是一门轻量级、嵌入式、高性能的脚本语言。Redis 执行 Lua 脚本时,整个脚本是原子执行的,不会被其他命令打断。
lua 执行 redis 命令
// 执行命令,出错直接报错(常用)
redis.call("命令名", 参数1, 参数2, 参数3...)
redis.call("SET", "name", "zhangsan")
redis.call("GET", "name")
// 执行命令,出错返回错误信息,不中断脚本
redis.pcall()
redis 执行 lua 脚本
EVAL “脚本内容” key数量 key1 key2 … arg1 arg2 …
EVAL “return redis.call(‘SET’, KEYS[1], ARGV[1])” 1 name tom
// 等价于 SET name tom
// 开发中(硬编码)
// execute参数1:lua脚本;参数2:KEYS;参数3:ARGV
String script = """
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end
""";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
lockId
);
// 也可以在resources下创建脚本,在代码中加载使用
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("文件名"));
// 指定返回值
SECKILL_SCRIPT.setResultType(Long.class);
}
优化
- 新增秒杀优惠券的同时,将优惠券信息保存到 Redis 中
- 基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
- 如果抢购成功,将优惠券id和用户id封装后存入阻塞队列
- 开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能
redis 需存储商品库存,购买过的用户(set 存储判断一人一单)
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)