赞
踩
分布式应用进行逻辑处理时经常会遇到并发问题。比如一个操作要修改用户的状态,修改状态需要先读出用户的状态, 在内存里进行修改,改完了再存回去。如果这样的操作同时进行了,就会出现并发问题, 因为读取和保存状态这两个操作不是原子的。(Wiki 解释:所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始, 就一直运行到结束,中间不会有任何 context switch 线程切换。)这个时候就要使用到分布式锁来限制程序并发的执行。本博文主要是的介绍分布式锁的原理和应用场景。
锁是一种常用的并发控制机制,用于保证一项资源在任何时候只能被一个线程使用,如果其他线程也要使用同样的资源,必须排队等待上一个线程使用完。
锁指的是程序级别的锁,例如 Java 语言中的 synchronized 和 ReentrantLock 在单应用中使用不会有任何问题, 但如果放到分布式环境下就不适用了,这个时候我们就要使用分布式锁。分布式锁比较好理解就是用于分布式环境下并发控制的一种机制, 用于控制某个资源在同一时刻只能被一个应用所使用。分布式锁比较常见的实现方式有三种:
场景描述:客户端模拟购买商品过程,在Redis中设定库存总数剩100个,多个客户端同时并发购买。
- package com.zhuangxiaoyan.springbootredis.controller;
-
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RestController;
- /**
- * @description
- * 最简单的情况,没有加任何的考虑,
- * 即使是单体应用,并发情况下数据一致性都有问题
- * @param: null
- * @date: 2022/4/9 21:25
- * @return:
- * @author: xjl
- */
-
- @RestController
- public class NoneController {
-
- @Autowired
- StringRedisTemplate template;
-
- @RequestMapping("/buy")
- public String index() {
- // Redis中存有goods:001号商品,数量为100 相当于是的redis中的get("goods")的操作。
- String result = template.opsForValue().get("goods");
- // 获取到剩余商品数
- int total = result == null ? 0 : Integer.parseInt(result);
- if (total > 0) {
- // 剩余商品数大于0 ,则进行扣减
- int realTotal = total - 1;
- // 将商品数回写数据库 相当于设置新的值的结果
- template.opsForValue().set("goods", String.valueOf(realTotal));
- System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
- return "购买商品成功,库存还剩:" + realTotal + "件";
- } else {
- System.out.println("购买商品失败");
- }
- return "购买商品失败";
- }
- }
使用Jmeter模拟高并发场景,测试结果如下:
测试结果出现多个用户购买同一商品,发生了数据不一致问题!解决办法:单体应用的情况下,对并发的操作进行加锁操作,保证对数据的操作具有原子性:
- package com.zhuangxiaoyan.springbootredis.controller;
-
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RestController;
-
- import java.util.concurrent.locks.Lock;
- import java.util.concurrent.locks.ReentrantLock;
-
- /**
- * @description 单体应用的情况下,对并发的操作进行加锁操作,保证对数据的操作具有原子性
- * 1. synchronized
- * 2. ReentrantLock
- * 这种情况下,不会产生并发问题
- * @param: null
- * @date: 2022/4/9 21:25
- * @return:
- * @author: xjl
- */
- @RestController
- public class ReentrantLockController {
- // 引入的ReentrantLock 锁机制
- Lock lock = new ReentrantLock();
-
- @Autowired
- StringRedisTemplate template;
-
- @RequestMapping("/buy")
- public String index() {
- // 加锁
- lock.lock();
- try {
- // Redis中存有goods:001号商品,数量为100 相当于是的redis中的get("goods")的操作。
- String result = template.opsForValue().get("goods");
- // 获取到剩余商品数
- int total = result == null ? 0 : Integer.parseInt(result);
- if (total > 0) {
- int realTotal = total - 1;
- // 将商品数回写数据库 相当于设置新的值的结果
- template.opsForValue().set("goods", String.valueOf(realTotal));
- System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
- return "购买商品成功,库存还剩:" + realTotal + "件";
- } else {
- System.out.println("购买商品失败");
- }
- } catch (Exception e) {
- //解锁
- lock.unlock();
- } finally {
- //解锁
- lock.unlock();
- }
- return "购买商品失败";
- }
- }
上面解决了单体应用的数据一致性问题,但如果是分布式架构部署呢,架构如下:提供两个服务,端口分别为8001、8002,连接同一个Redis服务,在服务前面有一台Nginx作为负载均衡。两台服务代码相同,只是端口不同。
将8001、8002两个服务启动,每个服务依然用ReentrantLock加锁,用Jmeter做并发测试,发现会出现数据一致性问题!
- package com.zhuangxiaoyan.springbootredis.controller;
-
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RestController;
-
- import java.util.UUID;
-
- /**
- * @description 面使用redis的set命令来实现加锁
- * 1.SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]
- * EX seconds − 设置指定的到期时间(以秒为单位)。
- * PX milliseconds - 设置指定的到期时间(以毫秒为单位)。
- * NX - 仅在键不存在时设置键。
- * XX - 只有在键已存在时才设置。
- * @param: null
- * @date: 2022/4/9 21:25
- * @return:
- * @author: xjl
- */
- @RestController
- public class RedisLockControllerV1 {
-
- public static final String REDIS_LOCK = "good_lock";
-
- @Autowired
- StringRedisTemplate template;
-
- @RequestMapping("/buy")
- public String index() {
-
- // 每个人进来先要进行加锁,key值为"good_lock"
- String value = UUID.randomUUID().toString().replace("-", "");
- try {
- Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value);
- // 加锁失败
- if (!flag) {
- return "抢锁失败!";
- }
- System.out.println(value + " 抢锁成功");
- String result = template.opsForValue().get("goods");
- int total = result == null ? 0 : Integer.parseInt(result);
- if (total > 0) {
- int realTotal = total - 1;
- template.opsForValue().set("goods", String.valueOf(realTotal));
- System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
- return "购买商品成功,库存还剩:" + realTotal + "件";
- } else {
- System.out.println("购买商品失败");
- }
- return "购买商品失败";
- } finally {
- // 如果在抢到所之后,删除锁之前,发生了异常,锁就无法被释放,所以要在finally处理 template.delete(REDIS_LOCK);
- template.delete(REDIS_LOCK);
- }
- }
- }
如果程序在运行期间,部署了微服务jar包的机器突然挂了,代码层面根本就没有走到finally代码块,也就是说在宕机前,锁并没有被删除掉,这样的话,就没办法保证解锁,所以,这里需要对这个key加一个过期时间,Redis中设置过期时间有两种方法:
第一种方法需要单独的一行代码,且并没有与加锁放在同一步操作,所以不具备原子性,也会出问题, 第二种方法在加锁的同时就进行了设置过期时间,所有没有问题,这里采用这种方式。
- // 为key加一个过期时间,其余代码不变
- Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK,value,10L,TimeUnit.SECONDS);
- package com.zhuangxiaoyan.springbootredis.controller;
-
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RestController;
-
- import java.util.UUID;
- import java.util.concurrent.TimeUnit;
-
- /**
- * @description 在第四种情况下,如果在程序运行期间,部署了微服务的jar包的机器突然挂了,代码层面根本就没有走到finally代码块
- * 没办法保证解锁,所以这个key就没有被删除
- * 这里需要对这个key加一个过期时间,设置过期时间有两种方法
- * 1. template.expire(REDIS_LOCK,10, TimeUnit.SECONDS);第一种方法需要单独的一行代码,并没有与加锁放在同一步操作,所以不具备原子性,也会出问题
- * 2. template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS);第二种方法在加锁的同时就进行了设置过期时间,所有没有问题
- * @date: 2022/4/9 21:25
- * @return:
- * @author: xjl
- */
- @RestController
- public class RedisLockControllerV2 {
-
- public static final String REDIS_LOCK = "good_lock";
-
- @Autowired
- StringRedisTemplate template;
-
- @RequestMapping("/buy")
- public String index() {
-
- // 每个人进来先要进行加锁,key值为"good_lock"
- String value = UUID.randomUUID().toString().replace("-", "");
- try {
- // 为key加一个过期时间 10s
- Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);
- // 加锁失败
- if (!flag) {
- return "抢锁失败!";
- }
- System.out.println(value + " 抢锁成功");
- String result = template.opsForValue().get("goods");
- int total = result == null ? 0 : Integer.parseInt(result);
- if (total > 0) {
- int realTotal = total - 1;
- template.opsForValue().set("goods", String.valueOf(realTotal));
- System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
- return "购买商品成功,库存还剩:" + realTotal + "件";
- } else {
- System.out.println("购买商品失败");
- }
- return "购买商品失败";
- } finally {
- // 如果在抢到所之后,删除锁之前,发生了异常,锁就无法被释放,所以要在finally处理 template.delete(REDIS_LOCK);
- template.delete(REDIS_LOCK);
- }
- }
- }
设置了key的过期时间,解决了key无法删除的问题,但问题又来了,上面设置了key的过期时间为10秒,如果业务逻辑比较复杂,需要调用其他微服务, 处理时间需要15秒(模拟场景,别较真),而当10秒钟过去之后,这个key就过期了,其他请求就又可以设置这个key, 此时如果耗时15秒的请求处理完了,回来继续执行程序,就会把别人设置的key给删除了,这是个很严重的问题!所以,谁上的锁,谁才能删除。
- package com.zhuangxiaoyan.springbootredis.controller;
-
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RestController;
-
- import java.util.UUID;
- import java.util.concurrent.TimeUnit;
- /**
- * @description
- * 在第五种情况下,设置了key的过期时间,解决了key无法删除的问题,但问题又来了
- * 我们设置了key的过期时间为10秒,如果我们的业务逻辑比较复杂,需要调用其他微服务,需要15秒
- * 10秒钟过去之后,这个key就过期了,其他请求就又可以设置这个key了
- * 但是如果耗时的请求处理完了,回来继续执行程序,就会把别人设置的key给删除了,这是个很严重的问题
- * 所以,谁上的锁,谁才能删除
- * @date: 2022/4/9 21:25
- * @return:
- * @author: xjl
- */
- @RestController
- public class RedislockControllerV3 {
-
- public static final String REDIS_LOCK = "good_lock";
-
- @Autowired
- StringRedisTemplate template;
-
- @RequestMapping("/buy")
- public String index() {
-
- // 每个人进来先要进行加锁,key值为"good_lock"
- String value = UUID.randomUUID().toString().replace("-", "");
- try {
- // 为key加一个过期时间10s
- Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);
- // 加锁失败
- if (!flag) {
- return "抢锁失败!";
- }
- System.out.println(value + " 抢锁成功");
- String result = template.opsForValue().get("goods");
- int total = result == null ? 0 : Integer.parseInt(result);
- if (total > 0) {
- // 如果在此处需要调用其他微服务,处理时间较长。。。
- int realTotal = total - 1;
- template.opsForValue().set("goods", String.valueOf(realTotal));
- System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
- return "购买商品成功,库存还剩:" + realTotal + "件";
- } else {
- System.out.println("购买商品失败");
- }
- return "购买商品失败";
- } finally {
- // 谁加的锁,谁才能删除
- if (template.opsForValue().get(REDIS_LOCK).equals(value)) {
- template.delete(REDIS_LOCK);
- }
- }
- }
- }
规定了谁上的锁,谁才能删除,但finally快的判断和del删除操作不是原子操作,并发的时候也会出问题,并发嘛,就是要保证数据的一致性,保证数据的一致性, 最好要保证对数据的操作具有原子性。在redis中的保证原子操作的是
- package com.zhuangxiaoyan.springbootredis.controller;
-
- import com.zhuangxiaoyan.springbootredis.utils.RedisUtils;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RestController;
- import redis.clients.jedis.Jedis;
-
- import java.util.Collections;
- import java.util.List;
- import java.util.UUID;
- import java.util.concurrent.TimeUnit;
-
- /**
- * @description 在第六种情况下,规定了谁上的锁,谁才能删除
- * 但finally快的判断和del删除操作不是原子操作,并发的时候也会出问题
- * 并发就是要保证数据的一致性,保证数据的一致性,最好要保证对数据的操作具有原子性
- * @param: null
- * @date: 2022/4/9 21:25
- * @return:
- * @author: xjl
- */
-
- @RestController
- public class RedisLockControllerV4 {
-
- public static final String REDIS_LOCK = "good_lock";
-
- @Autowired
- StringRedisTemplate template;
-
- /**
- * @description 使用Lua脚本,进行锁的删除
- * @param:
- * @date: 2022/4/9 21:56
- * @return: java.lang.String
- * @author: xjl
- */
- @RequestMapping("/buy")
- public String index() {
-
- // 每个人进来先要进行加锁,key值为"good_lock"
- String value = UUID.randomUUID().toString().replace("-", "");
- try {
- // 为key加一个过期时间
- Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);
- // 加锁失败
- if (!flag) {
- return "抢锁失败!";
- }
- System.out.println(value + " 抢锁成功");
- String result = template.opsForValue().get("goods");
- int total = result == null ? 0 : Integer.parseInt(result);
- if (total > 0) {
- // 如果在此处需要调用其他微服务,处理时间较长。。。
- int realTotal = total - 1;
- template.opsForValue().set("goods", String.valueOf(realTotal));
- System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
- return "购买商品成功,库存还剩:" + realTotal + "件";
- } else {
- System.out.println("购买商品失败");
- }
- return "购买商品失败,服务端口为8001";
- } finally {
- // 谁加的锁,谁才能删除 使用Lua脚本,进行锁的删除
- Jedis jedis = null;
- try {
- jedis = RedisUtils.getJedis();
- String script = "if redis.call('get',KEYS[1]) == ARGV[1] " +
- "then " +
- "return redis.call('del',KEYS[1]) " +
- "else " +
- " return 0 " +
- "end";
- Object eval = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value));
- if ("1".equals(eval.toString())) {
- System.out.println("-----del redis lock ok....");
- } else {
- System.out.println("-----del redis lock error ....");
- }
- } catch (Exception e) {
- System.out.println(e.getMessage());
- } finally {
- if (null != jedis) {
- jedis.close();
- }
- }
- }
- }
-
- /**
- * @description 使用redis事务
- * @param:
- * @date: 2022/4/9 21:56
- * @return: java.lang.String
- * @author: xjl
- */
- @RequestMapping("/buy2")
- public String index2() {
-
- // 每个人进来先要进行加锁,key值为"good_lock"
- String value = UUID.randomUUID().toString().replace("-", "");
- try {
- // 为key加一个过期时间
- Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);
- // 加锁失败
- if (!flag) {
- return "抢锁失败!";
- }
- System.out.println(value + " 抢锁成功");
- String result = template.opsForValue().get("goods");
- int total = result == null ? 0 : Integer.parseInt(result);
- if (total > 0) {
- // 如果在此处需要调用其他微服务,处理时间较长。。。
- int realTotal = total - 1;
- template.opsForValue().set("goods", String.valueOf(realTotal));
- System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
- return "购买商品成功,库存还剩:" + realTotal + "件";
- } else {
- System.out.println("购买商品失败");
- }
- return "购买商品失败,服务端口为8001";
- } finally {
- // 谁加的锁,谁才能删除 ,使用redis事务
- while (true) {
- template.watch(REDIS_LOCK);
- if (template.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)) {
- template.setEnableTransactionSupport(true);
- template.multi();
- template.delete(REDIS_LOCK);
- List<Object> list = template.exec();
- if (list == null) {
- continue;
- }
- }
- template.unwatch();
- break;
- }
- }
- }
- }
规定了谁上的锁,谁才能删除,并且解决了删除操作没有原子性问题。但还没有考虑缓存续命,以及Redis集群部署下,异步复制造成的锁丢失: 主节点没来得及把刚刚set进来这条数据给从节点,就挂了。所以直接上RedLock的Redisson落地实现。
- package com.zhuangxiaoyan.springbootredis.controller;
-
- import org.redisson.Redisson;
- import org.redisson.api.RLock;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RestController;
-
- import java.util.UUID;
- /**
- * @description
- * 在第六种情况下,规定了谁上的锁,谁才能删除
- * 1. 缓存续命
- * 2. redis异步复制造成的锁丢失:主节点没来得及把刚刚set进来这条数据给从节点,就挂了
- * @param: null
- * @date: 2022/4/9 21:25
- * @return:
- * @author: xjl
- */
-
- @RestController
- public class RedisLockControllerV5 {
-
- public static final String REDIS_LOCK = "good_lock";
-
- @Autowired
- StringRedisTemplate template;
-
- @Autowired
- Redisson redisson;
-
- @RequestMapping("/buy")
- public String index() {
- RLock lock = redisson.getLock(REDIS_LOCK);
- lock.lock();
- // 每个人进来先要进行加锁,key值为"good_lock"
- String value = UUID.randomUUID().toString().replace("-", "");
- try {
- String result = template.opsForValue().get("goods");
- int total = result == null ? 0 : Integer.parseInt(result);
- if (total > 0) {
- // 如果在此处需要调用其他微服务,处理时间较长。。。
- int realTotal = total - 1;
- template.opsForValue().set("goods", String.valueOf(realTotal));
- System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
- return "购买商品成功,库存还剩:" + realTotal + "件";
- } else {
- System.out.println("购买商品失败");
- }
- return "购买商品失败";
- } finally {
- // 如果锁依旧在同时还是在被当前线程持有,那就解锁。 如果是其他的线程持有 那就不能释放锁资源
- if (lock.isLocked() && lock.isHeldByCurrentThread()) {
- lock.unlock();
- }
- }
- }
- }
系统设计解决方案/1-分布式锁方案/分布式锁解决方案.md · 庄小焱/SeniorArchitect - Gitee.com
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。