基于Redis(SETNX)实现分布式锁,案例:解决高并发下的订单超卖,秒杀

2年前 (2022) 程序员胖胖胖虎阿
423 0 0

高并发的情况下订单超卖/秒杀问题

基于Redis(SETNX)实现分布式锁,案例:解决高并发下的订单超卖,秒杀
这里的问题就是在同一时间发过来了两个请求。这两个用户购买同一件数量的商品。线程之间没有互斥。也就是你运行你的。我运行我的。导致在线程1还未实际扣除库存的时候。线程2也获取到了和线程1相同的数量的商品。它两同时扣除库存。这样就出现了订单超卖的问题!!!

解决订单超卖/秒杀的办法

分布式锁?
分布式锁就是在多个服务集群的模式下 每个服务下的线程可以做到互斥的效果 这就是分布式锁 并且分布式锁还要达到 高性能 高可见 高互斥 高可用 这样才叫分布式锁

SETNX的作用

setnx就是使用redis实现分布式锁的一种方式 它的特点就是当有重复的锁key查询缓存就会返回false 或者空 那我们就可以利用它这种机制去实现多个服务同时调用同一个方法让所有服务都有互斥的效果

代码实现创建锁

    private final static String SUO_KEY = "cs:suo:";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 获取锁
     * je: 订单唯一ID
     * @return
     */
    public boolean huoqu(Long je) {
    	// 生成uuid 做锁的value
        String uuid = UUID.randomUUID().toString();
        // stringRedisTemplate.opsForValue().setIfAbsent(SUO_KEY + je, uuid,60,TimeUnit.SECONDS)
        // setIfAbsent() :在java中使用setnx就需要调用setIfAbsent()他会返回一个状态值
        // 根据你当前传送的key 判断是否存在。如果存在返回false。表示锁已被其他线程创建
        // 根据你当前传送的key 判断是否存在。如果存在返回true。表示锁已被当前创建成功
        // SUO_KEY + je :锁的唯一key
        // uuid:value值
        // 60,TimeUnit.SECONDS :失效时间 一定要设置失效时间。防止业务代码异常无法释放锁。可以靠失效时间自动释放
        return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(SUO_KEY + je, uuid,60,TimeUnit.SECONDS));
    }

代码实现释放锁

    /**
     * 释放锁
     * 释放锁就等于将对应的锁key 在缓存中删除即可
     * @return
     */
    public void shanc(Long je) {
        stringRedisTemplate.delete(SUO_KEY + je);
    }

代码实现解决订单超卖/秒杀

package com.macro.mall.service.impl;

import com.macro.mall.common.api.CommonResult;
import com.macro.mall.common.api.ResultCode;
import com.macro.mall.dao.RedisSetNxDAO;
import com.macro.mall.service.RedisSetNxService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
public class RedisSetNxServiceImpl implements RedisSetNxService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedisSetNxDAO redisSetNxDAO;

    private final static String SUO_KEY = "cs:suo:";

    @Override
    public CommonResult cs(Long uid, Long je) {
        // 获取锁
        boolean huoQu = huoqu(je);
        if (!huoQu) {
            try {
                // 没有获取到 持续获取 也可以直接返回一个状态 让他重新请求
                while (!huoqu(je)){
                    // 没有获取到锁 休眠一段时间在重新获取
                    // 目的是为了和上一个线程的业务代码错开
                    // 避免不必要的cpu消耗
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                throw new RuntimeException("异常了!!!!!!!");
            }
        }
        // 获取到了锁 执行代码
        try {
            // 业务代码
            // 查询剩余库存
            Long num = redisSetNxDAO.findKc(je);
            if (num == null || num < 1) {
            	// 库存不足。返回状态
                System.out.println("库存不足!!!!!");
                return CommonResult.failed(ResultCode.FAILED, "库存不足!!!!!");
            }
            // 库存足够 减库存
            // 获取当前线程ID
            Long id = Thread.currentThread().getId();
            // 扣减库存
            redisSetNxDAO.findDele(je);
            // 生成订单 记录基本信息
            redisSetNxDAO.saveKc(uid, je, Long.valueOf(i));
        } catch (Exception e) {
            throw new RuntimeException("异常了!!!!!!!");
        } finally {
            // 锁释放 释放锁一定要放在 finally 下
            // 防止业务代码报错。导致锁无法释放。造成死锁
            shanc(je);
        }
        return CommonResult.success(ResultCode.SUCCESS);
    }


    /**
     * 获取锁
     *
     * @return
     */
    public boolean huoqu(Long je) {
        String uuid = UUID.randomUUID().toString();
        return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(SUO_KEY + je, uuid,60,TimeUnit.SECONDS));
    }

    /**
     * 释放锁
     *
     * @return
     */
    public void shanc(Long je) {
        stringRedisTemplate.delete(SUO_KEY + je);
    }

}

SETNX特点
setnx是redis的锁机制 意思是只能存在一个唯一的key 如果缓存中已经存在这个key 在向缓存保存相同的key那么就会返回一个false 代表锁以被占有 只有获取锁的线程删除了这个key 其他线程才可以继续获取锁 这样就实现了 多线程之间互斥的效果。

注意!!!

  • 锁误删1
    因为设置每个锁的key都会带有一个失效的时间 这么做的目的就是因为怕业务代码出了问题报错了 没有执行删除锁 导致锁阻塞 所以我们可以先给这个锁加一个失效的时间 这样就算业务代码不删除锁 它自己也会释放 但是存在的问题就是 如果的业务代码指定的时间超过了 锁的失效时间 导致 你业务没执行完 锁已经释放了 这时候其他的线程就会获取到这个锁 这时候你这个业务执行完了 执行了删除锁 但是你删除的是线程2的锁 所以这时候锁又被释放了 这种恶性的循环

    解决方法
    每次存到锁key的value值保证一个唯一 也就是这个value只有这一个线程的 那么当失效失效时间过了 其他线程已经拿到了锁 你执行删除锁的时候 先拿你锁的value值。判断你设置的锁key是否存在 如果存在判断是否是和你当前线程设置的value值 如果一致 表示这个锁还是你当时设置的锁 如果不是 表示锁已经释放过 并且已经被其他的线程重新的设置 那么你就不能在执行删除锁

  • 锁误删2
    还有一种就是每次删除锁的时候 都会先判断value值是否相等 相等的删除 不等的不删除 那么如果在你判断完成后 是相等的 并且在删除锁的时候 由于某种原因导致代码阻塞了一段时间 并且时间超过了锁的失效时间 这时候锁被释放了 其他线程获取到了这个锁 而这时候 如果线程不阻塞了 那么就会执行删除锁 因为已经走过了判断条件。所以就会直接执行删除锁操作。其实你删除的已经是其他线程重新设置的锁 这样就又会出现恶性循环的问题

    解决方法
    如果想要解决这种问题 可以直接将查询和删除放到一起执行 但是redis中这两个操作本身就是分为两部分操作的 所以不可以在代码中实现同步操作 只能使用lua脚本保证同步操作 lua脚本就可以实现 将所有的Redis命令都放到一块然后顺序执行 这样只要操作了查询就必然会执行删除 中间不会有任何打断

后期有时间更新RedisSon实现分布式锁 性能更强大

ps:菜鸟一枚 一直在学习 从未敢停止

相关文章

暂无评论

暂无评论...