【Redis 开发】一人一单,超卖问题(悲观锁,乐观锁,分布式锁)

发布于:2024-04-30 ⋅ 阅读:(32) ⋅ 点赞:(0)

悲观锁

认为线程问题一定会发生,因此在操作数据库之前先获取锁,确保线程串行执行,例如Synchronized,Lock都属于悲观锁

乐观锁

认为线程安全问题不一定会发生,因此不加锁,只有在更新数据的时候去判断有没有其他线程对数据做了修改

  1. 如果没有修改则认为是安全的,自己才更新数据
  2. 如果已经被其它线程修改说明发生了安全问题,此时可以重试或异常

乐观锁的关键是判断之前查询到的数据是否被修改过,有两种方法:

第一种:版本号法

就是在商品上添加版本号,一个线程查询用户的时候查询到id和版本号信息,当他需要进行修改数据的时候进行对比现在的版本号看有没有发生变化马,如果有就停止操作,并重新开始,如果没有则修改数据并将版本号加一,每个线程都执行此操作

第二种:CAS法

CAS法就是在版本号法上面做出的微调,将监控版本号修改为直接监控库存就行

实现乐观锁

就是在扣减库存的时候加上一个条件。判断库存有没有发生变化

boolean success = seckillVoucherService.update()
         .setSql("Stock=stock-1")
         .eq("voucher_id",voucherId).gt("stock",0)
         .update();

但是这样请求的失败率太高,我们可以进行稍微的修改,将判断条件库存大于0就行

悲观锁与乐观锁的比较

悲观锁:
优点:简单粗暴
缺点: 性能一般
乐观锁:
优点:性能好
缺点: 存在成功率低的问题

一人一单

超卖问题一般对于秒杀一个商品时所采用的技术,但是对于秒杀商品这种东西,一般一个用户只能够买一个,下一单

那么如何实现一人一单的问题呢:
就是在下单的时候,在订单数据库中进行查询如果该用户已经存在单子,那么会返回异常,如果在库中没有该用户,在创建新的订单,注意在创建新的订单之前需要使用悲观锁,防止多线程问题

注意:这里只能使用悲观锁,因为使用乐观锁无法进行判断

将从查询库中的信息开始到创建新的数据,封装在一个方法中,使用悲观锁synchronized并在该方法上添加事务

分布式锁

上述的乐观锁在进行用户并发中防止一个用户多次进行买单的处理,这只解决了单一服务器的多并发问题,当服务器处在集群中时,这个锁是锁不住的

原因:在多个集群中就会有多个jvm下的锁监视器,这些锁监视器只会监控本服务器下的悲观锁

所以为了控制集群下的多线程问题,我们采取分布式锁

那么什么是分布式锁:

就是多个服务器共用一个锁监视器,在同一个锁监视器中进行锁的监视

分布式锁的核心是实现多进程间的互斥,而满足这一点的方式有很多:
在这里插入图片描述

这里我们主要通过Redis实现分布式锁机制:

Redis实现分布式锁

实现和前面的讲的缓存击穿是所使用的方法类似
在Redis客户端模拟实现
set lock thread1 EX 10 NX
这样就会保证设置互斥和释放锁的原子性

多线程访问访问多集群的分布式锁工作流程:
在这里插入图片描述

  • 实现

首先定义一个锁的接口,实现该接口,利用Redis实现分布式锁功能

public interface ILock {

    /**
     * 尝试获取锁
     * @param timeoutSec
     * @return
     */
    boolean tryLock(long timeoutSec);

    /**
     * 释放锁
     */
    void unlock();
}

实现上述接口进行Redis操作

public class SimpleRedisLock implements ILock{

    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String key_prefix="lock:";
    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程的标识
        long id = Thread.currentThread().getId();
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key_prefix + name, id + "", timeoutSec, TimeUnit.SECONDS);
        //自动拆箱的返回
        return Boolean.TRUE.equals(aBoolean);
    }

    @Override
    public void unlock() {
        //释放锁
        stringRedisTemplate.delete(key_prefix+name);
    }
}


网站公告

今日签到

点亮在社区的每一天
去签到