分布式锁
分布式锁,即分布式系统中的锁,控制分布式系统有序的对共享资源进行操作
JVM锁是解决多线程下对于共享资源的操作,而分布式锁则是多进程下对于共享资源的操作
锁分类
可重入锁
一个线程得到一个对象锁后再次请求该对象锁,是永远可以拿到锁的。
每一个锁关联一个线程持有者和计数器。
持有锁的线程如果再次请求这个锁,可以再次拿到这个锁,计数器会递增。当线程退出一个synchronized 方法/块时,计数器会递减,如果计数器为 0 则释放该锁。
悲观锁
范围:mysql中的行级锁是基于索引的,如果sql没有走索引,那将使用表级锁把整张表锁住。
mysql的排他锁,select .... for update来实现悲观锁。
begin;
select nums from tb_goods_stock where goods_id = {$goods_id} for update;
update tb_goods_stock set nums = nums - {$num} where goods_id = {$goods_id} and nums >= {$num};
commit;
读锁(共享锁) 写锁(排他锁)
读写锁不共存
读锁:允许其他会话读取数据,但不允许修改数据
写锁:禁止其他会话读取和修改数据。
MDL锁
元数据锁(Meta Data Lock) 是server层的锁,表级锁,每执行一条DML、DDL语句时都会申请MDL锁,DML操作需要MDL读锁,DDL操作需要MDL写锁
公平锁
公平锁:按顺序排队获取锁,持有锁的线程执行完了,唤醒队列的下一个线程去获取锁
非公平锁:谁抢到就是谁的,不排队,抢不到就进入队列等待下一次时机,但不是排队,看运气。
公平和非公平的区别就是:线程执行同步代码块时,是否会去尝试获取锁。
隔离级别
#图片来源 动力节点
脏读:读到未提交的数据
幻读:读到的数据是假的,前后读到的数据不一致,即数据集不一样
synchronized与ReentrantLock都是可重入锁
区别:
synchronized是自动加锁、解锁 ---------------------- ReentrantLock 手动加锁、解锁
synchronized是非公平锁,谁抢到谁执行-------------ReentrantLock允许实现公平锁,遵循先来后到
解决售卖商品超卖问题
MySql锁与JVM锁
#尚硅谷
使用JVM锁解决超卖问题 ----数据为服务内共享资源
@Data
public class Srock {
private Integer stack = 5000;
}
@Service
public class RedissionService {
private Srock srock = new Srock();
private ReentrantLock Lock = new ReentrantLock();
public void sell(){
Lock.lock();
try {
srock.setStack( srock.getStack()-1);
System.out.println("库存:"+srock.getStack());
}finally {
Lock.unlock();
}
}
}
适用于共享资源存在于服务内部(也可用于MySql)
synchronized与ReentrantLock都行。
使用JVM锁解决超卖问题 --数据在MySql
三种情况造成JVM锁失效
- 多例模式(Spring默认是单例模式)
- JVM锁是对象锁,多例情况下,有多个对象,锁不住了
- 事务
mysql默认隔离级别是 可重复读
- 集群部署
多个服务器访问MySql
集群部署 和 多例模式情况差不多,不同的服务器不同的锁,并发下失效。
直接使用一条SQL语句解决(自带表级锁)
update db_stock set count = count - 1 where product_code = '1001' and count >=1;
该sql语句没有走索引,所以是表级锁。
直接使用一条SQL语句解决(设置索引且条件字段是具体值,走行级锁)
product_code设置索引,且查询条件必须是具体值。
update db_stock set count = count - 1 where product_code = '1001' and count >=1;
使用以上SQL缺点
同一个字段有多条,更新哪个? 其实两个都更新了
无法记录库存变化前后的状态
MySql悲观锁中使用行级锁 select ... from update
设置索引且条件字段是具体值,走行级锁
对于一条SQL解决的问题: 可以选择合适的字段执行更新 、可以记录库存变化
问题
性能:低于一条SQL(2000) 悲观锁(500)
死锁问题
操作要统一: 统一使用select ... from update 语句,不能使用select * from ;之类的不加锁语句
乐观锁 (时间戳 version版本号 CAS机制) 效率极低 了解
提交数据更新时,才会对数据是否冲突进行检测,不会锁表。
使用时,不能加事务,递归调用前先sleep一会
CAS机制 (MySql没有)
Compare And Swap
执行更新操作时,第一次查询出来的变量X 再次与 数据库的旧值B 做比较 ,如果相等,则执行更新操作,且时间戳/版本号也同时更新。
实现
MySql数据库新增一列(时间戳)/(Version递增)来实现CAS机制
问题:
异常一致递归,栈溢出
连接超时
问题分析:
栈溢出 ---->使用sleep()
DML操作本身是会加MDL锁的,又加了事务注解,会造成阻塞,从而造成超时问题---->关闭事务
SQL执行失败就会释放锁
MySql锁总结
数据位于redis
JVM本地锁 类似以上
redis乐观锁 watch multi exec (了解 性能300低)
watch + key 监听数据,如果exec执行之前数据变化,则事务取消执行
multi 开启事务
exec 提交事务
代码操作
@Autowired
private RedisTemplate redisTemplate;
public void redisTest(){
//redis乐观锁 使用execute方法,才允许使用 watch multi exec等命令
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
//1.watch
operations.watch("stock");
//2.查询 数据
String stock = operations.opsForValue().get("stock").toString();
if(stock!=null&&stock.length()>0){
Integer count = Integer.valueOf(stock);
if(count>0){
//3.multi 开启事务
operations.multi();
//4.数据操作
operations.opsForValue().set("stock",String.valueOf(count--));
//5.exec 提交事务
List exec = operations.exec();
//如果结果集为空,表示执行失败
if(exec==null || exec.size()==0){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
redisTest();
}
//递归
return exec;
}
}
return null;
}
});
}
分布式锁 (重点)
跨进程 、 跨服务、跨服务器
应用场景
超卖现象
缓存击穿 热点key失效
三种主流实现
基于redis
初级:独占排他使用 setnx lock 加锁 del解锁
问题:加锁后服务器马上宕机,导致死锁问题。---->给锁设置过期时间
进阶1:防死锁(设置过期时间) expire key time
问题
1:加锁语句 和 设置过期时间语句 ---的时间内内服务器宕机---->原子性
2:不可重入问题 -->在进阶6
进阶3:原子性 set key value ex 时间 nx ----- 查看剩余时间 ttl key
问题:lock误删 、任务执行时间 大于 锁过期时间
进阶4:防误删 使用UUID 解锁前判断
仅使用UUID的问题:
@Service
public class RedissionService {
@Autowired
private RedisTemplate redisTemplate;
public void redisTest(){
//redis乐观锁
//1.加锁
while (!redisTemplate.opsForValue().setIfAbsent("lock","111",5, TimeUnit.MINUTES)){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//2.查询 数据
try {
String stock = redisTemplate.opsForValue().get("stock").toString();
if(stock!=null&&stock.length()>0){
Integer count = Integer.valueOf(stock);
if(count>0){
//3.数据操作
redisTemplate.opsForValue().set("stock",String.valueOf(count--));
}
}
} finally {
//4 解锁
redisTemplate.delete("lock");
}
}
}
进阶5:防误删加强版 保证判断和删除的原子性
使用Lua脚本的原因:一次性发送多个指令给redis ,redis是单线程的,执行指令遵循one bye one规则
Redis 的一大特性就是它是单线程的。这意味着在任何给定的时间点,Redis 只会执行一个命令。 这种“一次只执行一个命令”的规则,也被称为 “one by one” 规则。这是因为 Redis 使用单线程模型来处理命令,所以命令是顺序执行的,而不是并行执行的。当一个命令在执行时,其他的命令会被放入队列中,等待当前命令执行完毕后再执行。
redis提供了对Lua的支持----eval指令
参数:numkeys 表示key的数量,从1开始
Lua入门:
学习一门语言,从hello world开始 Lua不用print,使用return返回
变量
a = 5 -- 全局变量
local b = 5 -- 局部变量
流程控制
[ 0 为 true ]
if(0)
then
return("0 为 true")
end
第一条redis Lua脚本
if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end
代码实现:
} finally {
//4 解锁 代码改造部分
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(script,Boolean.class), Arrays.asList("lock"),uuid);
/*
老版本
String lock = redisTemplate.opsForValue().get("lock").toString();
if(uuid.equals(lock)){
redisTemplate.delete("lock");
}*/
}
}
进阶6:可重入
研究reentrantlock锁(可重入的) #尚硅谷截图
redis实现可重入锁 hash结构 外层key lock 内存key uuid value 重入次数
map<lock,map<uuid,value>> + Lua脚本
if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1
then
redis.call ('hincrby', KEYS[1], ARGV[1], 1)
redis.call ('expire', KEYS [1], ARGV[2])
return 1
else
return 0
end
加锁
hincrby 命令在也具有设置锁的功能,替换hset
参数替换
解锁
if redis.call ('hexists', KEYS[1], ARGV[1]) == 0
then
return nil
elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0
then
return redis.call ('del', KEYS[1])
else
return 0
代码实现:
//注意 : DistributedLockClient是单例的,容器初始化时会生成唯一的uuid,可以 加上当前线程的id,作为lock
public class RedissDistributedLock implements Lock {
private StringRedisTemplate redisTemplate;
private String lockName;
private String uuid;
private long expire = 30;
public RedissDistributedLock(StringRedisTemplate redisTemplate, String lockName,String uuid) {
this.redisTemplate = redisTemplate;
this.lockName = lockName;
this.uuid =uuid+":"+Thread.currentThread().getId();
}
@Override
public void lock() {
this.tryLock();
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
//加锁
@Override
public boolean tryLock() {
try {
this.tryLock(-1L,TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if(time!=-1){
this.expire = unit.toSeconds(time);
}
String script = "if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1" +
"then " +
"redis.call ('hincrby', KEYS[1], ARGV[1], 1)" +
"redis.call ('expire', KEYS [1], ARGV[2])" +
"return 1 " +
"else " +
"return 0" +
"end";
while (!redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName),uuid,String.valueOf(expire)))
{
Thread.sleep(50);
}
return true;
}
@Override
public void unlock() {
String script = "if redis.call ('hexists', KEYS[1], ARGV[1]) == 0" +
"then " +
"return nil" +
"elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0" +
"then" +
"return redis.call ('del', KEYS[1])" +
"else" +
"return 0";
Long flag = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuid);
if (flag == null) {
throw new IllegalMonitorStateException("lock is not yours");
}
}
@Override
public Condition newCondition() {
return null;
}
}
@Component
public class DistributedLockClient {
private String uuid;
public DistributedLockClient(String uuid) {
this.uuid = uuid;
}
@Autowired
private StringRedisTemplate redisTemplate;
public RedissDistributedLock getRedisClient(String lockName){
return new RedissDistributedLock(redisTemplate,lockName,uuid);
}
}
@Service
public class RedissionService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private DistributedLockClient distributedLockClient;
public void redisTest(){
String uuid = UUID.randomUUID().toString();
RedissDistributedLock redisClient = distributedLockClient.getRedisClient("lock");
redisClient.lock();
//2.查询 数据
try {
String stock = redisTemplate.opsForValue().get("stock").toString();
if(stock!=null&&stock.length()>0){
Integer count = Integer.valueOf(stock);
if(count>0){
//3.数据操作
redisTemplate.opsForValue().set("stock",String.valueOf(count--));
}
}
} finally {
//4 解锁
redisClient.unlock();
}
}
}
end
进阶7:自动续期 定时任务
添加定时刷新方法
进阶8:redis集群下,锁机制失效
问题:
客户端A从主节点获取到锁
在主节点将锁同步到从节点之前,主节点宕机
从节点晋升为主节点,但它不知道A已经拿到锁了
客户端B又从它获取到了同一个资源(a拿到锁的资源)拿到了锁
redLock算法 (麻烦)
#尚硅谷
Redisson
redisson配置
redisson:
address: redis://192.168.80.150:6379
#password: 123456
database: 2
@ConfigurationProperties(prefix = "redisson")
@ConditionalOnProperty("redisson.password")
@Data
public class RedissonProperties {
private int timeout = 3000;
private String address;
private String password;
private int database = 2;
private int connectionPoolSize = 64;
private int connectionMinimumIdleSize=10;
private int slaveConnectionPoolSize = 250;
private int masterConnectionPoolSize = 250;
private String[] sentinelAddresses;
private String masterName;
}
@Configuration
@ConditionalOnClass(Config.class)
@EnableConfigurationProperties(RedissonProperties.class)
@RequiredArgsConstructor
public class RedissonAutoConfiguration {
final RedissonProperties redissonProperties;
/**
* 哨兵模式自动装配
*
* @return
*/
@Bean
@ConditionalOnProperty(name = "redisson.master-name")
RedissonClient redissonSentinel() {
Config config = new Config();
SentinelServersConfig serverConfig = config
.useSentinelServers()
.addSentinelAddress(redissonProperties.getSentinelAddresses())
.setMasterName(redissonProperties.getMasterName())
.setTimeout(redissonProperties.getTimeout())
.setMasterConnectionPoolSize(redissonProperties.getMasterConnectionPoolSize())
.setSlaveConnectionPoolSize(redissonProperties.getSlaveConnectionPoolSize());
if (StringUtils.isNotBlank(redissonProperties.getPassword())) {
serverConfig.setPassword(redissonProperties.getPassword());
}
return Redisson.create(config);
}
/**
* 单机模式自动装配
*
* @return
*/
@Bean
@ConditionalOnProperty(name = "redisson.address")
RedissonClient redissonSingle() {
Config config = new Config();
config.setCodec(new org.redisson.codec.JsonJacksonCodec());
SingleServerConfig serverConfig = config
.useSingleServer()
.setAddress(redissonProperties.getAddress())
//等待节点回复命令的时间。该时间从命令发送成功时开始计时。
.setTimeout(redissonProperties.getTimeout())
//设置对于master节点的连接池中连接数最大为50
.setConnectionPoolSize(redissonProperties.getConnectionPoolSize())
// 连接池的最小空闲连接数。当连接池中的连接数低于此值时,会尝试创建新连接。
.setConnectionMinimumIdleSize(redissonProperties.getConnectionMinimumIdleSize());
if (StringUtils.isNotBlank(redissonProperties.getPassword())) {
serverConfig.setPassword(redissonProperties.getPassword());
}
return Redisson.create(config);
}
}
使用:RedissonClient接口
@Autowires
private RedissonClient redissonClient;