文章目录
分布式锁-Redis解决方案和Redisson解决方案
分布式锁-数据库mysql解决方案
分布式锁-Redis红锁解决方案
1:分布式锁的概念
1:概念
分布式锁(多服务共享锁) 在分布式的部署环境下,通过锁机制来让多客户端互斥的对共享资源进行访问
控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性。
2:锁/分布式锁/事务区别
- 锁 单进程的系统中,存在多线程同时操作一个公共变量,此时需要加锁对变量进行同步操作,保证多线程的操作线性执行消除并发修改。解决的是单进程中的多线程并发问题。
- 分布式锁 只要的应用场景是在集群模式的多个相同服务,可能会部署在不同机器上,解决进程间安全问题,防止多进程同时操作一个变量或者数据库。解决的是多进程的并发问题 事务 解决一个会话过程中,上下文的修改对所有数据库表的操作要么全部成功,要不全部失败。所以应用在service层。解决的是一个会话中的操作的数据一致性。
- 分布式事务 解决一个联动操作,比如一个商品的买卖分为添加商品到购物车、修改商品库存,此时购物车服务和商品库存服务可能部署在两台电脑,这时候需要保证对两个服务的操作都全部成功或者全部回退。解决的是组合服务的数据操作的一致性问题
2:本文使用的案例场景
1:需求
当在打车软件中,乘客下了订单。多个司机抢单,此时因为单子只有一个,多个司机对此共享资源进行抢,此处应该使用分布式锁;
2:controller层代码
@GetMapping("/do/{orderId}")
public String grab(@PathVariable("orderId") int orderId, int driverId){
System.out.println("order:"+orderId+",driverId:"+driverId);
//此处调用锁控制层代码
grabService.grabOrder(orderId,driverId);
return "";
}
3:锁控制层代码(使用synchronized 不成功)
使用synchronized 不能保证多台服务器只有一个抢成功;因为synchronized 只能锁本服务的资源;多台服务的资源是锁不住的;
@Autowired
OrderService orderService;
@Override
public String grabOrder(int orderId, int driverId) {
String lock = (orderId+"");
synchronized (lock.intern()) {
try {
System.out.println("司机:"+driverId+" 执行抢单逻辑");
//此处调用订单业务代码
boolean b = orderService.grab(orderId, driverId);
if(b) {
System.out.println("司机:"+driverId+" 抢单成功");
}else {
System.out.println("司机:"+driverId+" 抢单失败");
}
} finally {
}
}
return null;
}
4:调用的订单业务代码
这一层就是写的伪代码,后续并不关注他
3:Redis解决方案-手写redis
1:原理和问题优化处理
1:判断一个固定的key在Redis里是否存在,如果这个key存在,说明这把锁正在被使用,反之说明这把锁没有被使用可以被过去
2:如果一个请求获取锁成功,但还没等释放释放锁的时候,这个服务挂掉的改怎么办?这会导致其他的服务永远也无法获取到锁,
此时可以在set key时,设置key的过期时间
3:加超时时间,会有加不上的情况,比如设置key和加过期时间不是原子操作,
此时我们可以将这两个操作变为一个方法,实现原子操作
4:在一个请求释放锁之前,key过期了,锁又被其他请求用了,然后这个请求有释放了锁,导致了释放其他请求的锁,这种情况怎么处理?
那我们可以在sey key 时,value存放针对于这个请求唯一的字符串,在释放锁之前判断是否是我创建的锁,也就是说只能释放自己的锁
5:一个请求还没执行到关键代码,锁就过期(超时)了,这又会导致并发抢锁现象,这种情况怎么处理?
我们可以使用另一个线程(守护线程),来观察这次请求是否结束,如果没有结束延长锁的过期时间(续约)
2:锁机制代码-设置过期时间和只能释放自己的锁
package com.online.taxi.order.service.impl;
import com.online.taxi.order.service.GrabService;
import com.online.taxi.order.service.OrderService;
import com.online.taxi.order.service.RenewGrabLockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* @author
*/
@Service
public class GrabRedisLockServiceImpl implements GrabService {
//redis调用连接
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
OrderService orderService;
@Autowired
RenewGrabLockService renewGrabLockService;
@Override
public String grabOrder(int orderId , int driverId){
//生成key
String lock = "order_"+(orderId+"");
/*
* 情况一,如果锁没执行到释放,比如业务逻辑执行一半,运维重启服务,或 服务器挂了,没走 finally,怎么办?
* 加超时时间
* setnx
*/
// boolean lockStatus = stringRedisTemplate.opsForValue().setIfAbsent(lock.intern(), driverId+"");
// if(!lockStatus) {
// return null;
// }
/*
* 情况二:加超时时间,会有加不上的情况,运维重启
*/
// boolean lockStatus = stringRedisTemplate.opsForValue().setIfAbsent(lock.intern(), driverId+"");
// stringRedisTemplate.expire(lock.intern(), 30L, TimeUnit.SECONDS);
// if(!lockStatus) {
// return null;
// }
/*
* 情况三:超时时间应该一次加,不应该分2行代码,
*
*/
boolean lockStatus = stringRedisTemplate.opsForValue().setIfAbsent(lock.intern(), driverId+"", 10L, TimeUnit.SECONDS);
if(!lockStatus) {
return null;
}
try {
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println("司机:"+driverId+" 执行抢单逻辑");
boolean b = orderService.grab(orderId, driverId);
if(b) {
System.out.println("司机:"+driverId+" 抢单成功");
}else {
System.out.println("司机:"+driverId+" 抢单失败");
}
} finally {
/**
* 情况四:这种释放锁有,可能释放了别人的锁。
*/
// stringRedisTemplate.delete(lock.intern());
/**
* 下面代码避免释放别人的锁,通过key对应的value值来判断是否为自己的锁
*/
if((driverId+"").equals(stringRedisTemplate.opsForValue().get(lock.intern()))) {
stringRedisTemplate.delete(lock.intern());
}
}
return null;
}
}
3:锁机制代码-手动增加续约时间
1:写一个异步的守护线程来主动为当前redis的key增加到期时间
@Service
public class RenewGrabLockServiceImpl implements RenewGrabLockService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
@Async
public void renewLock(String key, String value, int time) {
System.out.println("续命"+key+" "+value);
String v = redisTemplate.opsForValue().get(key);
while (StringUtils.isNotBlank(v) && v.equals(value)){
int sleepTime = time / 3;
try {
Thread.sleep(sleepTime * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
redisTemplate.expire(key,time,TimeUnit.SECONDS);
}
}
}
2:业务代码
@Service
public class GrabRedisLockServiceImpl implements GrabService {
//redis调用连接
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
OrderService orderService;
@Autowired
RenewGrabLockService renewGrabLockService;
@Override
public String grabOrder(int orderId , int driverId){
//生成key
String lock = "order_"+(orderId+"");
boolean lockStatus = stringRedisTemplate.opsForValue().setIfAbsent(lock.intern(), driverId+"", 10L, TimeUnit.SECONDS);
// 开个子线程,原来时间N,每个n/3,去续上n
renewGrabLockService.renewLock(lock.intern(),driverId+"",10);
if(!lockStatus) {
return null;
}
try {
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println("司机:"+driverId+" 执行抢单逻辑");
boolean b = orderService.grab(orderId, driverId);
if(b) {
System.out.println("司机:"+driverId+" 抢单成功");
}else {
System.out.println("司机:"+driverId+" 抢单失败");
}
} finally {
/**
* 下面代码避免释放别人的锁,通过key对应的value值来判断是否为自己的锁
*/
if((driverId+"").equals(stringRedisTemplate.opsForValue().get(lock.intern()))) {
stringRedisTemplate.delete(lock.intern());
}
}
return null;
}
}
4:Redis解决方案-redisson
1:Redisson 一个用来进行分布式锁的工具类
目前基于Redis实现的分布式锁常用的框架是Redisson,它的使用比较简单,在项目中引入Redisson的依赖,然后基于Redis实现分布式锁的加锁与释放锁,
Redisson 的加锁机制:线程1去获取锁,获取成功: 执行lua脚本,保存数据到redis数据库。线程2也去获取锁,获取失败: 一直通过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。
Redisson 的还有一个重要的续约机制,只要客户端一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端还持有锁key,那么就会不断的延长锁key的生存时间。
Redisson 加锁工作流程如下:
2:导入redisson依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.8</version>
</dependency>
3:设置RedissonClient
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("127.0.0.1:6379").setDatabase(0);
return Redisson.create(config);
}
4:业务使用
@Service
public class GrabRedisRedissonServiceImpl implements GrabService {
@Autowired
RedissonClient redissonClient;
@Autowired
OrderService orderService;
@Override
public String grabOrder(int orderId , int driverId){
//生成key
String lock = "order_"+(orderId+"");
RLock rlock = redissonClient.getLock(lock.intern());
try {
// 此代码默认 设置key 超时时间30秒,过10秒,再延时
rlock.lock();
System.out.println("司机:"+driverId+" 执行抢单逻辑");
boolean b = orderService.grab(orderId, driverId);
if(b) {
System.out.println("司机:"+driverId+" 抢单成功");
}else {
System.out.println("司机:"+driverId+" 抢单失败");
}
} finally {
rlock.unlock();
}
return null;
}
}
5:缺点
- 客户端A从master获取到锁
- 在master将锁同步到slave之前,master宕掉了。
- slave节点被晋级为master节点
- 客户端B从新的master获取到锁
- 而客户端A也以为自己成功加了锁,此时就会导致多个客户端对一个分布式锁完成了加锁。这时就会导致各种脏数据的产生