如何实现分布式锁 分布式锁的解决方案

一、什么是超卖

  • 举例:某件商品库存数量10件,结果卖出了15件

二、超卖现象一

  1. 系统中库存为1,但是产生了两笔订单

  2. 解决方法

  • 扣减库存不在程序中进行,而是通过数据库
  • 向数据库传递库存增量,扣减1个库存,增量为-1
    先 - 在数据库update语句计算库存,通过update行锁解决并发
 <update id="updateProductCount">
      update product
      set count = count - #{purchaseProductNum,jdbcType=INTEGER},
      update_user = #{updateUser,jdbcType=VARCHAR},
       update_time = #{updateTime,jdbcType=TIME}
      where id = #{id,jdbcType=INTEGER}
    </update>

三、超卖现象二

  1. 系统中库存变为 -1
  2. 产生原因:并发检验库存,造成库存充足的假象,update更新库存,导致库存为负数
  3. 解决方法:校验库存、扣减库存统一加锁,使之成为原子性的操作,并发时只有获得锁的线程才能校验、扣减库存

使用synchronized锁,注意(事务要在锁中)
使用ReentrantLock锁解决(并发包中的锁)

 private Lock lock = new ReentrantLock();

    public Integer createOrder() throws Exception{
        Product product = null;

        lock.lock();
        try {
            TransactionStatus transaction1 = platformTransactionManager.getTransaction(transactionDefinition);
            product = productMapper.selectByPrimaryKey(purchaseProductId);
            if (product==null){
                platformTransactionManager.rollback(transaction1);
                throw new Exception("购买商品:"+purchaseProductId+"不存在");
            }

            //商品当前库存
            Integer currentCount = product.getCount();
            System.out.println(Thread.currentThread().getName()+"库存数:"+currentCount);
            //校验库存
            if (purchaseProductNum > currentCount){
                platformTransactionManager.rollback(transaction1);
                throw
                        new Exception("商品"+purchaseProductId+"仅剩"+currentCount+"件,无法购买");
            }

            productMapper.updateProductCount(purchaseProductNum,"xxx",new Date(),product.getId());
            platformTransactionManager.commit(transaction1);
        }finally {
            lock.unlock();
        }

        TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);
        Order order = new Order();
        order.setOrderAmount(product.getPrice().multiply(new BigDecimal(purchaseProductNum)));
        order.setOrderStatus(1);//待处理
        order.setReceiverName("xxx");
        order.setReceiverMobile("13311112222");
        order.setCreateTime(new Date());
        order.setCreateUser("xxx");
        order.setUpdateTime(new Date());
        order.setUpdateUser("xxx");
        orderMapper.insertSelective(order);

        OrderItem orderItem = new OrderItem();
        orderItem.setOrderId(order.getId());
        orderItem.setProductId(product.getId());
        orderItem.setPurchasePrice(product.getPrice());
        orderItem.setPurchaseNum(purchaseProductNum);
        orderItem.setCreateUser("xxx");
        orderItem.setCreateTime(new Date());
        orderItem.setUpdateTime(new Date());
        orderItem.setUpdateUser("xxx");
        orderItemMapper.insertSelective(orderItem);
        platformTransactionManager.commit(transaction);
        return order.getId();
    }

单体应用锁的局限性不能跨jvm,所以需要使用分布式锁

四、基于数据库悲观锁的分布式锁

基于数据库实现分布式锁(跨进程)
步骤

  1. 多个进程、多个线程访问共同组件数据库
  2. 通过select…for update 访问同一条数据
  3. for update锁定数据,其他线程只能等待

优点:
简单方便、易于理解、易于操作

缺点:
并发量大时,对数据库压力较大

select * from distribute_lock where business_code='demo' for update
@RestController
@Slf4j
public class DemoController {
    @Resource
    private DistributeLockMapper distributeLockMapper;

