redis分布式锁

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

分布式锁实现版本一 

锁更新为自己创建和释放不再是以前的synchronized锁

package com.hmdp.utils;

public interface ILock {
    /**
     * 尝试获取锁
     * @param timeoutSec 锁持有的超时时间 过期自动释放
     * @return true代表获取锁成功 false代表获取锁失败
     */
    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 {

    private StringRedisTemplate stringRedisTemplate;
    private String name;
    private static final String KEY_PREFIX="lock:";

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

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取当前线程ID 标识
        long threadId = Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);//直接返回success的话会面临自动拆箱风险如果为null空指针
    }

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

import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
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.utils.RedisIdWorker;
import com.hmdp.utils.SimpleRedisLock;
import com.hmdp.utils.UserHolder;
import org.springframework.aop.framework.AopContext;
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 seckillVoucherService;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        // 3.判断秒杀是否结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("秒杀已经结束!");
        }
        // 4.判断库存是否充足
        if (voucher.getStock()<1) {
            // 库存不足
            return Result.fail("库存不足!");
        }
        // 5.一人一单
        Long userId = UserHolder.getUser().getId();
        // 手动创建锁对象
        SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);
        // 获取锁
        boolean isLock = lock.tryLock(1200);
        // 判断是否获取锁成功
        if (!isLock) {
            // 获取锁失败,返回错误或重试
            return Result.fail("不允许重复下单!");
        }
        try {
            // 获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }finally {
            // 手动释放锁
            lock.unlock();
        }
    }

    // 方法加上同步锁肯定是线程安全的 this指向IVoucherOrderService
    // 不建议加上方法上 因为代表任何用户来了都要加上锁 串行执行 性能低
    // 我们只需要同一用户进来才需要加锁
    @Transactional
    public Result createVoucherOrder(Long voucherId){
        // 5.一人一单
        Long userId = UserHolder.getUser().getId();
        // 当用户id值一样时,锁就一样
        // synchronized (userId.toString().intern()) {
            // 5.1.查询订单
            int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
            // 5.2.判断是否存在
            if (count > 0) {
                //用户已经购买过
                return Result.fail("用户已经购买过一次!");
            }
            // 6.扣减库存
            // eq代表where条件
            boolean success = seckillVoucherService.update().setSql("stock=stock-1")// set stock=stock-1
                    .eq("voucher_id", voucherId).gt("stock", 0)// where id=? and stock>0
                    .update();
            if (!success) {
                // 扣减失败
                return Result.fail("库存不足!");
            }
            // 7.创建订单
            VoucherOrder voucherOrder = new VoucherOrder();
            // 7.1. 订单id
            long orderId = redisIdWorker.nextId("order");
            voucherOrder.setId(orderId);
            // 7.2. 用户id

            voucherOrder.setUserId(userId);
            // 7.3. 代金券id
            voucherOrder.setVoucherId(voucherId);
            save(voucherOrder);

            // 8.返回订单Id
            return Result.ok(orderId);
        //}
    }
}

发送2个请求启动2个服务器结果有1个获取锁成功另1个获取锁失败 

解决了同一个用户只能下一单的问题 

但是上面依旧还有不足之处,出现以下极端情况

线程1获取锁之后,它要去开始执行业务,因为某种原因,业务出现阻塞, 这样一来,锁的持有周期变长,两种情况业务执行完之后再释放锁或者业务阻塞时间太长了甚至于超过我们设置锁的超时时间,触发超时释放锁,这时其他线程获取锁就可以趁虚而入(线程2),获取成功之后,执行自己的业务,就在线程2刚刚获取锁了以后,线程1醒了,线程1业务完成以后,释放锁(没有判断直接释放),这时它释放的是线程2的锁,这时线程2它不知道情况,还在进行自己的业务,就在这时,线程3来了,它趁虚而入,也来获取锁,因为锁被删了,也能获取成功,开始执行自己的业务,此时此刻,同时有2个线程都拿到锁,都在执行业务,所以又一次出现并行执行情况,线程安全问题就发生了

原因其一业务阻塞导致锁提前释放了 其二当线程1醒了这时候的锁不是它自己的,而是线程2的锁,二话不说直接把别人的锁给删了

解决分布式锁误删问题:同一进程内由线程ID区分,不同进程间由UUID区分

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 {

    private StringRedisTemplate stringRedisTemplate;
    private String name;
    private static final String KEY_PREFIX="lock:";
    private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";

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

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取当前线程ID 标识
        // 加上UUID更可靠 因为2台jvm可能会产生相同的线程ID
        String threadId = ID_PREFIX+Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId , timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);//直接返回success的话会面临自动拆箱风险如果为null空指针
    }

    @Override
    public void unlock() {
        // 获取当前线程ID 标识
        String threadId = ID_PREFIX+Thread.currentThread().getId();
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        if (threadId.equals(id)){
            // 释放锁
            stringRedisTemplate.delete(KEY_PREFIX+name);
        }
    }
}

