(一)、使用伪代码解决单体应用中超买超卖的问题
问题的引出:有三张表,分别为商品表、库存表、订单表。
首先使用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命令,释放锁之前校验之前的随机数,如果相同,才能释放
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);
}
}
}
}
@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>
然后去看官方文档
使用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";
}
}