Java中的锁(JVM本地锁、分布式锁)

Java中的锁(单机锁、分布式锁)

JVM本地锁:

概念:

单机锁只存在于单体项目中,会存在并发问题即同一时刻多个请求进来导致对数据库表数据的重复的CUD操作,不用考虑多线程问题即集群问题);

举例:账户默认有500块,一个线程循环5次存100,一个线程循环5次取100,最后结果应该是500不变。如果不处理的话再高并发情况下会出现结果超过500或者负数的情况(出现了幂等性/数据泄露问题)

解决方案:
  • 在Service层的实现类的方法前面加入JVM的synchronized关键字,即可解决单体项目中的并发问题
    /**
     * 并发问题使用:本地锁
     */
    @Override
    // 方法前面加synchronized关键字
    public synchronized R registerUser(UserRegisterVo userRegisterVo) {
        String tel = userRegisterVo.getTel();
        int i = userMapper.existTel(Long.valueOf(tel));
        if (i == 0) {
            User user = new User();
            user.setTel(userRegisterVo.getTel());
            user.setName(userRegisterVo.getName());
            user.setGender(userRegisterVo.getGender());
            userMapper.insert(user);
        } else {
            log.info("------------->注册失败");
            return R.error("注册失败,该手机号已注册!");
        }
        return R.ok("注册成功");
    }
  • 在Service层的实现类的方法中对会造成重复CUD的代码放入synchronized(){}方法中,必须在synchronized(){}方法添加唯一参数:
// 参数为:线程的任务实例(线程类的实例是唯一的时候资源才会遵守幂等性问题==单体项目)
synchronized(线程的任务实例){
	// 需要保存的资源的代码(为对sql的cud方法)
}
// 参数为:不相关对象(在当前类中new一个Object对象即可)
synchronized(不相关对象){
	// 需要保存的资源(为对sql的cud方法)
}
// 参数为:线程类.class
synchronized(线程类.class){
	// 需要保存的资源(为对sql的cud方法)
}
// 参数为:静态常量
synchronized(静态常量){
	// 需要保存的资源(为对sql的cud方法)
}
  • 单体项目中(单实例情况下):synchronized关键字和synchronized()可以互换;

分布式锁:

概念:

解决项目多实例情况下的多线程高并发导致的(幂等性、数据泄露)问题,
注意(单实例不存在多线程高并发问题,只存在单线程高并发问题);

