前言
随着分布式、微服务应用的流行,同时也伴随着一些新问题的产生以及解决这些问题的技术方案,如服务注册与发现、负载均衡、网关、分布式锁、分布式事务等等。本节我们主要讲一下分布式锁的应用。实战案例依然使用我们多次提到的ATP应用测试平台实现。关于分布式锁的章节,我们会分为三小节来讲解,分别是基于MYSQL数据库方式实现分布式锁、基于zookeeper实现分布式锁、基于缓存redis实现分布式锁这三大分布式技术解决方案。并为每一小节提供一个实战案例,本小节我们主要使用mysql的乐观锁实现分布式锁方案。
正文
在java单体应用中,对于一个需要加锁的业务处理,我们使用java提供的Synchronized关键字或者一些显示锁API,如ReentrantLock等就可以轻松实现。但是这种方案在我们的分布式或者微服务这种非单体应用中就无法处理了,因为我们都知道,对于java应用,我们单个应用都需要单独的JVM虚拟机实例运行,各个虚拟机实例都是隔离的,数据并不能共享访问。正是由于虚拟机实例的隔离,导致我们的一个应用的多个实例间数据并不能共享,所以基于JVM虚拟机实例的锁在这样的分布式、微服务项目中就锁失效了,导致我们的数据可能出现多线程并发访问,导致重复写入,查询数据缓存击穿等等的问题。这里我们就引入了我们第一个技术解决方案,使用mysql数据库的乐观锁解决。从本质上来讲我们使用分布式应用能够通过共享的数据实现分布锁的实现。
分布式锁要具备以下的条件:
-
在分布式应用中,同一个方法在同一个时间只能被一个机器中的某一个线程执行。
-
能够高可用、高性能的获取锁。
-
锁具备可重入性。
-
锁具有失效机制,防止发生死锁。
-
锁具备非阻塞特性,在没有获取到锁时直接返回获取锁失败的内容。
mysql实现分布式锁案例:这里我们以一个查询全部用户信息接口案例演示,并将我们前面缓存的部分结合到该案例中,这里假设我们该接口是一个高并发的访问。
- 创建表用于存储分布式锁数据
说明:这里我们通过方法名method_name字段唯一标识存储各个业务的分布式锁的名称,这里是使用mysql的唯一性索引保证数据的唯一性,当然我们也可以通过mysql的乐观锁机制实现,使用一个版本字段version实现,每次获取锁都通过比对版本号来去获取锁,版本号一致得到锁,不一致则获取锁失败。这里使用mysql的唯一性索引,我们是通过插入数据来获取锁,如果能够插入数据成功,就说明获取到了锁,否则获取锁失败。create_time字段只是标识一下我们什么时间创建锁,没有具体的业务。expire_time字段是用来解决锁的过期问题的,例如我们的程序由于系统宕机,导致我们的锁还没来的及释放,这样锁一直存储到数据库中,即使服务恢复了正常,导致其它线程也一直获取不到锁,我们需要通过一些手段来人为的释放锁,例如通过定时任务定时来清理锁的任务数据或者在我们执行获取锁之前先清理一下过期的锁任务数据。
CREATE TABLE `sys_distribute_lock` ( `id` varchar(50) COLLATE utf8mb4_general_ci NOT NULL COMMENT '主键', `method_name` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '方法名称', `create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间', `expire_time` timestamp NULL DEFAULT NULL COMMENT '过期时间', PRIMARY KEY (`id`), UNIQUE KEY `index_method_name` (`method_name`) USING BTREE COMMENT '执行方法唯一索引' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='分布式锁';
- 使用ATP应用测试平台中的代码生成器生成我们的sys_distribute_lock表的模板代码
- 在我们的UserServiceImpl实现类中添加一个获取全部用户信息的业务方法
package com.yundi.atp.platform.module.sys.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.yundi.atp.platform.module.sys.entity.User; import com.yundi.atp.platform.module.sys.mapper.UserMapper; import com.yundi.atp.platform.module.sys.service.UserService; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import java.util.List; /** * <p> * 用户管理 服务实现类 * </p> * * @author yanp * @since 2021-03-12 */ @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Cacheable(cacheNames = "user", key = "#id") @Override public User findUserInfoById(String id) { return this.getById(id); } @Override public void saveUser(User user) { this.save(user); } @Override public void updateUserById(User user) { this.updateById(user); } @CacheEvict(cacheNames = "user", key = "#id") @Override public void removeUserById(String id) { this.removeById(id); } @Cacheable(cacheNames = "user", key = "#root.method") @Override public List<User> findAllUserInfo() { try { //模拟业务需要执行5秒钟 Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } return this.list(); } }
说明: 该方法我们配置了redis的缓存,如果没有缓存将会查询数据库,如果存在缓存则直接从缓存中获取数据。同时为了方便演示我们的分布式锁,我们选择该业务睡眠5秒钟,模仿我们应用的处理过程。
- 实现一个分布式锁的http接口
package com.yundi.atp.platform.module.sys.controller; import com.yundi.atp.platform.common.Result; import com.yundi.atp.platform.module.sys.entity.User; import com.yundi.atp.platform.module.sys.service.DistributeLockService; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; /** * <p> * 分布式锁 前端控制器 * </p> * * @author yanp * @since 2021-04-29 */ @RestController @RequestMapping("/sys/distributeLock") public class DistributeLockController { @Autowired private DistributeLockService distributeLockService; /** * 分布式锁获取查询数据:方式一(mysql) * @return */ @ApiOperation(value = "通过mysql方式获取分布式锁案例") @GetMapping(value = "/findAllUserInfoByMysqlLock") public Result findAllUserInfoByMysqlLock() { List<User> userList = distributeLockService.findAllUserInfoByMysqlLock(); //1.没有获取到锁,直接返回提示信息 if (userList == null) { return Result.fail("正在全力为您加载中,请稍后重试!"); } return Result.success(userList); } }
- 实现一个分布式锁的具体业务逻辑
package com.yundi.atp.platform.module.sys.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.yundi.atp.platform.module.sys.entity.DistributeLock; import com.yundi.atp.platform.module.sys.entity.User; import com.yundi.atp.platform.module.sys.mapper.DistributeLockMapper; import com.yundi.atp.platform.module.sys.service.DistributeLockService; import com.yundi.atp.platform.module.sys.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.List; /** * <p> * 分布式锁 服务实现类 * </p> * * @author yanp * @since 2021-04-29 */ @Service public class DistributeLockServiceImpl extends ServiceImpl<DistributeLockMapper, DistributeLock> implements DistributeLockService { @Autowired private UserService userService; @Override public List<User> findAllUserInfoByMysqlLock() { //1.删除过期的锁 this.remove(new QueryWrapper<DistributeLock>().eq("method_name", "findAllUserInfoByMysqlLock").le("expire_time", LocalDateTime.now())); //2.申请锁 DistributeLock distributeLock = new DistributeLock(); distributeLock.setMethodName("findAllUserInfoByMysqlLock"); distributeLock.setCreateTime(LocalDateTime.now()); distributeLock.setExpireTime(LocalDateTime.now().plusMinutes(1)); try { this.save(distributeLock); } catch (Exception e) { log.error(Thread.currentThread().getName() + ":分布式锁已被占用,请稍后重试!"); return null; } //3.执行具体的业务 List<User> userList = userService.findAllUserInfo(); //4.释放锁 this.removeById(distributeLock.getId()); //5.返回结果 return userList; } }
说明:整个过程分为以下几个步骤,①删除过期的锁,避免死锁②获取锁③如果没有获取到锁,直接返回null④如果获取到锁执行具体的业务逻辑⑤释放锁⑥返回结果
- 验证
说明:我们通过maven打包,通过变更端口号,启动俩个ATP应用测试服务,同时访问俩台服务器的接口,看锁是否能够生效。并且验证缓存服务是否生效。
9000和9001端口的俩台服务器已经启动完成:
同时访问我们的测试接口,可以看到9000端口的接口获取到了锁,9001端口没有获取到锁,直接返回了没有获取到锁的处理结果。
同时我们再次访问,发现都可以获取到数据了,只有加锁过程的sql语句执行了,说明我们的缓存已经生效了。不会再执行查询数据库业务的方法。因为执行时间很快,我们加锁的情况就没法通过手动演示了,我们也可以将业务方法中加入一个线程睡眠时间就可以看到这个过程了。
- mysql数据库锁存在的问题及优化
①数据库的可用性和性能将直接影响分布式锁的可用性及性能。为了保证数据库分布式锁的高可用,数据库需要做双机部署、数据同步、主备切换。
②数据库分布式锁目前不具备可重入的特性,因为同一个线程在释放锁之前,行数据一直存在,无法再次成功插入数据。所以,需要在表中新增一列,用于记录当前获取到锁的机器和线程信息,再次获取锁的时候,先查询表中机器和线程信息是否和当前机器线程相同,若相同则直接获取锁。保证锁的可重入性提高性能。
③锁失效机制通过过期时间控制,每次插入锁之前先删除过期的锁,可以根据锁的使用情况合理的设定锁的过期时间。
④不具备阻塞锁特性,获取不到锁就直接返回失败,所以需要优化获取逻辑,循环多次去获取锁,通过线程睡眠加循环的方式实现,多次获取不到再返回获取锁失败,增加用户体验效果。
⑤因为数据库主要是做数据持久化存储服务的,其它数据库业务可能会影响到分布式锁的性能,所以需要考虑使用数据库分布式锁的性能问题。
结语
ok,到这里我们基于MYSQL数据库方式实现分布式锁的实战应用就结束了,我们下期见哦。