分布式锁(解决超买超卖问题)

(一)、使用伪代码解决单体应用中超买超卖的问题

问题的引出:有三张表,分别为商品表、库存表、订单表。
首先使用Java代码去处理用户下订单

public class Shopping {

	@Transactional(rollbackFor = "Exception.class")
	public void 购买(商品ID,购买数量){
	
		//首先查看商品库存
		int 库存数量 = select(商品ID);
	
		if(购买数量 > 库存数量) {
			抛出异常,事务回滚
		}
		库存数量 = 库存数量 - 购买数量
		insert(商品ID,新的库存数量)
	}

	创建5个线程同时去执行购买()方法
}

那么这段程序产生了什么问题呢?
查看数据库,发现本来商品库存数量为1,现在库存数量为0了,但是却产生了5笔订单,一个商品怎么能被5个人同时买到呢?显然是有问题的。

因为上面的程序走的是Java代码,因此下面使用数据库的行锁去解决

public class Shopping {

	@Transactional(rollbackFor = "Exception.class")

	//首先查看商品库存
	int 库存数量 = select(商品ID);
	
	public void 购买(商品ID,购买数量){
		if(购买数量 > 库存数量) {
			抛出异常,事务回滚
		}
		//在数据库中执行扣减库存操作,上面是在Java中计算库存后更新到数据库(注意!!!)
		UPDATE 库存 SET 库存数 = 库存数 - 购买数量 WHERE 商品ID = 商品ID
	}

	创建5个线程同时去执行购买()方法
}

此时又产生了什么问题呢?
发现数据库中库存数为-4,产生了5笔订单。问题的原因出在哪里呢?因为5个线程并发执行,所以同时查到了库存数为1,然后排队在数据库中 - 1。

怎么解决?
使用锁(synchronized或者ReentrantLock)解决。让查询库存和扣减库存都被锁住,同时只有一个线程能查询库存和扣减库存,其他线程全部等待,那么就能解决问题(最终的结果是,第一个线程拿到锁后,查询库存为1,然后 - 1 后,库存变0,然后提交事务后释放锁,其他线程拿到锁,查询库存为0,回滚,回滚,回滚…最终只有第一个线程成功了,其他全失败)

当然上面是单体应用,因为都在一个JVM中,所以可以使用锁去解决,但是如果多个应用同时呢?加锁有用吗?肯定没用了,因此就需要分布式锁来解决问题了。

MySQL的行锁默认是自动提交的,虽然在执行MySQL语句时需要排队,但是在分布式项目中,因为Java的锁不管用了,所以这里需要关闭MySQL行锁的自动提交
在这里插入图片描述

使用 SET @@AUTOCOMMIT = 0; 就行了

把自动提交关闭后,打开两个MySQL窗口,对同一行数据,A窗口SELECT * FROM xxx FOR UPDATE,B窗口UPDATE xxx SET xxx = xxx就会被阻塞,等到A窗口COMMIT后,B窗口才能成功。使用行锁确实可以解决分布式锁问题,但是因为对于MySQL的压力就变大了。因此不建议使用。

(二)分布式锁

基于Redis的分布式锁

SET resource_name my_random_value NX PX 30000

resource_name:资源名称,可根据不同的业务区分不同的锁
my_random_value:随机值,每个线程的随机值都不同,用于释放锁时的校验
NX:key不存在时设置成功,key存在则设置不成功
PX:自动失效时间,出现异常情况,锁可以过期失效

利用NX的原子性,多个线程并发时,只有一个线程可以设置成功,设置成功即获得锁,可以执行后续的业务处理,如果出现异常,过了锁的有效期,锁自动释放。
释放锁使用Redis的delete命令,释放锁之前校验之前的随机数,如果相同,才能释放
分布式锁(解决超买超卖问题)_第1张图片
Spring Redis分布式锁实现
首先编写业务代码,这里使用一个函数式接口

@FunctionalInterface
public interface Bussiness {
    void handle();
}
@Slf4j
public class RedisLockUtil {
    public static void getLock(RedisTemplate redisTemplate, Expiration expiration, String key, Bussiness bussiness) {
        log.info("进 入 方 法...");

        String value = UUID.randomUUID().toString();

        RedisCallback<Boolean> redisCallback = connection -> {
            RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.SET_IF_ABSENT;
            final byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
            final byte[] redisValue = redisTemplate.getValueSerializer().serialize(value);
            Boolean result = (Boolean) connection.set(redisKey, redisValue, expiration, setOption);
            return result;
        };

        Boolean lock = (Boolean) redisTemplate.execute(redisCallback);

        if (lock) {
            log.info("拿 到 了 锁...");
            try {
            	//自己要执行的业务代码
                bussiness.handle();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                String delete = "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> script = RedisScript.of(delete, Boolean.class);
                Boolean r = (Boolean) redisTemplate.execute(script, Arrays.asList(key), value);
                log.info("释放锁的结果是:{}", r);
            }
        }
    }
}

使用Zookeeper实现分布式锁
分布式锁(解决超买超卖问题)_第2张图片
分布式锁(解决超买超卖问题)_第3张图片
分布式锁(解决超买超卖问题)_第4张图片
分布式锁(解决超买超卖问题)_第5张图片
分布式锁(解决超买超卖问题)_第6张图片

@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 bussinessCode) throws InterruptedException, KeeperException {
        log.info("获 得 锁 成 功...");

        //创建业务根节点
        final Stat stat = zooKeeper.exists("/" + bussinessCode, false);
        if (null == stat) {
            zooKeeper.create("/" + bussinessCode, bussinessCode.getBytes(StandardCharsets.UTF_8),
                    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }

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

        final List<String> children = zooKeeper.getChildren("/" + bussinessCode, false);
        Collections.sort(children);

        String firstNode = children.get(0);

        //如果创建的第一个节点是字节点,则获得锁
        if (znode.endsWith(firstNode)) {
            return true;
        }

        //如果不是第一个字节点则监听前一个节点
        String lastNode = firstNode;
        for (String node : children) {
            if (znode.endsWith(node)) {
                zooKeeper.exists("/" + bussinessCode + "/" + bussinessCode, true);
                break;
            } else {
                lastNode = node;
            }
        }

        synchronized (this) {
            wait();
        }

        return true;
    }

    @Override
    public void close() throws Exception {
        zooKeeper.delete(znode, -1);
        zooKeeper.close();
        log.info("释 放 锁 成 功...");
    }

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

接下来有一种更简单的实现分布式锁的方法
首先引入curator的jar包

<dependency>
	<groupId>org.apache.curatorgroupId>
	<artifactId>curator-recipesartifactId>
	<version>4.2.0version>
dependency>

然后去看官方文档
分布式锁(解决超买超卖问题)_第7张图片
使用redisson-spring-boot-starter实现分布式锁,其实它帮你实现了,只需要调用即可,十分简单
首先引入需要的jar包

<dependency>
    <groupId>org.redissongroupId>
    <artifactId>redisson-spring-boot-starterartifactId>
	<version>3.15.3version>
dependency>

配置application.yaml文件

spring:
  redis:
    host: localhost
    port: 6379
    password: rhw19990625

开始编写代码

@Slf4j
@RestController
public class RedissonLock {

    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/redissonLock")
    public String lock() {
        final RLock lock = redissonClient.getLock("key");

        //进入锁
        lock.lock();
        log.info("已经进入锁");

        System.out.println("这里是业务代码");

        //释放锁
        lock.unlock();
        log.info("已经释放锁");

        return "Method has done";
    }
}

你可能感兴趣的:(分布式锁)