以上锁误删还会出现问题:就是判断是否同一把锁和释放锁是2个动作,即已经判断是同一把锁了,准备要释放锁了这时出现业务阻塞,而且锁超时自动释放,线程2去获取锁成功,执行业务,此时刚好线程1醒了,它就去释放锁了,线程3获取锁也成功,执行业务,这时又出现两个线程并发执行了,不安全

Lua脚本解决多条命令原子性问题

package com.hmdp.utils;

import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements ILock {

    private StringRedisTemplate stringRedisTemplate;
    private String name;
    private static final String KEY_PREFIX="lock:";
    private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT=new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.loc"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }
    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取当前线程ID 标识
        // 加上UUID更可靠 因为2台jvm可能会产生相同的线程ID
        String threadId = ID_PREFIX+Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId , timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);//直接返回success的话会面临自动拆箱风险如果为null空指针
    }

    /*@Override
    public void unlock() {
        // 获取当前线程ID 标识
        String threadId = ID_PREFIX+Thread.currentThread().getId();
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        if (threadId.equals(id)){
            // 释放锁
            stringRedisTemplate.delete(KEY_PREFIX+name);
        }
    }*/

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

unlock.lua脚本文件 

-- 锁的key
--local key="lock:order:5"
--local key=KEYS[1]
-- 当前线程标识
--local threadId="ejoafjoijfoiw-33"
--local threadId=ARGV[1]

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

Redisson入门一人一单 

步骤一:引入依赖

<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.6</version>
        </dependency>

 步骤二:配置Redisson客户端

package com.hmdp.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissionConfig {
    @Bean
    public RedissonClient redissonClient() {
        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
}

步骤三:使用Redisson分布式锁

package com.hmdp.service.impl;

import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
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.utils.RedisIdWorker;
import com.hmdp.utils.SimpleRedisLock;
import com.hmdp.utils.UserHolder;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.aop.framework.AopContext;
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 seckillVoucherService;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedissonClient redissonClient;
    @Override
    public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        // 3.判断秒杀是否结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("秒杀已经结束!");
        }
        // 4.判断库存是否充足
        if (voucher.getStock()<1) {
            // 库存不足
            return Result.fail("库存不足!");
        }
        // 5.一人一单
        Long userId = UserHolder.getUser().getId();
        // 手动创建锁对象
        // SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        // 获取锁
        boolean isLock = lock.tryLock();
        // 判断是否获取锁成功
        if (!isLock) {
            // 获取锁失败,返回错误或重试
            return Result.fail("不允许重复下单!");
        }
        try {
            // 获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }finally {
            // 手动释放锁
            lock.unlock();
        }
    }

    // 方法加上同步锁肯定是线程安全的 this指向IVoucherOrderService
    // 不建议加上方法上 因为代表任何用户来了都要加上锁 串行执行 性能低
    // 我们只需要同一用户进来才需要加锁
    @Transactional
    public Result createVoucherOrder(Long voucherId){
        // 5.一人一单
        Long userId = UserHolder.getUser().getId();
        // 当用户id值一样时,锁就一样
        // synchronized (userId.toString().intern()) {
            // 5.1.查询订单
            int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
            // 5.2.判断是否存在
            if (count > 0) {
                //用户已经购买过
                return Result.fail("用户已经购买过一次!");
            }
            // 6.扣减库存
            // eq代表where条件
            boolean success = seckillVoucherService.update().setSql("stock=stock-1")// set stock=stock-1
                    .eq("voucher_id", voucherId).gt("stock", 0)// where id=? and stock>0
                    .update();
            if (!success) {
                // 扣减失败
                return Result.fail("库存不足!");
            }
            // 7.创建订单
            VoucherOrder voucherOrder = new VoucherOrder();
            // 7.1. 订单id
            long orderId = redisIdWorker.nextId("order");
            voucherOrder.setId(orderId);
            // 7.2. 用户id

            voucherOrder.setUserId(userId);
            // 7.3. 代金券id
            voucherOrder.setVoucherId(voucherId);
            save(voucherOrder);

            // 8.返回订单Id
            return Result.ok(orderId);
        //}
    }
}

Redisson可重入锁原理

package com.hmdp;

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.jupiter.api.BeforeEach;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

@Slf4j
@SpringBootTest
public class RedissonTest {
    @Resource
    private RedissonClient redissonClient;

    private RLock lock;

    @BeforeEach
    void setUp(){
        lock=redissonClient.getLock("order");
    }

    @Test
    void method1(){
        // 尝试获取锁
        boolean isLock=lock.tryLock();
        if (!isLock) {
            log.error("获取锁失败 .... 1");
            return;
        }
        try{
            log.info("获取锁成功 .... 1");
            method2();
            log.info("开始执行业务 ... 1");
        }finally {
            log.warn("准备释放锁 ... 1");
            lock.unlock();
        }
    }
    void method2(){
        // 尝试获取锁
        boolean isLock=lock.tryLock();
        if (!isLock) {
            log.error("获取锁失败 .... 2");
            return;
        }
        try{
            log.info("获取锁成功 .... 2");
            log.info("开始执行业务 ... 2");
        }finally {
            log.warn("准备释放锁 ... 2");
            lock.unlock();
        }
    }
}

 

Redisson分布式锁的锁重试和WatchDog机制

 

 

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值