分布式锁的应用篇章
什么是分布式锁
- 为了防止分布式系统中的多个进程之间在操作某一临界区时相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。而这个分布式协调技术的核心就是来实现这个分布式锁。
为什么使用
- 我们都知道,在单点系统中,如果使用多线程编程,那么,我们对于一些共享资源,如数据库、缓存,我们可以通过加锁的方式来保证数据的准确性,假如在购买某一个商品,如果不在数据库层面上设置事务隔离级别,那么,我们会通过加锁的方式来保证数据数量的增减的正确。
- 在分布式系统中,并发控制的范围由原本的线程,拓展到通过网络拓扑结构的多节点并发控制,是一种多进程多线程模型,因此,引入了分布式锁的概念。
基础特性
- 有限等待
- 尝试在有限的时间内申请获取锁
- 让权等待
- 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败
- 空闲让进
- 锁在不被占用的情况下,必须有足够的性能、可用性,给予申请方占用锁
- 互斥
- 在分布式系统环境下,一个临界区在同一时间只能被一个机器的一个线程执行
同时,该锁最好还是可重入的。
分布式的应用场景
上面讲了一些基本概念,这里是本章的核心,毕竟是应用篇。
场景一 消费
该分布式集群有三个节点,同时有多个客户端选择了该系统的某一个物件进行购买,这些请求被调度到了三个节点上,因此,我们无法像单进程内使用锁来解决这个问题,同时,由于系统的数据库不仅提供给我们这个系统使用,还提供给内部一些系统使用,我们更不能够去设置数据库的事务级别。这个时候就需要分布式锁,流程大概如下:
- 多个请求来到三个节点,调用到节点内的同一个方法
- 首先请求分布式锁
- 成功获得,执行业务,最后释放锁。
- 获取失败,有限等待
- 成功获取,执行业务,释放锁
- 超时等待,可重试或者对失败任务进行调度。
分布式锁的Sample Guidence
接下来,我对上述的购物场景进行足够仔细的描述实现。
数据库
CREATE TABLE `goods` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`numbers` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `goods` VALUES ('1', '100');
DAO层
@Mapper
public interface GoodsDao {
@Update("UPDATE goods SET numbers=#{newAmount} WHERE id=#{id} ")
void updateAmount(@Param("newAmount") int newAmount, @Param("id") int id);
@Select("SELECT * FROM goods WHERE id=#{id}")
Goods findById(@Param("id") int id);
}
逻辑层—使用数据库事务
@Autowired
private GoodsDao goodsMapper;
@Transactional(rollbackFor = Exception.class,isolation = Isolation.SERIALIZABLE)
@Override
public void sellGood(int id) {
Goods goods = goodsMapper.findById(id);
log.warn("cur numbers is :" + goods.getNumbers());
goodsMapper.updateAmount(goods.getNumbers() - 1, goods.getId());
}
测试
- 使用100线程购买商品,每次购买一件,执行一次
private CyclicBarrier cyclicBarrier = new CyclicBarrier(100);
@Test
void doSellTest() {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
goodService.sellGood(1);
}).start();
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 结果
Exception in thread "Thread-4" org.springframework.dao.DeadlockLoserDataAccessException:
### Error updating database. Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
### The error may exist in com/qgailab/dlock/dao/GoodsDao.java (best guess)
### The error may involve com.qgailab.dlock.dao.GoodsDao.updateAmount-Inline
### The error occurred while setting parameters
### SQL: UPDATE goods SET numbers=? WHERE id=?
### Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction...
- 分析
我们使用了事务,而且事务的隔离级别是最高的,意味着事务是串行执行的,但是执行结果为何会出现死锁?
如果你阅读过spring与mybatis的源码实现,你能够发现,其实,一个事务的执行步骤如下:
- Connection.setAutoCommit(false)
- do…
- if(rollback)
- commit
它是针对数据库连接进行设置隔离级别的,因此,我再去查看我的数据库连接池配置,连接数为5,没错,使用@Transactional你仍然达不到事务的真正隔离,因为,你使用连接池!
错误结果总结
- 连接池使得@Transactional哪怕是串行化隔离级别也无法实现数据的正确递减
- 要么把连接数改为1(使得数据库连接池失去意义)
- 要么加锁,但是上面我们也说过,加锁只能解决单机级别的并发问题。
- 补充一点,也给读者强调一点,@Transactional注解的作用是将一系列的DB操作聚合成一个原子级别操作,通过抛异常的形式来回滚事务(手动回滚),它并不是锁的作用,请大家要理解好。
Sample
这里使用真正的分布式锁使用,基于高性能的redission。
引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.10.6</version>
</dependency>
创建注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
/**
*
* @return 锁方法的参数下标
*/
int lockIdx() default -1;
/**
*
* @return 多久自动释放锁
*/
long releaseTime() default 5000L;
}
创建切面
@Aspect
@Order(1)
@Component
@Slf4j
public class DistributedLockAspect {
@Autowired
private Redisson redisson;
@Around("@annotation(DistributedLock)")
public Object around(ProceedingJoinPoint joinPoint, DistributedLock DistributedLock) throws Throwable {
Object obj = null;
Object[] args = joinPoint.getArgs();
int lockIdx = DistributedLock.lockIdx();
//取得方法名
String key = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint
.getSignature().getName();
//if lockIdx=-1,则是锁整个方法
if (lockIdx != -1) {
key += args[lockIdx];
}
long releaseTime = DistributedLock.releaseTime();
long waitTime = 4000;
RLock rLock = redisson.getLock(key);
if (rLock.tryLock(waitTime, releaseTime, TimeUnit.SECONDS)) {
log.info("get lock.");
obj = joinPoint.proceed();
rLock.unlock();
log.info("release lock");
} else {
log.info("----------no----------");
throw new RuntimeException("没有获得锁");
}
return obj;
}
}
redission配置
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Bean(name = {"redisTemplate", "stringRedisTemplate"})
public StringRedisTemplate stringRedisTemplate( RedisConnectionFactory factory) {
StringRedisTemplate redisTemplate = new StringRedisTemplate();
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://" + host + ":6378");
config.useSingleServer().setConnectionMinimumIdleSize(12);
config.useSingleServer().setPassword("****");
return (Redisson) Redisson.create(config);
}
}
修改逻辑层
@DistributedLock(lockIdx = 0, releaseTime = 10000L)
@Override
public void sellGood(int id) {
Goods goods = goodsMapper.findById(id);
log.warn("cur numbers is :" + goods.getNumbers());
goodsMapper.updateAmount(goods.getNumbers() - 1, goods.getId());
}
同样执行之前的test即可,结果正确,因此,使用redission实现分布式锁的sample就到这里,下一篇是redission高性能分布式锁的实现原理。