基于秒杀-----分布式锁----lua脚本

基于商品显示秒杀-一人一单业务_xzm_的博客-CSDN博客改进

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁

 分布式锁的五个基本要求:多进程可见,互斥,高可用,高性能,安全性

三种实现方式

 redis

 1.创建获取锁删除锁的工具类

public interface ILock {

    /**
     * 获取锁
     * @param timeoutSec 自动超时时间
     * @return
     */
    boolean tryLock(long timeoutSec);

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

}
package com.hmdp.utils;

import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements ILock{

    public String name;
    public 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 thread = Thread.currentThread().getId();
        //获取锁
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, thread + "", timeoutSec, TimeUnit.SECONDS);

        return Boolean.TRUE.equals(aBoolean);
    }

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

2.修改代码

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
   @Resource
    private ISeckillVoucherService iSeckillVoucherService;
   @Resource
   private RedisIdWorker redisIdWorker;
   @Autowired
   private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result seckillVoucher(Long voucherId) {
        //查询优惠卷
        SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
        //判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            //秒杀尚未开始
            return Result.fail("秒杀尚未开始,请耐心等待");
        }
        //判断秒杀是否结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            //秒杀已经结束
            return Result.fail("本次秒杀已经结束");
        }
        //秒杀处于正常时间段
        //判断库存是否充足
        if (voucher.getStock()<1) {
            //库存不足
            return Result.fail("本次秒杀已经被抢完");
        }
        //库存充足

        //从拦截器中获取用户id
        Long userId = UserHolder.getUser().getId();
        //对相同的id进行加锁
//        synchronized(userId.toString().intern()) {
            //获取代理对象
//            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            //在service中添加createVoucherOrder方法,也可利用idea代码修复功能自动添加
//            return proxy.createVoucherOrder(voucherId);
//            return createVoucherOrder(voucherId);
//        }

        //创建锁对象
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        //获取锁
        boolean tryLock = lock.tryLock(500);

        if (!tryLock) {
            //获取锁失败
            return Result.fail("每人仅限一单");
        }
        //获取锁成功

        try {
            return createVoucherOrder(voucherId);
        } finally {
            //释放锁
            lock.unlock();
        }


    }

    @Transactional
    public  Result createVoucherOrder(Long voucherId) {
        //从拦截器中获取用户id
        Long userId = UserHolder.getUser().getId();
         /**
          * 进行判断,一人一单
          */
         int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
         if (count > 0) {
             //用户已经购买过商品
             return Result.fail("每位用户仅限购一单");
         }

         //扣减库存
         boolean update = iSeckillVoucherService.update().setSql("stock = stock - 1")
                 .eq("voucher_id", voucherId)
                 .gt("stock", 0)
                 .update();
         //判断库存扣减是否成功
         if (!update) {
             //库存扣减失败,返回信息
             return Result.fail("库存不足");
         }
         //库存扣减成功,添加订单信息
         VoucherOrder voucherOrder = new VoucherOrder();
         //添加订单id
         //使用订单生成器生成id
         long id = redisIdWorker.nextId("order");
         voucherOrder.setId(id);
         //添加用户id

         voucherOrder.setUserId(userId);
         //添加消费卷id
         voucherOrder.setVoucherId(voucherId);
         //添加到数据库
         boolean save = this.save(voucherOrder);
         if (!save) {
             //添加失败
             return Result.fail("下单失败");
         }
         return Result.ok(id);
    }
}

测试结果:实现了多个服务器下的一人一单

现阶段存在问题:当线程阻塞时间超过setnx的自动过期时间时可能导致一人多单和setnx的key误删情况

 优化误删问题

思路:

 实现:

修改工具类

package com.hmdp.utils;

import cn.hutool.core.lang.UUID;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements ILock{

    public String name;
    public StringRedisTemplate stringRedisTemplate;

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

    private static final String KEY_PREFIX="lock:";
    private static final String ID_PREFIX= UUID.fastUUID().toString(true)+"-";



    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程标识
        String thread = ID_PREFIX+Thread.currentThread().getId();
        //获取锁
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, thread , timeoutSec, TimeUnit.SECONDS);

        return Boolean.TRUE.equals(aBoolean);
    }

    @Override
    public void unlock() {
        //获取锁标识
        String thread = ID_PREFIX+Thread.currentThread().getId();
        //获取redis中的值
        String s = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        if (thread.equals(s)){
            //释放锁
            stringRedisTemplate.delete(KEY_PREFIX+name);
        }
    }
}