解决方案:
  • 数据库的悲观锁(在sql语句中添加for update可以后对数据进行加锁,这个时候其他线程无法进行CURD操作
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.xxb.user.mapper.UserMapper">
    <select id="selectUserLock" resultType="com.xxb.user.pojo.User">
        select *
        from kss_user
        where id = #{userId} for update
    </select>
</mapper>
  • 数据库的乐观锁(表字段添加一个版本字段,在sql语句对版本字段进行判断,不会进行加锁:如果版本字段变化了,说明其他线程在此时进行了操作,那么当前这个线程就更新失败了
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.xxb.user.mapper.UserMapper">
    <update id="updateUserLock">
        update kss_user
        set amount  = amount - #{money},
            version = version + 1
        where id = #{userId}
          and amount > 0
          and (amount - #{money}) > 0
          and version = #{version}
    </update>
</mapper>
  • Redis不可重入锁

归功于Redis底层是单线程执行命令,而Java多多线程实例的: (Redis拿到Java实例并上锁 set key value EX 20 NX不存在则创建返回TRUE无并发,存在返回FALSE有并发,20表示超过20秒内会自动关闭Java实例锁,注意:Redis分布式锁在关闭锁的时候需要判断key来关闭锁,防止一些方法执行超过20秒后关闭锁时关闭了其他线程的锁,这里有关闭锁的两种情况:超过20秒后Redis自动关闭/finally代码关闭锁)

   /**
     * 并发问题使用:Redis非重入锁(拿不到锁就离开【没有排队机制】)
     *
     * @param userRegisterVo
     * @return
     */
    @Override
    public R registerUserForRedisLock(UserRegisterVo userRegisterVo) {
        String tel = userRegisterVo.getTel();
        String key = "user:amount:Key:" + tel;// 根据用户id生成的key,会放入redis中
        String value = System.nanoTime() + "_" + UUID.randomUUID();// 生成value,会放入redis中

        // 关键代码:拿到一个Java实例
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        // 关键代码:(被动释放锁)给实例进行上锁,20秒钟内所有实例的请求和操作,只能执行成功一次(等价于 set key value EX 20 NX)
        Boolean isLock = opsForValue.setIfAbsent(key, value, 20, TimeUnit.SECONDS);
        log.info(".....................isLock ={}", isLock);
        try {
            // 不存在(TRUE)没有并发问题则拿到锁修改共享资源,存在(FALSE)有并发问题则离开
            if (isLock) {
                int i = userMapper.existTel(Long.valueOf(tel));
                if (i == 0) {
                    User user = new User();
                    user.setTel(userRegisterVo.getTel());
                    user.setName(userRegisterVo.getName());
                    user.setGender(userRegisterVo.getGender());
                    userMapper.insert(user);
                } else {
                    log.info("------------->注册失败");
                    return R.error("注册失败,该手机号已注册!");
                }
            } else {
           		// 不重入锁这里会进
                log.info("没有拿到锁离开了!");
                System.out.println("客官请慢一点!");
            }
            return R.ok("注册成功");
        } catch (Exception ex) {
            return R.error("执行失败!请查看日志!");
        } finally {
            // 关键代码:(主动释放自己key对应的锁)无论该实例执行正确还是失败,都需要在redis加锁成功后释放锁
            if (value.equalsIgnoreCase(opsForValue.get(key).toString())) { // 防止删除别人的锁
                log.info("=================opsForValue.get(key) = {}", opsForValue.get(key));
                stringRedisTemplate.delete(key);
            }
        }
    }
  • Redis + Redisson的不可重入锁/可重入锁(在Redis拿到Java实例的基础上解决了可重入的问题以及自己不可重入的方案,且可以指定上锁的key所以在锁回收的时候就不用对key进行判断了
   /**
     * 并发问题使用:Redis + Redisson(可重入锁)
     *
     * @param userRegisterVo
     * @return
     */
    @Override
    public R registerUserForRedissonReentrant(UserRegisterVo userRegisterVo) {
        String tel = userRegisterVo.getTel();
        String key = "user:amount:Key:" + tel;// 根据用户id生成的key,会放入redis中

        // 关键代码:Redisson客户端申请锁(用户级别的锁),同时也拿到了Java实例
        RLock redissonLock = redissonClient.getLock(key);
        try {
            // 关键代码:设置锁逻辑(获取到锁只有20秒时间执行,到了自动释放锁,防止死锁【001、002、003直到轮到你】)
            redissonLock.lock(20L, TimeUnit.SECONDS);

            int i = userMapper.existTel(Long.valueOf(tel));
            if (i == 0) {
                User user = new User();
                user.setTel(userRegisterVo.getTel());
                user.setName(userRegisterVo.getName());
                user.setGender(userRegisterVo.getGender());
                userMapper.insert(user);
            } else {
                log.info("------------->注册失败");
                return R.error("注册失败,该手机号已注册!");
            }
            return R.ok("注册成功");
        } catch (Exception ex) {
            return R.error("执行失败......");
        } finally {
            // 关键代码:无论如何都需要释放锁
            redissonLock.unlock();
        }
    }
  • ZooKeeper的可重入锁

原理:

  • 并发的线程进来(线程A、B、C、N)会在Zookeeper的临时顺序节点(znode)创建对应的顺序的编号(0001,0002,0003,000N),
  • 从编号最小依次分给对应的(线程A、B、C、N),线程拿到编号后执行程序访问数据库共享资源,执行后删除编号,
  • Zookeeper通过watch看门狗机制监控删除编号行为:行为执行后,将临时顺序节点(znode)下创建对应的顺序的编号分给下一个线程B、C、N;
    在这里插入图片描述
    /**
     * 并发问题使用:Zookeeper(可重入锁)
     *
     * @param userRegisterVo
     * @return
     */
    @Override
    public R registerUserForZooKeeperLock(UserRegisterVo userRegisterVo) {
        String tel = userRegisterVo.getTel();

        /**
         * ZooKeeper原理逻辑:
         * A实例---并发的线程1 2 3 X---/useramount/zklock/a_lock 001 002 003 00X
         * B实例---并发的线程1 2 3 X---/useramount/zklock/b_lock 001 002 003 00X
         */
        // 关键代码:设置锁的力度(用户级别的锁,用户可以各自执行),临时顺序节点的路径和名称
        String lockPath = "/tel/zklock/" + tel + "_lock";
        // 关键代码:生成临时顺序节点znode,并生成对应的顺序编码
        InterProcessMutex lock = new InterProcessMutex(client, lockPath);
        try {
            // 关键代码:编码给到线程,获取锁判断(20秒判断是否获取到了锁,没获取到锁的话继续等待【001、002、003直到轮到你】)
            boolean isLock = lock.acquire(20L, TimeUnit.SECONDS);
            if (isLock) {
                int i = userMapper.existTel(Long.valueOf(tel));
                if (i == 0) {
                    User user = new User();
                    user.setTel(userRegisterVo.getTel());
                    user.setName(userRegisterVo.getName());
                    user.setGender(userRegisterVo.getGender());
                    userMapper.insert(user);
                    return R.ok("注册成功");
                } else {
                    log.info("------------->注册失败");
                    return R.error("注册失败,该手机号已注册!");
                }
            } else {
                // 注意:这里不进,表示这个ZooKeeper锁是(可重入的)
                log.info("没有拿到锁,离开了...");
            }

        } catch (Exception ex) {
            return R.error("执行失败......");
        } finally {
            try {
                // 关键代码:无论如何都需要释放锁
                lock.release();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        return R.ok("执行完成......");
    }
  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值