1.redis客户端 jedis和lettuce
网上大部分是基于jedis的分布式锁 jedis是多线程下不是线程安全的 lettuce是基于netty线程安全性能高,
springboot2.0都采用lettuce,所以分布式锁最好基于lettuce
2.分布式锁原理
采用原子操作如下:
如果不存在key就设置这个key 存在这个key就阻塞住等待锁key自动或手动释放
3.多线程或多进程下存在多问题
当同步代码块阻塞时间过长,导致代码还没执行完,但是此时超过了持有锁的超时时间(key的过期时间)锁会自动被释放,之后同步代码块执行完毕后又会去执行释放锁的操作(类似unlock)。在锁超时之后,未手动释放锁之前如果其他进程重新设了一把锁,当前进程之后的手动释放又会把其他进程设置的新锁释放掉,所以在手动释放锁之前一定要核对value的值是否和设置锁的时候一致的,这样就不会把其他进程的锁给释放掉。
置key时 value一定要随机而且唯一(就像uuid)原因如上;
4.贴代码
@Component
@Slf4j
public class RedisLock {
@Autowired
private RedisTemplate redisTemplate;
/**
* 存储当前线程 设置锁对应的value 用来释放锁时进行校验
*/
private final ThreadLocal<String> lockValue = new ThreadLocal<>();
private final String keyLock = "lock";
/**
* 获取锁 (带超时时间)
*
* @param timeout
* @param expireTime
* @return
* @throws InterruptedException
*/
public boolean tryLockWithTimeout(Integer timeout, Integer expireTime) {
String value = UUID.randomUUID().toString();
//获取未来过期时间点
long invalidTime = System.currentTimeMillis() + timeout * 1000;
boolean flag = false;
while (System.currentTimeMillis() < invalidTime) {
flag = tryLock(keyLock, value, expireTime);
if (flag) {
break;
} else {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
log.error(e.toString());
}
}
}
if (false == flag) {
log.error("竞争锁失败");
}
return flag;
}
/**
* 获取锁 (一直抢到为止) 阻塞锁
*
* @param expireTime 锁失效时间
* @return
*/
public boolean blockLock(Long expireTime) throws InterruptedException {
String value = UUID.randomUUID().toString();
boolean flag = false;
while (true) {
flag = tryLock(keyLock, value, expireTime);
if (flag) {
break;
} else {
Thread.sleep(10);
}
}
if (false == flag) {
log.error("竞争锁失败");
}
return flag;
}
/**
* 释放锁
*
* @return
*/
public boolean unlock() {
//重试次数
Integer retryTimes = 3;
//先拿到当前锁对应的值(理解为版本号)
String value =(String)redisTemplate.opsForValue().get(keyLock);
Boolean flag = false;
//如果相等 锁应该未被释放 不相等则锁已过期自动释放此时如果有锁应为其他进程的锁
String s = lockValue.get();
if (lockValue.get().equals(value)) {
while (retryTimes >0) {
try {
flag = redisTemplate.delete(keyLock);
break;
} catch (Exception e) {
log.error("释放锁异常");
retryTimes--;
}
}
}else {
}
return flag;
}
/**
* 设置锁 如果不存在key设置成功,
*
* @param key redis key
* @param value redis value
* @param expireTime key过期时间 好吗
* @return
*/
private boolean tryLock(String key, String value, long expireTime) {
try {
String result = (String) redisTemplate.execute((RedisCallback) connection -> {
try {
String redisResult = null;
//获取连接
Object nativeConnection = connection.getNativeConnection();
//获取序列化工具 (不使用这个工具序列化 redistemplate会取不到值)
RedisSerializer keySerializer = redisTemplate.getKeySerializer();
//序列化
byte[] keyByte = keySerializer.serialize(key);
byte[] valueByte = keySerializer.serialize(value);
//单机模式
if (nativeConnection instanceof RedisAsyncCommands) {
RedisAsyncCommands commands = (RedisAsyncCommands) nativeConnection;
redisResult = commands.getStatefulConnection()
.sync()
.set(keyByte, valueByte, SetArgs.Builder.nx().ex(expireTime));
} else if (nativeConnection instanceof RedisAdvancedClusterAsyncCommands) {
//集群模式
RedisAdvancedClusterAsyncCommands clusterAsyncCommands = (RedisAdvancedClusterAsyncCommands) nativeConnection;
redisResult = clusterAsyncCommands.getStatefulConnection()
.sync()
.set(keyByte, valueByte, SetArgs.Builder.nx().ex(expireTime));
} else {
log.error("REDISLIBMISTCH");
}
return redisResult;
} catch (Exception e) {
log.error("Failed to lock, closing connection");
connection.close();
return "";
}
});
//如果成功设置锁 则将value逸出
boolean eq = "OK".equals(result);
if (eq) {
lockValue.set(value);
}
return eq;
} catch (Exception e) {
log.error("设置锁异常");
return false;
}
}
}
测试代码:
为了模拟线程不安全 用一个Integer 变量多线程累加 累加不是原子操作而且需要依赖于当前状态。
1.写一个需要执行的任务(加锁同步)
public class task implements Runnable {
//没用内置锁不能保证内存的可见性 为了保证可见性加个voliatile
private static volatile Integer times = 0;
public static void doTask() {
RedisLock redisLock = (RedisLock) SpringUtil.getBean("redisLock");
boolean b = redisLock.tryLockWithTimeout(10, 100);
if(b==false){
return;
}
while (times <= 20) {
System.out.println(times++ + " : id:" + Thread.currentThread().getId());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
redisLock.unlock();
}
@Override
public void run() {
task.doTask();
}
}
不加锁同步任务
public class task implements Runnable {
private static volatile Integer times = 0;
public static void doTask() {
while (times <= 20) {
System.out.println(times++ + " : id:" + Thread.currentThread().getId());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
task.doTask();
}
}
测试test:
@RunWith(SpringRunner.class)
@SpringBootTest
public class LockdemoApplicationTests {
@Autowired
private ApplicationContext applicationContext;
@Test
public void contextLoads() {
ThreadPoolExecutor executor=new ThreadPoolExecutor(3,3,50, TimeUnit.SECONDS,new LinkedBlockingQueue<>(10));
executor.execute(new task());
executor.execute(new task());
executor.execute(new task());
while (true){
Thread.sleep(1000);
}
}
}
测试结果:
1.不加锁
总共三个线程在执行 id 分别为 16,17,18
累加过程中出现问题。
2.加分布式锁的累加
只有16号线程在执行 其余线程被阻塞,阻塞超过了指定的时间(这里我设置的是10秒) 就会放弃竞争。
从结果来看是安全的累加 分布式锁达到效果
更新 写了一个优化版 并用aop做了封装
https://blog.csdn.net/qq_36559868/article/details/102462807