现阶段依旧存在误删除的问题

逻辑:

 解决方法:让判断锁标识与释放锁保持原子性

Lua脚本

 解决方法

1.创建nulock.lua文件

2.编写lua脚本

-- 获取锁中的线程标识 get key
local  id=redis.call('get',KEYS[1])
--比较线程标识与锁中的标识是否一致
if id == ARGV[1] then
    --释放锁
    return redis.call('del',KEYS[1])
end
return 0

3.修改unlock方法

 //创建接收lua脚本
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    //在静态方法中初始化UNLOCK_SCRIPT
    static {
        UNLOCK_SCRIPT=new DefaultRedisScript<>();
        //设置接收lua脚本文件
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("nulock.lua"));
        //设置返回值类型
        UNLOCK_SCRIPT.setResultType(Long.class);
    }


    @Override
    public void unlock() {
        //使用lua脚本
        stringRedisTemplate.execute(UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX+Thread.currentThread().getId()
                );
    }

到目前为止已经可以做到生产可用

redis分布式锁的优化

需要优化的问题:不可重入,不可重试,超时释放,主从一致

 解决方法:使用redis的框架redisson实现分布式锁

 使用方法:

1.引入依赖

<!--        redis框架redisson-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.6</version>
        </dependency>

2.编写配置文件


@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient(){
        //配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.2.182:6379").setPassword("123456");
        //创建Redissonclient对象
        return Redisson.create(config);
    }
}

3.修改业务类(仅进行注入了RedissonClient 和创建锁对象和trylock的参数)

package com.hmdp.service.impl;

import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.Voucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.service.IVoucherService;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.SimpleRedisLock;
import com.hmdp.utils.UserHolder;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
   @Resource
    private ISeckillVoucherService iSeckillVoucherService;
   @Resource
   private RedisIdWorker redisIdWorker;
   @Autowired
   private StringRedisTemplate stringRedisTemplate;
    @Autowired
   private RedissonClient redissonClient;

    @Override
    public Result seckillVoucher(Long voucherId) {
        //查询优惠卷
        SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);
        //判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            //秒杀尚未开始
            return Result.fail("秒杀尚未开始,请耐心等待");
        }
        //判断秒杀是否结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            //秒杀已经结束
            return Result.fail("本次秒杀已经结束");
        }
        //秒杀处于正常时间段
        //判断库存是否充足
        if (voucher.getStock()<1) {
            //库存不足
            return Result.fail("本次秒杀已经被抢完");
        }
        //库存充足

        //从拦截器中获取用户id
        Long userId = UserHolder.getUser().getId();
        //对相同的id进行加锁
//        synchronized(userId.toString().intern()) {
            //获取代理对象
//            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            //在service中添加createVoucherOrder方法,也可利用idea代码修复功能自动添加
//            return proxy.createVoucherOrder(voucherId);
//            return createVoucherOrder(voucherId);
//        }

        //创建锁对象
//        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        //获取锁
        boolean tryLock = lock.tryLock();

        if (!tryLock) {
            //获取锁失败
            return Result.fail("每人仅限一单");
        }
        //获取锁成功

        try {
            return createVoucherOrder(voucherId);
        } finally {
            //释放锁
            lock.unlock();
        }


    }

    @Transactional
    public  Result createVoucherOrder(Long voucherId) {
        //从拦截器中获取用户id
        Long userId = UserHolder.getUser().getId();
         /**
          * 进行判断,一人一单
          */
         int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
         if (count > 0) {
             //用户已经购买过商品
             return Result.fail("每位用户仅限购一单");
         }

         //扣减库存
         boolean update = iSeckillVoucherService.update().setSql("stock = stock - 1")
                 .eq("voucher_id", voucherId)
                 .gt("stock", 0)
                 .update();
         //判断库存扣减是否成功
         if (!update) {
             //库存扣减失败,返回信息
             return Result.fail("库存不足");
         }
         //库存扣减成功,添加订单信息
         VoucherOrder voucherOrder = new VoucherOrder();
         //添加订单id
         //使用订单生成器生成id
         long id = redisIdWorker.nextId("order");
         voucherOrder.setId(id);
         //添加用户id

         voucherOrder.setUserId(userId);
         //添加消费卷id
         voucherOrder.setVoucherId(voucherId);
         //添加到数据库
         boolean save = this.save(voucherOrder);
         if (!save) {
             //添加失败
             return Result.fail("下单失败");
         }
         return Result.ok(id);
    }
}

原理:

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值