    @RequestMapping("singleLock")
    @Transactional(rollbackFor = Exception.class)
    public String singleLock() throws Exception {
        log.info("我进入了方法!");
        DistributeLock distributeLock = distributeLockMapper.selectDistributeLock("demo");
        if (distributeLock==null) throw new Exception("分布式锁找不到");
        log.info("我进入了锁!");
        try {
            Thread.sleep(20000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "我已经执行完成!";
    }
}

五、基于Redis的Setnx实现分布式锁

  1. 实现原理:
    1. 获取锁的Redis命令
    2. SET resource_name my_random_value NX PX 30000
      解释:
      resource_name :资源名称,可根据不同的业务区分不同的锁
      my_random_value :随机值,每个线程的随机值都不同,用于释放时的校验
      NX:key不存在时设置成功,key存在则设置不成功
      PX:自动失效时间,出现异常情况,锁可以过期失效
    3. 利用NX的原子性,多个线程并发时,只有一个线程可以设置成功
    4. 设置成功即获得锁,可以执行后续的业务
    5. 如果出现异常,过了锁的有效期,锁自动释放
    6. 释放锁采用Redis的delete命令
    7. 释放锁时校验之前设置的随机数,相同才能释放
    8. 释放锁的LUA脚本

自己的理解:使用Redis的Setnx这个特性,给一个key赋值,key存在择赋值不成功,key不存在赋值成功

LUA脚本

if redis.call("get",KEYS[1])==ARGV[1] then
	return redis.call("del",KEYS[1])
else 
	return 0
end

redis实现分布式锁代码

RedisLock.java

@Slf4j
public class RedisLock implements AutoCloseable {

    private RedisTemplate redisTemplate;
    private String key;
    private String value;
    //单位:秒
    private int expireTime;

    public RedisLock(RedisTemplate redisTemplate,String key,int expireTime){
        this.redisTemplate = redisTemplate;
        this.key = key;
        this.expireTime=expireTime;
        this.value = UUID.randomUUID().toString();
    }

    /**
     * 获取分布式锁
     * @return
     */
    public boolean getLock(){
        RedisCallback<Boolean> redisCallback = connection -> {
            //设置NX
            RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
            //设置过期时间
            Expiration expiration = Expiration.seconds(expireTime);
            //序列化key
            byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
            //序列化value
            byte[] redisValue = redisTemplate.getValueSerializer().serialize(value);
            //执行setnx操作
            Boolean result = connection.set(redisKey, redisValue, expiration, setOption);
            return result;
        };

        //获取分布式锁
        Boolean lock = (Boolean)redisTemplate.execute(redisCallback);
        return lock;
    }

    public boolean unLock() {
        String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
                "    return redis.call(\"del\",KEYS[1])\n" +
                "else\n" +
                "    return 0\n" +
                "end";
        RedisScript<Boolean> redisScript = RedisScript.of(script,Boolean.class);
        List<String> keys = Arrays.asList(key);

        Boolean result = (Boolean)redisTemplate.execute(redisScript, keys, value);
        log.info("释放锁的结果:"+result);
        return result;
    }


    @Override
    public void close() throws Exception {
        unLock();
    }
}

RedisLockController.java

@RestController
@Slf4j
public class RedisLockController {
    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping("redisLock")
    public String redisLock(){
        log.info("我进入了方法!");
        try (RedisLock redisLock = new RedisLock(redisTemplate,"redisKey",30)){
            if (redisLock.getLock()) {
                log.info("我进入了锁!!");
                Thread.sleep(15000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        log.info("方法执行完成");
        return "方法执行完成";
    }
}

六、基于Zookeeper的瞬间节点实现分布式锁

  1. Zookeeper的数据结构
    是一棵树,持久节点:zk会话结束和zk重启这些会话都是不会消失的,瞬时节点:瞬时节点不可再有子节点,会话结束后瞬时节点自动消失
  2. Zookeeper的观察器
    可设置观察器的3个方法:getData();getChildren();exists()
    节点数据发生变化,发送给客户端
    观察器只能控制一次,再监控需重新设置
  3. 实现原理:
    利用Zookeeper的瞬时有序节点的特性
    多线程并发创建瞬时节点时,得到有序的序列
    序号最小的线程获得锁
    其他的线程则监听自己序号的前一个序号
    前一个线程执行完成,删除自己序号的节点
    下一个序号的线程得到通知,继续执行
    创建节点时,已经确定了线程的执行顺序

ZkLock.java

@Slf4j
public class ZkLock implements AutoCloseable, Watcher {

    private ZooKeeper zooKeeper;
    private String znode;

    public ZkLock() throws IOException {
        this.zooKeeper = new ZooKeeper("localhost:2181",
                10000,this);
    }

    public boolean getLock(String businessCode) {
        try {
            //创建业务 根节点
            Stat stat = zooKeeper.exists("/" + businessCode, false);
            if (stat==null){
                zooKeeper.create("/" + businessCode,businessCode.getBytes(),
                        ZooDefs.Ids.OPEN_ACL_UNSAFE,
                        CreateMode.PERSISTENT);
            }

            //创建瞬时有序节点  /order/order_00000001
            znode = zooKeeper.create("/" + businessCode + "/" + businessCode + "_", businessCode.getBytes(),
                    ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.EPHEMERAL_SEQUENTIAL);

            //获取业务节点下 所有的子节点
            List<String> childrenNodes = zooKeeper.getChildren("/" + businessCode, false);
            //子节点排序
            Collections.sort(childrenNodes);
            //获取序号最小的(第一个)子节点
            String firstNode = childrenNodes.get(0);
            //如果创建的节点是第一个子节点,则获得锁
            if (znode.endsWith(firstNode)){
                return true;
            }
            //不是第一个子节点,则监听前一个节点
            String lastNode = firstNode;
            for (String node:childrenNodes){
                if (znode.endsWith(node)){
                    zooKeeper.exists("/"+businessCode+"/"+lastNode,true);
                    break;
                }else {
                    lastNode = node;
                }
            }
            synchronized (this){
                wait();
            }

            return true;

        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }





    @Override
    public void close() throws Exception {
        zooKeeper.delete(znode,-1);
        zooKeeper.close();
        log.info("我已经释放了锁!");
    }

    @Override
    public void process(WatchedEvent event) {
        if (event.getType() == Event.EventType.NodeDeleted){
            synchronized (this){
                notify();
            }
        }
    }
}

七、基于分布式Zookeeper的Curator客户端实现分布式锁

  1. curator已经实现了分布式锁的方法
  2. 直接调用即可
    @Test
    public void testCuratorLock(){
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework client = CuratorFrameworkFactory.newClient("localhost:2181", retryPolicy);
        client.start();
        InterProcessMutex lock = new InterProcessMutex(client, "/order");
        try {
            if ( lock.acquire(30, TimeUnit.SECONDS) ) {
                try  {
                    log.info("我获得了锁!!!");
                }
                finally  {
                    lock.release();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        client.close();
    }
@RestController
@Slf4j
public class ZookeeperController {
    @Autowired
    private CuratorFramework client;

    @RequestMapping("zkLock")
    public String zookeeperLock(){
        log.info("我进入了方法!");
        try (ZkLock zkLock = new ZkLock()) {
            if (zkLock.getLock("order")){
                log.info("我获得了锁");
                Thread.sleep(10000);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        log.info("方法执行完成!");
        return "方法执行完成!";
    }

    @RequestMapping("curatorLock")
    public String curatorLock(){
        log.info("我进入了方法!");
        InterProcessMutex lock = new InterProcessMutex(client, "/order");
        try{
            if (lock.acquire(30, TimeUnit.SECONDS)){
                log.info("我获得了锁!!");
                Thread.sleep(10000);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                log.info("我释放了锁!!");
                lock.release();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        log.info("方法执行完成!");
        return "方法执行完成!";
    }
}

八、基于Redisson实现分布式锁

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值