• 锁的使用


    一、业务场景分析

      在实际开发中,我们需要使用到锁,来防止并发问题,以秒杀为例,如果没有锁的处理,就会发生超卖的问题,超卖问题的出现,是由于多线程并发处理,一个事务未提交,但是另外的线程来查询时,可以查询到仍有库存,就会发生超卖。

      解决这类问题,一般有三种方案:

        1、使用锁处理

          也就是让所有的操作都串行的执行,

        2、让所有的操作原子性执行

          因为redis是天然的单线程的操作,因此可以使用redis进行处理,也就是将库存数据存储在redis中,使用 res = hincrent(“seckill_goods_stock_1”,-1) 进行库存的扣减,不过这里还需要判断一下库存是否需要满足。

        3、使用队列

          队列的长度等于库存的数量,然后队列中的存储的数据是商品ID,然后使用POP操作,进行扣减库存,同时每一种商品对应一个队列。

      下面就从锁的角度来解决这种问题。

      针对项目部署的情况,可以使用单机锁和分布式锁,单机锁和分布式锁,分布式锁又可以使用数据库、Redis、ZK、etcd来实现

    二、单机锁

    (一)单机锁与事务

       JDK中提供了ReentranLock,胆码逻辑如下。

        //程序锁
        //互斥锁 参数默认false,不公平锁
        private Lock lock = new ReentrantLock(true);
    public HttpResult startKilled(Long killId, String userId){
            // 加锁
            lock.lock();
            try {
                // 执行业务
            } catch (Exception e) {
                //异常处理
            } finally {
                // 释放锁
                lock.unlock();
            }
            return null;
        }

      其实这样是有个问题的,就是会存在超卖问题,这是因为事务是在该方法执行完之后才会提交,但是在事务提交前,就是释放了锁,导致超卖。

      对于这种事务在释放锁之后的问题,只需要将加锁和释放锁的操作往上提取,在事务提交之后处理即可,但是所有的操作加锁和解锁操作都放在service上层,就会使Controller层太臃肿,因此可以使用自定义注解来进行处理。

    (二)AOP方式解决超卖问题

      1、定义一个注解

    @Target({ElementType.PARAMETER,ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface ServiceLock {
    
        String description() default "";
    
    }

      2、编写AOP切面

      在切面中使用自定义的注解,然后使用环绕通知,在业务处理的前后进行加锁和解锁操作。

      这里需要特殊说明一点,由于该注解需要在事务注解前执行,因此需要@Order注解来注明注解的执行顺序,oder为int类型,越小越早执行,由于事务注解没有Order,那么就说明其Order的默认值为int的最大值(2147483647),因此只要设置order大于该值,那么该注解就会在事务注解前执行。

    @Component
    @Scope
    @Aspect
    @Order(1)
    public class LockAspect {
        // 定义锁对象
        private static Lock lock = new ReentrantLock(true);
        @Pointcut("@annotation(com.sugo.seckill.aop.lock.ServiceLock)")
        public void lockAspect(){
    
        }
        // 增强方法
        @Around("lockAspect()")
        public Object around(ProceedingJoinPoint joinPoint){
            Object obj = null;
            // 加锁
            lock.lock();
            // 执行业务
            try {
                obj = joinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            } finally {
                // 释放锁
                lock.unlock();
            }
            return obj;
        }
    }

      3、业务代码使用单机锁注解

        @Transactional
        @ServiceLock
        @Override
        public HttpResult startKilledByLocked(Long killId, String userId) {
                //业务处理
                //下单
                return HttpResult.ok("秒杀成功");
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }

    三、分布式锁--Mysql

      对于分布式部署的项目来说,单机锁肯定不行,因此就需要分布式锁。所谓分布式锁,就是依赖于第三方的组件,完成锁的处理,目前比较常见的,可以使用数据库、Redis、ZK、etcd等。

      对于单机锁,其锁的是线程,也就是防止多线程操作同一资源,而分布式锁,其锁的是进程,也就是防止多个进程间操作同一资源。

      锁分为悲观锁和乐观锁。

      1、悲观锁的实现方式

        悲观锁的实现方式,其实就是在查询的时候使用for update,这种处理的好处是非常简单,缺点是非常影响性能。

        @Select(value = "select * from tb_seckill_goods where id = #{seckillId} for update")
        TbSeckillGoods selectByPrimaryKeyBySQLLock(Long seckillId);

      2、乐观锁的实现方式

        乐观锁的实现方式,其实就是加上一个版本号,在提交事务的时候保证提交时的版本和查询时的版本号一致。

        @Update(value = "UPDATE tb_seckill_goods SET stock_count=stock_count-1 WHERE id=#{seckillId} AND stock_count>0")
        int updateSeckillGoodsByPrimaryKeyByLock(@Param("seckillId") Long seckillId);

    四、分布式锁--redis

    (一)Reddssion客户端

      如果使用Redis锁,强烈建议使用Redssion客户端,因为Redssion客户端已经对分布式锁做的非常完善。对于可重入锁、异步锁、公平锁、联锁、红锁都做了实现,具体的demo如下所示。

    /**
     * redis分布式锁Demo
     * @author hubin
     */
    public class RedissLockDemo {
        /**
         * 可重入锁(Reentrant Lock) 
         * Redisson的分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口,同时还支持自动过期解锁
         * @param redisson
         */
        public void testReentrantLock(RedissonClient redisson) {
            RLock lock = redisson.getLock("anyLock");
            try {
                // 1. 最常见的使用方法
                // lock.lock();
                // 2. 支持过期解锁功能,10秒钟以后自动解锁, 无需调用unlock方法手动解锁
                // lock.lock(10, TimeUnit.SECONDS);
                // 3. 尝试加锁,最多等待3秒,上锁以后10秒自动解锁
                boolean res = lock.tryLock(3, 10, TimeUnit.SECONDS);
                if (res) { // 成功
                    // do your business
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
        /**
         * Redisson同时还为分布式锁提供了异步执行的相关方法
         * @param redisson
         */
        public void testAsyncReentrantLock(RedissonClient redisson) {
            RLock lock = redisson.getLock("anyLock");
            try {
                lock.lockAsync();
                lock.lockAsync(10, TimeUnit.SECONDS);
                Future<Boolean> res = lock.tryLockAsync(3, 10, TimeUnit.SECONDS);
                if (res.get()) {
                    // do your business
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
        /**
         * 公平锁(Fair Lock)
         * Redisson分布式可重入公平锁也是实现了java.util.concurrent.locks.Lock接口的一种RLock对象。
         * 在提供了自动过期解锁功能的同时,保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。
         * @param redisson
         */
        public void testFairLock(RedissonClient redisson){  
            RLock fairLock = redisson.getFairLock("anyLock");  
            try{  
                // 最常见的使用方法  
                fairLock.lock();  
                // 支持过期解锁功能, 10秒钟以后自动解锁,无需调用unlock方法手动解锁  
                fairLock.lock(10, TimeUnit.SECONDS);  
                // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁  
                boolean res = fairLock.tryLock(100, 10, TimeUnit.SECONDS);  
                if (res) {
                    // do your business
                }
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            } finally {  
                fairLock.unlock();  
            }  
    //      Redisson同时还为分布式可重入公平锁提供了异步执行的相关方法:
    //        RLock fairLock = redisson.getFairLock("anyLock");  
    //        fairLock.lockAsync();  
    //        fairLock.lockAsync(10, TimeUnit.SECONDS);  
    //        Future<Boolean> res = fairLock.tryLockAsync(100, 10, TimeUnit.SECONDS);  
        } 
        /**
         * 联锁(MultiLock)
         * Redisson的RedissonMultiLock对象可以将多个RLock对象关联为一个联锁,每个RLock对象实例可以来自于不同的Redisson实例
         * @param redisson1
         * @param redisson2
         * @param redisson3
         */
        public void testMultiLock(RedissonClient redisson1,RedissonClient redisson2, RedissonClient redisson3){  
            RLock lock1 = redisson1.getLock("lock1");  
            RLock lock2 = redisson2.getLock("lock2");  
            RLock lock3 = redisson3.getLock("lock3");  
            RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);  
            try {  
                // 同时加锁:lock1 lock2 lock3, 所有的锁都上锁成功才算成功。  
                lock.lock();  
                // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁  
                boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);  
                if (res) {
                    // do your business
                }
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            } finally {  
                lock.unlock();  
            }  
        }  
        /**
         *  红锁(RedLock)
         *  Redisson的RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例
         * @param redisson1
         * @param redisson2
         * @param redisson3
         */
        public void testRedLock(RedissonClient redisson1,RedissonClient redisson2, RedissonClient redisson3){  
            RLock lock1 = redisson1.getLock("lock1");  
            RLock lock2 = redisson2.getLock("lock2");  
            RLock lock3 = redisson3.getLock("lock3");  
            RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);  
            try {  
                // 同时加锁:lock1 lock2 lock3, 红锁在大部分节点上加锁成功就算成功。  
                lock.lock();  
                // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁  
                boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);  
                if (res) {
                    // do your business
                }
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            } finally {  
                lock.unlock();  
            }  
        }  
        //读写锁(ReadWriteLock)、信号量(Semaphore)、可过期性信号量(PermitExpirableSemaphore)、闭锁(CountDownLatch)
    }

    (二)对于超卖问题使用redis

      无论对于Redis锁还是ZK锁,其实都和单机锁存在同样的问题,就是事务提交时间的问题,所以使用Redis和ZK都可以使用AOP将锁的处理前移。

      1、添加Redisson工具类

    public class RedissLockUtil {
        private static RedissonClient redissonClient;
        
        public void setRedissonClient(RedissonClient locker) {
            redissonClient = locker;
        }
        
        /**
         * 加锁
         * @param lockKey
         * @return
         */
        public static RLock lock(String lockKey) {
            RLock lock = redissonClient.getLock(lockKey);
            lock.lock();
            return lock;
        }
    
        /**
         * 释放锁
         * @param lockKey
         */
        public static void unlock(String lockKey) {
            RLock lock = redissonClient.getLock(lockKey);
            lock.unlock();
        }
        
        /**
         * 释放锁
         * @param lock
         */
        public static void unlock(RLock lock) {
            lock.unlock();
        }
    
        /**
         * 带超时的锁
         * @param lockKey
         * @param timeout 超时时间   单位:秒
         */
        public static RLock lock(String lockKey, int timeout) {
            RLock lock = redissonClient.getLock(lockKey);
            lock.lock(timeout, TimeUnit.SECONDS);
            return lock;
        }
        
        /**
         * 带超时的锁
         * @param lockKey
         * @param unit 时间单位
         * @param timeout 超时时间
         */
        public static RLock lock(String lockKey, TimeUnit unit ,int timeout) {
            RLock lock = redissonClient.getLock(lockKey);
            lock.lock(timeout, unit);
            return lock;
        }
        
        /**
         * 尝试获取锁
         * @param lockKey
         * @param waitTime 最多等待时间
         * @param leaseTime 上锁后自动释放锁时间
         * @return
         */
        public static boolean tryLock(String lockKey, int waitTime, int leaseTime) {
            RLock lock = redissonClient.getLock(lockKey);
            try {
                return lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                return false;
            }
        }
        
        /**
         * 尝试获取锁
         * @param lockKey
         * @param unit 时间单位
         * @param waitTime 最多等待时间
         * @param leaseTime 上锁后自动释放锁时间
         * @return
         */
        public static boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime) {
            RLock lock = redissonClient.getLock(lockKey);
            try {
                return lock.tryLock(waitTime, leaseTime, unit);
            } catch (InterruptedException e) {
                return false;
            }
        }
    
        /**
         * 初始红包数量
         * @param key
         * @param count
         */
        public void initCount(String key,int count) {
            RMapCache<String, Integer> mapCache = redissonClient.getMapCache("skill");
            mapCache.putIfAbsent(key,count,3,TimeUnit.DAYS);
        }
        /**
         * 递增
         * @param key
         * @param delta 要增加几(大于0)
         * @return
         */
        public int incr(String key, int delta) {
            RMapCache<String, Integer> mapCache = redissonClient.getMapCache("skill");
            if (delta < 0) {
                throw new RuntimeException("递增因子必须大于0");
            }
            return  mapCache.addAndGet(key, 1);//加1并获取计算后的值
        }
    
        /**
         * 递减
         * @param key 键
         * @param delta 要减少几(小于0)
         * @return
         */
        public int decr(String key, int delta) {
            RMapCache<String, Integer> mapCache = redissonClient.getMapCache("skill");
            if (delta < 0) {
                throw new RuntimeException("递减因子必须大于0");
            }
            return mapCache.addAndGet(key, -delta);//加1并获取计算后的值
        }
    }

      2、添加自定义注解

    @Target({ElementType.PARAMETER,ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface ServiceRedisLock {
        String description() default "";
    }

      2、AOP

    @Component
    @Scope
    @Aspect
    @Order(1)
    public class LockRedisAspect {
        @Pointcut("@annotation(com.sugo.seckill.aop.redis.ServiceRedisLock)")
        public void lockAspect(){
        }
        // 增强方法
        @Around("lockAspect()")
        public Object around(ProceedingJoinPoint joinPoint){
            Object obj = null;
            // 加锁
            boolean res = RedissLockUtil.tryLock(Constants.DISTRIBUTED_REDIS_LOCK_KEY,
                    TimeUnit.SECONDS, 3,
                    10);
            // 执行业务
            try {
                if(res){
    
                    obj = joinPoint.proceed();
                }
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            } finally {
                // 释放锁
                if(res){
    
                    RedissLockUtil.unlock(Constants.DISTRIBUTED_REDIS_LOCK_KEY);
                }
            }
            return obj;
        }
    }

      3、业务代码

        业务代码和使用单机锁的业务代码一致,只需要替换锁的注解即可。

      4、总结

        Redis 分布式设置时候的时候,为了防止线程阻塞,设置了锁的等待时候,锁的等待时间一旦设置,意味着一旦网络延迟,加锁时间超时,导致加锁失败;

    五、分布式锁--zk

    (一)zk锁demo

      zk客户端有ZkClient和Curator ,其中ZkClient 是一个开源客户端,在 Zookeeper 原生 API 接口的基础上进行了包装,更便于开发人员使用。内部实现了 Session 超时重连,Watcher 反复注册等功能。像 dubbo 等框架 对其也进行了集成使用。Curator 是 Netflix 公司开源的一套 zk 客户端框架,与 ZkClient 一样,其也封装了 zk 原生 API。其目前已经成为 Apache 的顶级项目。同时,Curator 还提供了一套易用性、可读性更 强的 Fluent 风格的客户端 API 框架。

       这里使用Curator来进行实现分布式锁。

    /**
     * 基于curator的zookeeper分布式锁
     * 这里我们开启5个线程,每个线程获取锁的最大等待时间为5秒,为了模拟具体业务场景,方法中设置4秒等待时间。
     * 开始执行main方法,通过ZooInspector监控/curator/lock下的节点如下图:
     */
    public class CuratorUtil {
        private static String address = "192.168.1.180:2181";
        
        public static void main(String[] args) {
            //1、重试策略:初试时间为1s 重试3次
            RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); 
            //2、通过工厂创建连接
            CuratorFramework client = CuratorFrameworkFactory.newClient(address, retryPolicy);
            //3、开启连接
            client.start();
            //4 分布式锁
            final InterProcessMutex mutex = new InterProcessMutex(client, "/curator/lock"); 
            //读写锁
            //InterProcessReadWriteLock readWriteLock = new InterProcessReadWriteLock(client, "/readwriter");
            
            ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
            
            for (int i = 0; i < 5; i++) {
                fixedThreadPool.submit(new Runnable() {
                    @Override
                    public void run() {
                        boolean flag = false;
                        try {
                            //尝试获取锁,最多等待5秒
                            flag = mutex.acquire(5, TimeUnit.SECONDS);
                            Thread currentThread = Thread.currentThread();
                            if(flag){
                                System.out.println("线程"+currentThread.getId()+"获取锁成功");
                            }else{
                                System.out.println("线程"+currentThread.getId()+"获取锁失败");
                            }
                            //模拟业务逻辑,延时4秒
                            Thread.sleep(4000);
                        } catch (Exception e) {
                            e.printStackTrace();
                        } finally{
                            if(flag){
                                try {
                                    mutex.release();
                                } catch (Exception e) {
                                    e.printStackTrace();
                                }
                            }
                        }
                    }
                });
            }
        }
    }

    (二)zk锁实现业务

      1、自定义注解

    @Target({ElementType.PARAMETER,ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface ServiceZkLock {
    
        String description() default "";
    
    }

      2、AOP

    @Component
    @Scope
    @Aspect
    @Order(1)
    public class LockZkAspect {
        @Pointcut("@annotation(com.sugo.seckill.aop.zk.ServiceZkLock)")
        public void lockAspect(){
        }
        // 增强方法
        @Around("lockAspect()")
        public Object around(ProceedingJoinPoint joinPoint){
            Object obj = null;
            // 加锁
            boolean acquire = ZkLockUtil.acquire(10, TimeUnit.SECONDS);
            // 执行业务
            try {
                if(acquire){
    
                    obj = joinPoint.proceed();
                }
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            } finally {
                // 释放锁
                if(acquire){
    
                    ZkLockUtil.release();
                }
            }
            return obj;
        }
    }

       3、业务实现

        业务实现只需要替换注解即可。

    ------------------------------------------------------------------
    -----------------------------------------------------------
    ---------------------------------------------
    朦胧的夜 留笔~~
  • 相关阅读:
    SVN使用教程总结
    SVN
    js中设置元素class的三种方法小结
    Javascript 删除tr 元素
    SQL Server执行计划的理解
    java多线程知识点汇总(四)多线程知识点脉络图
    hibernate将connection放进threadlocal里实现数据库连接池
    数据库连接池中是将connection放进threadlocal里的
    java jdbc深入理解(connection与threadlocal与数据库连接池和事务实)
    java项目怎样添加jar包依赖?
  • 原文地址:https://www.cnblogs.com/liconglong/p/15470647.html
Copyright © 2020-2023  润新知