一 分布式锁简介
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。
分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路
那么分布式锁他应该满足一些条件:
可见性:多个线程都能看到相同的结果,注意:这个地方说的可见性并不是并发编程中指的内存可见 性,只是说多个进程之间都能感知到变化的意思
互斥:互斥是分布式锁的最基本的条件,使得程序串行执行
高可用:程序不易崩溃,时时刻刻都保证较高的可用性
高性能:由于加锁本身就让性能降低,所有对于分布式锁本身需要他就较高的加锁性能和释放锁性能
安全性:安全也是程序中必不可少的一环
常见的分布式锁有三种
Mysql:mysql本身就带有锁机制,但是由于mysql性能本身一般,所以采用分布式锁的情况下,其实使用mysql作为分布式锁比较少见
Redis:redis作为分布式锁是非常常见的一种使用方式,现在企业级开发中基本都使用redis或者zookeeper作为分布式锁,利用setnx这个方法,如果插入key成功,则表示获得到了锁,如果有人插入成功,其他人插入失败则表示无法获得到锁,利用这套逻辑来实现分布式锁
Zookeeper:zookeeper也是企业级开发中较好的一个实现分布式锁的方案
非原子操作(setnx + expire)
一说到实现 Redis 的分布式锁,很多小伙伴马上就会想到 setnx+ expire 命令。也就是说,先用 setnx 来抢锁,如果抢到之后,再用 expire 给锁设置一个 过期时间。
伪代码如下:
if(jedis.setnx(lock_key,lock_value) == 1){ //加锁
jedis.expire(lock_key,timeout); //设置过期时间
doBusiness //业务逻辑处理
}
这块代码是 有坑的,因为 setnx 和 expire 两个命令是分开写的,并不是原子操作!如果刚要执行完 setnx 加锁,正要执行 expire 设置过期时间时,进程 crash 或者要重启维护了,那么这个锁就“ 长生不老”了,别的线程永远获取不到锁啦。
Base案例(SpringBoot+Redis)
使用场景:多个服务间保证同一时刻同一时间段内同一个用户只能有一个请求(防止关键业务出现并发攻击)
建module
redis_distributed_lock2redis_distributed_lock3
改POM
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.2.3</version>
</dependency>
</dependencies>
写YML
server.port=7777
spring.application.name=redis_distributed_lock2
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
# ========================================Redis单机############################
spring.redis.database=0
spring.redis.host=192.168.10.101
spring.redis.port=6379
spring.redis.password=111111
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
业务类
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale() {
String message = "";
lock.lock();
try {
//查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//扣减库存,每次扣减一个
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
message = "成功卖出商品,库存剩余: " + inventoryNumber;
log.info(message + "\t" + "服务端口号:" + port);
} else {
message = "商品卖完了,(;′⌒`)";
}
} finally {
lock.unlock();
}
return message + "\t" + "服务端口号:" + port;
}
}
@Api(tags = "redis分布式测试")
@RestController
@Slf4j
public class InventoryController {
@Autowired
private InventoryService inventoryService;
@ApiOperation("扣减库存,一次卖一个")
@GetMapping("inventory/sale")
public String sale() {
return inventoryService.sale();
}
}
手写分布式锁思路分析
初始化版本简单添加
将7777的业务逻辑代码原样拷贝到8888
nginx分布式微服务架构
v2.0版本代码分布式部署后,单机锁还是出现超卖现象,需要分布式锁
Nginx配置负载均衡
● 配置地址:/usr/local/nginx/conf (我的在windows系统下)
● 修改配置文件,nginx.conf新增反向代理和负载均衡配置
v2.0版本代码修改+启动
Nginx访问,可以看到效果,一边一个,默认轮询。
手工验证ok,开始高并发模拟
使用jmete进行压测
bug-why
在单机环境下,可以使用synchronized或lock来实现,但是在分布式系统中,因为竞争的线程可能不在同一节点上(同一个JVM中),所以需要一个让所有进程都能访问到的锁来实现(比如redis或者zookeeper来构建)
不同进程JVM层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程
分布式锁出现
● 跨进程+跨服务
● 解决超卖
● 防止缓存击穿
解决
Redis具有极高的性能且其命令对分布式锁支持友好,借助SET命令即可实现加锁处理
Redis分布式锁
递归方式,不推荐,容易
package com.atguigu.redislock.service;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @auther zzyy
* @create 2022-10-22 15:14
*/
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale()
{
String retMessage = "";
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue);
if(!flag){
//暂停20毫秒后递归调用
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
sale();
}else{
try{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
System.out.println(retMessage);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}finally {
stringRedisTemplate.delete(key);
}
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
while代替if,相当于juc的虚假唤醒,自旋
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
public String sale() {
String retMessage = "";
String key = "wghRedisLock";
String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
// 不用递归了,高并发下容易出错,我们用自旋替代递归方法调用;也不用if,用while来替代
while (!stringRedisTemplate.opsForValue().setIfAbsent(key, value)) {
// 暂停20毫秒,进行递归重试
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
// 抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
System.out.println(retMessage);
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
stringRedisTemplate.delete(key);
}
return retMessage + "\t" + "服务端口号:" + port;
}
}
上个版本的代码存在问题:如果部署了微服务的Java程序挂了,代码层面根本没有走到finally这块,没办法保证解锁(无过期时间,改key一直存在),这个key没有被删除,需要加入一个过期时间限定key。
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue))
{
//暂停20毫秒,进行递归重试.....
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
stringRedisTemplate.expire(key,30L,TimeUnit.SECONDS);
设置key+过期时间必须合并一行保证原子性
package com.atguigu.redislock.service;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale()
{
String retMessage = "";
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS))
{
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
System.out.println(retMessage);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}finally {
stringRedisTemplate.delete(key);
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
实际业务处理时间如果超过了默认设置key的过期时间,张冠李戴,就会把别人的锁删除了!!!
我们的要求是只能删除自己的,不能动别人的。
package com.atguigu.redislock.service;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @auther zzyy
* @create 2022-10-22 15:14
*/
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale()
{
String retMessage = "";
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS))
{
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t"+uuidValue;
System.out.println(retMessage);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}finally {
// v5.0判断加锁与解锁是不是同一个客户端,同一个才行,自己只能删除自己的锁,不误删他人的
if(stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue)){
stringRedisTemplate.delete(key);
}
}
return retMessage+"\t"+"服务端口号:"+port;
}
}
Lua保证原子性
最后的判断+delete不是原子操作,需要用Lua脚本进行修改。
Lua脚本浅谈
Redis调用Lua脚本通过eval命令保证代码执行的原子性,直接用return返回脚执行后的结果值
命令:eval luascript numkeys [key[key…]] [arg[arg…]]
eval “redis.call(‘set’,‘k1’,‘v1’) redis.call(‘expire’,‘k1’,‘30’) return redis.call(‘get’,‘k1’)” 0
eval “return redis.call(‘mset’,KEYS[1],ARGV[1],KEYS[2],ARGV[2])” 2 k1 k2 lua1 lua2
进阶判断:eval “if redis.call(‘get’,KEYS[1])==ARGV[1] then return redis.call(‘del’,KEYS[1]) else return 0 end” 1 wghRedisLock 11112222
package com.atguigu.redislock.service;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @auther zzyy
* @create 2022-10-22 15:14
*/
@Service
@Slf4j
public class InventoryService
{
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();
public String sale()
{
String retMessage = "";
String key = "zzyyRedisLock";
String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS))
{
//暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
}
try
{
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if(inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t"+uuidValue;
System.out.println(retMessage);
}else{
retMessage = "商品卖完了,o(╥﹏╥)o";
}
}finally {
//V6.0 将判断+删除自己的合并为lua脚本保证原子性
String luaScript =
"if (redis.call('get',KEYS[1]) == ARGV[1]) then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
stringRedisTemplate.execute(new DefaultRedisScript<>(luaScript, Boolean.class), Arrays.asList(key), uuidValue);
}
return retMessage+"\t"+"服务端口号:"+port;
}
}