spring-data-redis之redisTemplate实现redis分布式锁,模拟高并发抢购场景

相关框架:spring、spring-data-redis、lettuce

Step1、编写分布式锁工具,完成加锁与释放锁功能

/**
 * 基于redis的分布式锁实现工具类
 * 高并发访问同一资源时,使所有访问线程同步进行操作,使得该方法具有原子性
 * 该资源可以存储在任何地方
 * 例如:修改某库存数量,方法中先判断库存是否大于1,然后减去1
 * 这两个操作中间可能延迟,让其他线程先把库存减成了0,最后库存变成了负数
 * 所以需要引入分布式锁操作
 */
@Component
@Transactional
public class LockUtil {
    @Autowired
    @Qualifier("jsonSyncRedisTemplate")
    //JSON序列化、同步
    private RedisTemplate jsonSyncRedisTemplate;
    //用于存储标识此线程的ID
    private ThreadLocal threadIdBox = new ThreadLocal<>();

    /**
     * 加锁
     * @param serviceId KEY  用于标识高并发修改资源的业务
     * @param timeout   超时时间
     * @param timeUnit  超时单位
     * @return 创建KEY成功返回true   创建KEY失败返回false
     */
    public boolean lock(String serviceId, Long timeout, TimeUnit timeUnit) {
        try {
            threadIdBox.set(UUID.randomUUID().toString());
            //如果KEY不存在,则创建KEY
            //KEY不存在,返回true  存在返回false
            return jsonSyncRedisTemplate.opsForValue().setIfAbsent(serviceId, threadIdBox.get(), timeout, timeUnit);
        } catch (Exception e) {
            //引发任何异常,spring将不会创建KEY,直接返回false
            threadIdBox.remove();
            return false;
        }
    }

    /**
     * 释放锁
     * @param serviceId  KEY  用于标识高并发修改资源的业务
     * @return
     */
    public boolean unlock(String serviceId) {
        //当前线程没有生成线程标识,说明压根没有创建过KEY
        if (threadIdBox.get() == null) return true;
        try {
            //lua脚本(保证查询和删除的原子性)
            //如果根据serviceId查找出的值与当前线程的标识码一致,则删除该KEY并返回删除结果,没找到KEY直接返回false
            String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
            DefaultRedisScript redisScript = new DefaultRedisScript(luaScript);
            redisScript.setResultType(Boolean.class);
            DefaultScriptExecutor defaultScriptExecutor = new DefaultScriptExecutor(jsonSyncRedisTemplate);
            return (boolean) defaultScriptExecutor.execute(redisScript, Arrays.asList(serviceId), threadIdBox.get());
        } catch (Exception e) {
            //引发任何异常,spring将不会执行删除操作,直接返回false
            return false;
        } finally {
            threadIdBox.remove();
        }
    }
}

Step2、编写库存实体类

/**
 * 库存实体类
 */
@Component
public class Inventory {
    private Long commodityId;           //商品ID
    private String commodityName;       //商品名称
    private Long surplus;               //剩余量
    private Long lucyNumber;            //幸运用户数
}

Step3、编写业务类,完成库存更新方法

/**
 * 业务操作类
 */
@Component
public class MyService {
    @Autowired
    private ApplicationContext applicationContext;
    /**
     * 高并发修改资源的方法
     * 该资源例如:一条库存记录,该数据存储在本机内存中
     * 库存减1成功  返回true  其他情况返回false
     */
    public boolean minus(){
        Inventory inventory = (Inventory) applicationContext.getBean("inventory");
        Long surplus = inventory.getSurplus();
        Long lucyNumber = inventory.getLucyNumber();
        if(surplus > 0){
            inventory.setSurplus(--surplus);
            inventory.setLucyNumber(++lucyNumber);
            return true;
        }
        return false;
    }
}

Step4、编写环绕通知类,对业务类方法进行前后加锁与释放锁操作

@Component
@Aspect
public class LockAspect {
    @Autowired
    private LockUtil lockUtil;//注入分布式锁工具类
    //定义业务标识
    private static final String SERVICE_ID = "物流系统->库存管理模块->库存更新服务";
    //定义MyService类minus方法操作的超时时间  10秒
    private static final Long TIMEOUT = 10L;
    //定义MyService类minus方法操作的超时时间单位
    private static final TimeUnit TIME_UNIT = TimeUnit.SECONDS;

    //环绕通知
    @Around("execution(* com.night.redis.lock.MyService.minus(..))))")
    public boolean around(ProceedingJoinPoint joinPoint)throws Throwable{
        try{
            boolean getLock = lockUtil.lock(SERVICE_ID,TIMEOUT,TIME_UNIT);//加锁
            if(!getLock){  //没获取到锁
                return false;
            }else {     //获取到锁后执行业务逻辑操作
                if(!(boolean) joinPoint.proceed())
                    throw new SoldOutException();
                return true;
            }
        }catch (Throwable e){
           throw e;
        } finally {
            lockUtil.unlock(SERVICE_ID);//释放锁
        }
    }
    //自定义异常类(卖完了异常)
    class SoldOutException extends RuntimeException {
    }
}

Step5、编写线程类,模拟用户行为

/**
 * 模拟抢购线程
 */
@Component
@Scope("prototype")
public class MyTask implements Runnable{
    @Autowired
    private MyService myService;
    @Autowired
    private ApplicationContext applicationContext;

    @Override
    public void run() {
        Inventory inventory = (Inventory) applicationContext.getBean("inventory");
        try{
            //抢购失败,歇半秒后再抢
            while(!myService.minus()){
                String s = String.format("【%s】抱歉,当前拥堵,稍后再试!!",Thread.currentThread().getName());
                System.out.println(s);
                try {
                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (InterruptedException e) {
                }
            }
            //抢到之后自动停止
            String s = String.format("恭喜【%s】抢到了一辆【%s】,库存余量为:【%s】,当前幸运用户数为:【%s】",
                    Thread.currentThread().getName(),inventory.getCommodityName(),
                    inventory.getSurplus(),inventory.getLucyNumber());
            System.out.println(s);
        }catch (LockAspect.SoldOutException e){
            //没抢到。。。
            String s = String.format("【%s】,很遗憾,你没抢到,活动已结束...",Thread.currentThread().getName());
            System.out.println(s);
        }
    }
}

Step6、编写启动类

public class Starter {
    public static void main(String[] args)throws Exception {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext();
        context.setConfigLocation("classpath:spring/spring-*.xml");
        context.refresh();
        //定义一个库存实体
        Inventory inventory = (Inventory) context.getBean("inventory");
        inventory.setCommodityId(656232323265L);
        inventory.setCommodityName("兰博基尼(豪华镶钻版)1.0元抢购版");
        inventory.setSurplus(800L);       //库存800
        inventory.setLucyNumber(0L);      //幸运用户数
        //定义1000个人来抢购商品,一个人限购一件,注定有200人抢不到
        for(int  i = 0 ; i < 1000 ; i ++){
            MyTask person = context.getBean("myTask", MyTask.class);
            new Thread(person,"用户"+i).start();
        }
    }
}

Step7、查看执行效果

spring-data-redis之redisTemplate实现redis分布式锁,模拟高并发抢购场景_第1张图片

 

你可能感兴趣的:(spring-data-redis之redisTemplate实现redis分布式锁,模拟高并发抢购场景)