使用redis,如果某个线程没有抢到锁,就让他睡眠一会儿。
误删:假设线程1获得了lock,过期时间是10s,如果第10秒redis执行了过期,删除了lock,而线程1下订单还需要两秒钟执行,那么第二个线程就会获得锁,第12s的时候,线程1删除锁lock,线程2失去锁lock,另外的线程3获取锁。也就是说,过期时间不应该小于业务执行时间,因此需要在下面的代码中引入守护进程。
解锁时,需要看设置时的value与此时redis的value是否相同,如果不判断,有可能出现超卖,也就是说,上面的"第12s的时候,线程1删除锁lock,线程2失去锁lock,另外的线程3获取了锁" ,线程3获取了不应该得到的锁,有可能超卖(想想卖票的例子),如果不设置守护进程,线程1也不应该删去线程2的锁,因此解锁时需要额外判断。
下面是加锁的过程,守护进程在其中实现:
@Component
public class RedisLock {
@Autowired
private JedisPool jedisPool;
private static final String key = "lock";
private ThreadLocal<String> threadLocal = new ThreadLocal<>();
private static AtomicBoolean isHappened = new AtomicBoolean(true);
//加锁
public void lock() {
while (true) {
boolean b = tryLock(); //尝试加锁
if (b) {
//拿到了锁
return;
}
try {
// 等待一段时间,之后再去获取锁,也可以设置跳出条件
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//尝试加锁,被lock调用
public boolean tryLock() {
SetParams setParams = new SetParams();
setParams.ex(2); //过期时间 2s
setParams.nx(); // setNx的方式去设置,如果key已经存在,返回null
String s = UUID.randomUUID().toString();
Jedis resource = jedisPool.getResource();
String lock = resource.set(key, s, setParams);
resource.close();
if ("OK".equals(lock)) {
//拿到了锁,并且随即保存一个值
threadLocal.set(s);
// 设置守护进程,不断延长锁的过期时间,
if (isHappened.get()) {
ThreadUtil.newThread(new MyRUnble(jedisPool)).start();
isHappened.set(false);
}
return true;
}
return false;
}
// 守护进程,保证在持有者执行任务的时候,redis中的值不会过期。
static class MyRUnble implements Runnable {
private JedisPool jedisPool;
public MyRUnble(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
@Override
public void run() {
Jedis jedis = jedisPool.getResource();
while (true) {
Long ttl = jedis.ttl(key);
if (ttl != null && ttl > 0) {
jedis.expire(key, (int) (ttl + 1));
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
下面是解锁的代码:请放入RedisLock 中。
//解锁,注意查看resource.eval方法参数的意思。
public void unlock() throws Exception {
String script = "if redis.call(\"get\",KEYS[1])==ARGV[1] then\n"
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
Jedis resource = jedisPool.getResource();
//正确的解锁方式
Object eval = resource.eval(script, Arrays.asList(key), Arrays.asList(threadLocal.get()));
if (Integer.valueOf(eval.toString()) == 0) {
resource.close();
throw new Exception("解锁失败");
}
resource.close();
}
}
配置类()
@Configuration
public class SpringConfig { @Bean
public JedisPool jedisPool(){
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(101);
jedisPoolConfig.setMaxIdle(1000);
jedisPoolConfig.setMinIdle(1);
jedisPoolConfig.setMaxWaitMillis(2000);
jedisPoolConfig.setTestOnBorrow(true);
jedisPoolConfig.setTestOnReturn(true);
JedisPool jedisPool = new JedisPool(jedisPoolConfig,"xxx.xx.xx.xx",6379);
return jedisPool;
}
}
测试:countDownLatch用来控制主线程的结束,如果不设置,程序会很快结束。
private int count = 6;
@Autowired
@Qualifier("redisLock")
private RedisLock lock;
private CountDownLatch countDownLatch = new CountDownLatch(6);
@Test
public void Test() throws InterruptedException {
TicketsRunBle ticketsRunBle = new TicketsRunBle();
Thread thread1 = new Thread(ticketsRunBle, "窗口1");
Thread thread2 = new Thread(ticketsRunBle, "窗口2");
Thread thread3 = new Thread(ticketsRunBle, "窗口3");
thread1.start();
thread2.start();
thread3.start();
countDownLatch.await();
Thread.currentThread().interrupt();
}
public class TicketsRunBle implements Runnable {
@Override
public void run() {
while (count > 0) {
lock.lock();
try {
if (count > 0) {
Thread.sleep(ThreadLocalRandom.current().nextInt(100, 200));
countDownLatch.countDown();
System.out.println(Thread.currentThread().getName() + "售出第" + count-- + "张票");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
lock.unlock();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}