前言
随着互联网的发展,人们网上购物已然成为常态,特别是双十一和618等大型的购物节,网站的并发数量也急剧上升,因此我们后台的框架也逐渐从以前的单机版升级到现在的分布式集群。这一切的演变最终目的都是为了提升系统性能,给用户带来更好的购物体验。在曾今的单机环境下多线程的并发抢夺资源的情况我们用Synchronized和ReentrantLock都可以完美的解决,但是由于现在的环境是同一个服务有多个节点,当同一个服务的多个节点同时操作某一个公共资源时我们又该如何去解决这个问题呢?这就是提到过的分布式锁的问题
一、什么是分布式锁
首先分布式锁的这个概念只有在分布式集群环境下才有,单机版的系统中不存在这个概念。在单机版的系统中我们为解决多线程抢夺共享资源的问题引入了Synchronized和ReentrantLock两种单机锁,那么分布式锁其实也是一个道理,是一种专门解决分布式集群环境下多个jvm抢夺共享资源的一种处理手段。下面举个比较恶心的例子来说明单机锁和分布式锁:
前提:假如一层楼只有一个厕所并且只有一个坑位,每次只能去一个人
单机锁:
该层楼只有一个办公室,为了防止多人抢夺坑位,就给办公室的大门上了一把锁,并且锁只有一把钥匙,因此需要上厕所就必须拿到办公室的钥匙才可以。(注意此时上锁的对象是办公室的大门)
分布式锁:
该楼层有多个办公室,如果依旧采用单机锁也就是给每个办公室的都上一把锁,此时会发现还是会出现抢占坑位的情况,因为每个办公室是独立的。因此这种情况我们就需要给厕所门上一把锁,让所有办公室的人去抢夺这个厕所门的锁,而不是给每个办公室的门上一把锁,如此我们才能解决多个办公室场景下的抢夺资源问题。
以上案例中,厕所就是我们需要抢夺的资源,而办公室就是我们程序运行的jvm,办公室中的每个人就是每个jvm中运行的线程。
二、分布式锁应该具备哪些条件
1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2、高可用的获取锁与释放锁;
3、高性能的获取锁与释放锁;
4、具备可重入特性;
5、具备锁失效机制,防止死锁;
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
三、如何实现分布式锁
分布式锁的实现常用方式有如下三种:
-
MySQL数据库实现:基于数据库的乐观锁实现(工作中不推荐使用)
-
Redis数据库实现:基于Redisson实现、基于Lua脚本实现(推荐)
-
Zookeeper数据:基于Zookeeper中不能创建相同的临时节点二实现
四、Redis实现分布式锁(推荐)
1.实现流程
①获取锁
②执行业务逻辑
③释放锁
其实我们分布式锁中主要解决的问题就是上锁和解锁过程的原子性,只要保证了上锁和解锁过程的原子性以及保证锁在异常情况下锁能够得到释放。
2.搭建基础框架环境(springboot+redis+mysql+MybatisPlus)
2.实现方式
用lua脚本实现:
/** 添加分布式锁,用lua脚本方式 */
private String getDataByLua(Long userId) {
// 抢占分布式锁
String value = UUID.randomUUID().toString().replaceAll("-", ""); // 当前锁的指
Boolean lock =
redisTemplate
.opsForValue()
.setIfAbsent(ConstantUtil.REDIS_USER_LOCK_KEY + userId, value, 300, TimeUnit.SECONDS);
String fromDb = "";
if (lock) {
System.out.println("获取分布式锁成功!");
try {
fromDb = getDataFromDb(userId);
} finally {
// 删除分布式锁,传入value,确保删除的是自己的锁
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";
redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class), Arrays.asList("lock"), value);
}
} else {
System.out.println("获取分布式锁失败,等待重试!");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getDataByLua(userId);
}
return fromDb;
}
Redisson实现:
private String getDataByRedison(Long userId) {
RLock lock = redissonClient.getLock(ConstantUtil.REDIS_USER_LOCK_KEY + userId);
// 阻塞式等待,默认锁为30S,业务执行期间看门狗会自动续锁,业务执行完如果不手动删除锁,30S后自动删除
lock.lock();
String fromDb = "";
try {
System.out.println("加锁成功,执行业务"+Thread.currentThread().getId());
fromDb = getDataFromDb(userId);
} finally {
System.out.println("释放锁"+Thread.currentThread().getId());
lock.unlock();
}
return fromDb;
}
五、相关资料
Redis分布式锁官方汉化版文档:http://www.redis.cn/topics/distlock.html
本示例demo链接:www.baidu.com
Redisson源码教程链接:https://github.com/redisson/redisson
六、小结
我自己只研究过用redis来实现,在项目中也看到老大用redis来实现的,不过他是用lua脚本的方式实现的,至于为什么不用redisson来实现我也不得而知,其实两种方法都是一样的,因为redisson的底层其实也是通过lua脚本来实现的。至于mysql和zookeeper的实现方式,大家也可以去学习一下。