单机版redis解决方案(redis集群还在学)
此博客解决方案适用于绝大部分业务场景,不能容忍任何一点的race condition(如钱相关的业务)并不适用
原理分析参考另一篇博客
什么分布式锁?
本地锁:在多个线程中,保证只有一个线程执行(线程安全的问题)
分布锁:在分布式中,保证只有一个jvm执行(多个jvm线程安全问题)
如果我们服务器是集群的时候,定时任务可能会重复执行 可以采用分布式锁解决
分布式锁解决方案:
- 基于数据库方式实现
- 基于Zk方式实现 采用临时节点+事件通知
- 基于Redis方式实现 setnx 方式
解决分布式锁核心思路:
- 获取锁
多个不同的jvm 同时创建一个相同的标记(全局唯一的) 只要谁能够创建成功谁就能够获取锁 - 释放锁
释放该全局唯一的标记,其他的jvm重新进入到获取锁资源。 - 超时锁(没有获取锁、已经获取锁)
等待获取锁的超时时间
已经获取到锁 锁的有效期 5s
分析:基于Redis实现分布式锁思路
获取锁
多个不同的jvm 同时创建一个相同的标记使用Setnx命令,因为Rediskey必须保证是唯一的,只要谁能够创建成功谁就能够获取锁
Set命令的时候:如果key不存在则创建,如果key已经存在则修改原值;
SetNx命令: 如果key不存在则创建 返回1,如果已经存在则不执行任何操作返回0
1 不存在创建成功 0 已经存在 不执行任何操作。释放锁
对我们的redis的key设置一个有效期(或者是主动删除该key)可以灵活的自动的释放该全局唯一的标记,其他的jvm重新进入到获取锁资源。超时锁(没有获取锁、已经获取锁)
等待获取锁的超时时间
已经获取到锁 锁的有效期 5s
分析基于Zk实现分布式锁思路
获取锁
多个不同的jvm在zk集群上创建一个相同的全局唯一的临时路径,只要谁能够创建成功谁就能够获取到锁。
分析:临时节点对我们节点设置有效期释放锁
人为主动删除该节点或者使用Session有效期超时锁(没有获取锁、已经获取锁)
等待获取锁的超时时间
已经获取到锁 锁的有效期 5s
代码实现
- maven依赖
<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>
- redis工具类配置redis
public class RedisUtil {
//protected static Logger logger = Logger.getLogger(RedisUtil.class);
private static String IP = "你自己的redis IP";
//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);
//有密码用下面这种构造方法
// 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;
}
}
- 分布式锁实现工具类
public class RedisLock {
private static int lockSuccss = 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;
try {
//当前系统时间小于endTime说明获取锁没有超时
while (System.currentTimeMillis()<endTime){
String lockValue = UUID.randomUUID().toString();
// 当多个不同的jvm同时创建一个相同的rediskey 只要谁能够创建成功谁就能够获取锁
if(jedis.setnx(lockKey,lockValue)==1){
jedis.expire(lockKey,timeOut/1000);
return lockValue;
}
}
}catch (Exception e){
e.printStackTrace();
}
finally {
if(jedis!=null){
jedis.close();
}
}
return null;
}
/**
* 释放锁
* @return
*/
public boolean unLock(String lockKey,String lockValue){
//获取Redis连接
Jedis jedis=RedisUtil.getJedis();
try {
// 判断获取锁的时候保证自己删除自己(防止空指针将lockValue写前面,因为redis可能获取到空值)
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;
}
}
- 测试类
public class TestService {
private static final String LOCKKEY = "lock";
public static void service() {
// 1.获取锁
RedisLock mayiktRedisLock = new RedisLock();
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();
}
/***
*
* 尝试获取锁为什么次数限制?
* 如果我们业务逻辑5s 内没有执行完毕呢?
*
* 分场景:
* 1.锁的超时时间根据业务场景来预估
* 2.可以自己延迟锁的时间
* 3.在提交事务的时候检查锁是否已经超时 如果已经超时则回滚(手动回滚)否则提交。
*
* 仅限于单机版本
*/
}
From 蚂蚁课堂