1 分布式锁的概述
概述
随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题,常用的分布式锁的解决方案。
CAP理论:一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)
方案 | 细分方案 | 说明 | |
---|---|---|---|
基于数据库实现分布式锁 | 可使用乐观锁、悲观锁来来控制 | 性能最差,高并发不合适 | |
基于Redis | 多主机单Redis:CP | 综合来看,Redis性能最高。多台主机集群,Redis异步复制可能会造成锁丢失:主机刚把key set了就挂了,从机还没复制到该条数据。解决:不适用master-slaver,而使用多台master主机。 | |
多主机多Redis集群:AP | |||
基于Zookeeper集群 | 集群CP | 可靠性最高。多台主机集群,为了追求可靠性,牺牲了高可用性。 多用于银行等对于数据一致性要求很高的业务场景。 |
ZooKeeper集群:CP
Eureka集群:AP
Redis集群:AP
详见RedLock算法之Redisson的落地实现框架。
Redis实现分布式锁的原理
实现逻辑
# set sku:1:info “OK” NX PX 10000
EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
XX :只在键已经存在时,才对键进行设置操作。
1. 多个客户端同时获取锁(setnx)
2. 获取成功,执行业务逻辑{从db获取数据,放入缓存},执行完成释放锁(del)
3. 其他客户端等待重试
XShell下测试
在xsehll下的撰写栏输入:setnx name fly,可以看到只有一个设置成功了。
2 多主机单Redis锁控制
使用Lua+Redis自己编码
copy机器模拟负载均衡:在Services->Copy Configuration,在program arguments: --server.port=10003
注意网关必须配置了才能达到负载均衡的效果//当然如果只是简易的测试,可以使用nginx做轮询
spring:
cloud:
gateway:
routes:
- id: product_route
uri: lb://gulimall-product-micservere
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
其中:gulimall-product-micservere在项目间的配置
spring:
application:
name: gulimall-product-micservere
代码注意点
问题 | 解决方案 |
---|---|
核心原理是什么? | 使用Redis的setnx命令:由于setnx的命令都是原子性、单线程的,一次只能设置一次,设置失败了就相当于锁(坑)被占用了。注意:锁的key是一定的,value可以变。 |
两个线程同时去抢,线程2没有抢到锁,如何处理? | 使用自旋像Synchronize是自旋锁,其会自动等待并抢占锁,如果是自己设置的锁必须阻塞循环调用方法等待下次抢锁。 使用while循环 |
Redis设置好了锁后,Java服务器宕机了,没有执行到删除锁这一逻辑,造成坑位一直被占据了后续逻辑方法一直死锁,如何解决? | - 抢占了锁之后必须要设置过期时间(兜底)。 - 不能把设置锁和设置过期时间的语句在Java里面分隔开,否则中途执行过程中死机了就会死锁。可使用 ops.setIfAbsent("lock", uuid, 500, TimeUnit.SECONDS); ,这一条语句可保证既可以设置锁又可以同时设置过期时间。 |
|
| 业务还没执行完锁就过期了,别人拿到锁,自己执行完去删了别人的锁,如何处理? |
- 锁自动续期,自动续期自己不好实现。redisson有看门狗机制,推荐使用Redisson分布式锁框架。
- 同时删锁的时候要明确是自己的锁,如key加上uuid,再删除的时候不是直接del掉key,而是要验证value是否一致,这样即使别人上了锁,我去删的时候发现uuid不一样,也是不能删掉该锁。
- 删除锁必须保证原子性(保证判断和删除是原子的):使用redis+Lua脚本完成。脚本是原子的。
为什么判断和删除需要保证原子性?像如下没有保证原子性的代码:
|
使用总结
- 单主机版:synchronized和Lock在单机版可行,但是一旦多台主机涉及到分布式就不可以。
- 多主机版&单Redis锁
- 只加了锁,没有释放锁,出异常的话,可能无法释放锁,必须要在代码层面finally释放锁
- 宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定
- 为redis的分布式锁key增加过期时间,此外还必须确保setnx+过期时间必须在同一行
- 必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴。1删2、2删3
- note1:要想简单,可以不用自己编码,直接使用Redisson框架
- note2:如果单个Redis挂了,那分布式锁就失效了,此时必须要上Redis集群使用多分布式锁来控制。
- 多主机&多Redis锁:redis集群环境下,我们自己写的也不OK,直接上RedLock之Redisson落地实现。
代码演示
//1.0 入口代码
@RequestMapping("/list")
//@RequiresPermissions("product:brand:list")
public R list(@RequestParam Map<String, Object> params) {
PageUtils page = getPageUtilsValueByRedisDisLock(params);
return R.ok().put("page", page);
}
//2.0 分布式锁代码--搭配了lua脚本
public PageUtils getPageUtilsValueByRedisDisLock(Map<String, Object> params) {
ValueOperations<String, String> ops = redisTemplate.opsForValue();
//1.0 使用UUID:确保只能删除本人存入的数据
String uuid = UUID.randomUUID().toString();
//2.0 setIfAbsent:类似于Linux的setnx,如果值为true则代表抢到了锁。
//ops.setIfAbsent语句是原子性的。
Boolean lock = ops.setIfAbsent("lock", uuid, 500, TimeUnit.SECONDS);
if (lock) {
//System.out.println("获取分布式锁成功");
//3.0 抢上了锁,去执行查询语句,该查询语句内部会加本地锁判断。
PageUtils page = null;
try {
page = getDataFromRedisOrDB(params);
} finally {
//4.0 使用Lua脚本删除之前设置的Lock Key值。
//放到finally里面是为了达到如果有异常也可以删除锁(当然Redis里面的锁到期后也会删除)
String lockValue = ops.get("lock");
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), lockValue);
}
return page;
} else {
//System.out.println("获取分布式锁失败...等待重试");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 睡眠0.1s后,重新调用 //自旋
return getPageUtilsValueByRedisDisLock(params);
}
}
//3.0 查询数据库的语句
public PageUtils getDataFromRedisOrDB(Map<String, Object> params) {
//1.0 从Redis获取page数据(由于上面已经做了分布锁,所以此处不用再写锁了)
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
//不能直接使用(String)valueOperations.get("page");
String pageJson = valueOperations.get("page");
if (!StringUtils.isEmpty(pageJson)) {
// 缓存不为null直接返回
PageUtils page = JSON.parseObject(pageJson, new TypeReference<PageUtils>() {
});
return page;
}
//2.0 获取不到再读取数据库并设置Redis
System.out.println("读取数据库");
PageUtils pageDB = brandService.queryPageFromDB(params);
valueOperations.set("page", JSON.toJSONString(pageDB), 1, TimeUnit.DAYS);
return pageDB;
}
使用RedLock-Redisson框架
多主机单Redis下可以直接使用Redisson分布式锁框架来实现。详细使用参考:使用RedLock-Redisson来实现多主机单Redis分布式锁
Redisson框架的实现原理也是通过Lau+Redis来实现,只不过比我们考虑的更全面,功能更强大(如多次可重入、集群等功能)
package com.atguigu.redis.controller;
import com.atguigu.redis.util.RedisUtils;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Jedis;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.springframework.core.io.support.EncodedResource;
@RestController
public class GoodController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public static final String KEY = "goods20220101";
@Value("${server.port}")
private String serverPort;
@Autowired
private Redisson redisson;
@GetMapping("/buy_goods")
public String buy_Goods() throws Exception {
RLock redissonLock = redisson.getLock(KEY);
redissonLock.lock();
try {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口:" + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort;
} finally {
if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {
redissonLock.unlock();
}
}
}
}
3 多主机多Redis锁控制
为什么需要上Redis集群:为了预防某个Redis节点挂了,必须上Redis集群,这个时候就必须上框架了,可以使用RedLock算法之Redisson的落地实现框架。
03 Redis高阶:RedLock-Redisson的使用