流程

在这里插入图片描述

解决超卖问题

原因: 多用户同时抢购,并发查询库存都显示有货,一起下单扣库存,扣多了。
悲观锁: 认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。效率差

乐观锁

认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改。
版本号法:
在这里插入图片描述
优化一:以判断库存是否改变 来判断是否被修改

  • 问题:多个线程同时访问的时候会导致只有一个线程成功购买,导致少卖

优化二:直接判断库存是否大于 0,大于就卖

一人一单

单服务器

实现:根据用户 id 以及商品 id 判断是否购买过,如果购买过就直接返回
此时连续请求可能会导致多次下单,所以要对用户 id 加互斥锁保证串行执行

多服务器(集群):

每个服务器都有自己的锁监视器,服务器 a 的互斥锁无法对服务器 b 的锁起到互斥作用,多个服务器同时用一个账号购买的话仍会造成一人多单,所以需要加分布式锁

分布式锁

多台服务器抢同一个共享资源时,让多个服务器使用一个锁监视器,保证同一时间只有一台机器能操作,避免数据错乱。
常用 redis、mysql、zookeeper 实现

reids setnx 实现分布式锁

在这里插入图片描述

  1. 加锁:
// 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);
}
  1. 释放锁
// 错误!非原子!
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(会误删)
    1. 线程 A 加锁成功
    2. 线程 A 业务很慢,执行了 4 秒
    3. Redis 自动过期释放锁(3 秒到了)
    4. 线程 B 马上抢到锁 → 开始执行业务
    5. 线程 A 终于执行完了
    6. 线程 A 直接执行 del key → 把线程 B 的锁删了

问题

  1. 不可重入(同一个进程无法多次获取锁)
methodA() {
    lock(); // 加锁成功
    methodB(); // 再次尝试加锁
}
methodB() {
    lock(); // 阻塞/加锁失败
}
  1. 不可重试,只能手动写循环重试会占用 cpu、增加 redis 压力
  2. 主从一致性
1.主节点加锁成功
2.主节点立刻宕机,锁还没同步到从节点
3.从节点升级为主节点
4.锁丢失,其他客户端可以加锁
根本原因:
Redis 异步复制,不保证强一致性。

redisson 实现

  1. 引入依赖
  2. 配置类中配置 redis
  3. 使用
@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);
    }

优化

  1. 新增秒杀优惠券的同时,将优惠券信息保存到 Redis 中
  2. 基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
  3. 如果抢购成功,将优惠券id和用户id封装后存入阻塞队列
  4. 开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能
    redis 需存储商品库存,购买过的用户(set 存储判断一人一单)
    在这里插入图片描述
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