075:基于Redis实现分布式锁
1 Redis中为什么没有回滚的事务
课程内容:
1.分布式锁常见有哪些解决方案?
2.基于Redis实现分布式锁的思路
3.基于Redis实现分布式锁超时控制
4.对比与Zookeeper实现分布式锁的区别
为什么redis不支持回滚事务?
Mysql开启事务,对该行数据上行锁,commit数据提交,回滚对事务和行锁都会撤销;redis没有对数据上锁,没必要回滚事务,直接取消事务(不提交事务)即可。
2 什么是分布式锁?应用场景有哪些
什么是分布式锁? 举个场景
本地锁:在多个线程中,保证只有一个线程执行(线程安全的问题)
分布锁:在分布式中,保证只有一个jvm执行(多个jvm线程安全问题)
如果当前服务器是集群的时候,定时任务可能会重复执行
可以采用分布式锁解决,谁抢到锁谁执行任务。
3 实现分布式锁核心的思路
分布式锁实现方案:
- 基于数据库方式实现
- 基于Zk方式实现 采用临时节点+事件通知
- 基于Redis方式实现 setnx 命令方式
实现分布式锁核心思路:
1.获取锁
多个不同的jvm 同时创建一个相同的标记(全局唯一的),只要谁能够创建成功谁就能够获取锁;
2.释放锁
释放该全局唯一的标记,其他的jvm重新进入到获取锁资源;
3.超时锁(没有获取锁、已经获取锁)
没有获取到锁:等待获取锁的超时时间,jvm循环重试不能一直等待下去;
已经获取到锁:设置锁的有效期,到期自动释放锁,防止死锁;
4 基于Redis实现分布式锁的思路
分析:基于Redis实现分布式锁思路
1.获取锁
多个不同的jvm 同时创建一个相同的标记,使用Setnx命令,因为Redis key必须保证是唯一的,只要谁能够创建成功谁就能够获取锁;
Set命令的时候:如果key不存在则创建,如果key已经存在则修改原值;
SetNx命令: 如果key不存在则创建返回1;如果已经存在则不执行任何操作返回0;
2.释放锁
对redis的key设置一个有效期(或者是主动删除该key)可以灵活的自动释放该全局唯一的标记,其他的jvm重新进入到获取锁资源;
3.超时锁(没有获取锁、已经获取锁)
没有获取到锁:等待获取锁的超时时间,jvm循环重试不能一直等待下去;
已经获取到锁:设置锁的有效期,到期自动释放锁,防止死锁;
5 基于Redis实现获取锁代码实现
Maven依赖
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
</dependencies>
RedisUtil
public class RedisUtil {
//protected static Logger logger = Logger.getLogger(RedisUtil.class);
private static String IP = "192.168.0.54";
//Redis的端口号
private static int PORT = 6379;
//可用连接实例的最大数目,默认值为8;
//如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。
private static int MAX_ACTIVE = 100;
//控制一个pool最多有多少个状态为idle(空闲的)的jedis实例,默认值也是8。
private static int MAX_IDLE = 20;
//等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException;
private static int MAX_WAIT = 3000;
private static int TIMEOUT = 3000;
//在borrow一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的;
private static boolean TEST_ON_BORROW = true;
//在return给pool时,是否提前进行validate操作;
private static boolean TEST_ON_RETURN = true;
private static JedisPool jedisPool = null;
/**
* redis过期时间,以秒为单位
*/
public final static int EXRP_HOUR = 60 * 60; //一小时
public final static int EXRP_DAY = 60 * 60 * 24; //一天
public final static int EXRP_MONTH = 60 * 60 * 24 * 30; //一个月
/**
* 初始化Redis连接池
*/
private static void initialPool() {
try {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(MAX_ACTIVE);
config.setMaxIdle(MAX_IDLE);
config.setMaxWaitMillis(MAX_WAIT);
config.setTestOnBorrow(TEST_ON_BORROW);
jedisPool = new JedisPool(config, IP, PORT, TIMEOUT, "123456");
} catch (Exception e) {
//logger.error("First create JedisPool error : "+e);
e.getMessage();
}
}
/**
* 在多线程环境同步初始化
*/
private static synchronized void poolInit() {
if (jedisPool == null) {
initialPool();
}
}
/**
* 同步获取Jedis实例
*
* @return Jedis
*/
public synchronized static Jedis getJedis() {
if (jedisPool == null) {
poolInit();
}
Jedis jedis = null;
try {
if (jedisPool != null) {
jedis = jedisPool.getResource();
}
} catch (Exception e) {
e.getMessage();
// logger.error("Get jedis error : "+e);
}
return jedis;
}
/**
* 释放jedis资源
*
* @param jedis
*/
public static void returnResource(final Jedis jedis) {
if (jedis != null && jedisPool != null) {
jedisPool.returnResource(jedis);
}
}
public static Long sadd(String key, String... members) {
Jedis jedis = null;
Long res = null;
try {
jedis = getJedis();
res = jedis.sadd(key, members);
} catch (Exception e) {
//logger.error("sadd error : "+e);
e.getMessage();
}
return res;
}
}
6 基于Redis实现释放锁代码实现
public class MayiktRedisLock {
private static int LockSuccess = 1;
/**
* @param lockKey 在Redis中创建的key
* @param notLockTime 尝试获取锁的超时时间
* @return 返回lock锁成功标记值
*/
public String getLock(String lockKey, int notLockTime, int timeout) {
// 获取redis连接
Jedis jedis = RedisUtil.getJedis();
// 计算尝试获取锁的超时时间
Long endTime = System.currentTimeMillis() + notLockTime;
// 当前系统时间小于endTime,说明获取锁没有超时,继续循环,否则退出循环
while (System.currentTimeMillis() < endTime) {
String lockValue = UUID.randomUUID().toString();
// 多个不同的jvm同时创建一个相同的redis key,谁创建成功谁获取锁,返回值1
if (jedis.setnx(lockKey, lockValue) == LockSuccess) {
// 加上key有效期
jedis.expire(lockKey, timeout / 1000);
// 退出循环
return lockValue;
}
// 否则情况下继续循环重试
try {
if (jedis != null) {
jedis.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
/**
* 释放锁
*
* @param lockKey
* @param lockValue
* @return
*/
public boolean unLock(String lockKey, String lockValue) {
// 获取redis连接
Jedis jedis = RedisUtil.getJedis();
try {
// 根据lockValue确定对应的JVM
if (lockValue.equals(jedis.get(lockKey))) {
return jedis.del(lockKey) > 0 ? true : false;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
}
return false;
}
}
7 测试基于Redis实现分布式锁代码
public class OrderService {
private static final String LOCKKEY = "mayikt_lock";
public static void service() {
// 1.获取锁
MayiktRedisLock mayiktRedisLock = new MayiktRedisLock();
String lockValue = mayiktRedisLock.getLock(LOCKKEY, 5000, 5000);
if (StringUtils.isEmpty(lockValue)) {
System.out.println(Thread.currentThread().getName() + ",获取锁失败");
return;
}
// 执行正常业务逻辑
System.out.println(Thread.currentThread().getName() + ",获取锁成功,lockValue:" + lockValue);
// 3.释放锁
mayiktRedisLock.unLock(LOCKKEY, lockValue);
}
public static void main(String[] args) {
service();
}
}
运行结果:
8 在使用分布式锁如果超时了,如何处理
尝试获取锁为什么没有次数限制?
根据时间判断更合理,notLockTime,尝试获取锁的超时时间。
如果业务逻辑没有在5s内(设置的锁过期时间)执行完毕?
1.锁的超时时间根据业务场景来预估;
2.出现异常可以手动延长锁的超时时间(不建议);
3.在提交事务的时候检查锁是否已经超时,计算时间差判断是不是在锁的超时时间范围内,如果已经超时则手动回滚,否则提交。
9 zk与Redis实现分布式锁的优缺点
分析基于Zk实现分布式锁思路
1.获取锁
多个不同的jvm在zk集群上创建一个相同的全局唯一的临时路径,只要谁能够创建成功谁就能够获取到锁。
分析:临时节点相当于对节点设置有效期
2.释放锁(zk效率没有redis高)
人为主动删除该节点或者使用Session有效期
3.超时锁(没有获取锁、已经获取锁)
没有获取到锁:等待获取锁的超时时间,jvm循环重试不能一直等待下去;
已经获取到锁:设置锁的有效期,到期自动释放锁,防止死锁;
Redis也可以实现类似于zk事件通知,key过期通知。
Redis官方提供一个redis解决分布锁框架redission。