import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Component; import com.alibaba.fastjson.JSON; @Component public class RedisUtil { @Autowired private RedisTemplate<String, String> template; /** * * @param key * @param value * @param timeOut 秒 */ public void setValue(final String key,Object value,long timeOut) { ValueOperations<String, String> ops = this.template.opsForValue(); ops.set(key, JSON.toJSONString(value), timeOut, TimeUnit.SECONDS); } public void removeKey(final String key) { this.template.delete(key); } /** * 更新过期时间 * @param key * @param timeOut (单位,秒) */ public void expire(final String key,long timeOut) { template.expire(key, timeOut, TimeUnit.SECONDS); } public boolean getLock(String key) { String value = String.format("%s", System.currentTimeMillis()+90*1000); if(template.opsForValue().setIfAbsent(key, value)) { return true; } //currentValue=A 这两个线程的value都是B 其中一个线程拿到锁 String currentValue = template.opsForValue().get(key); //如果锁过期 if (!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) { //获取上一个锁的时间 String oldValue = template.opsForValue().getAndSet(key, value); if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) { return true; } } return false; } /** * 解锁 * @param key * @param value */ public void removeLock(String key) { template.opsForValue().getOperations().delete(key); } public <T> T getValue(final String key,Class<T> clz) { ValueOperations<String, String> ops = this.template.opsForValue(); String jsonStr = ops.get(key); if (jsonStr != null) { T t = JSON.parseObject(jsonStr, clz); return t; } return null; } public boolean hasKey(String key){ return template.hasKey(key); } }
测试类
import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import com.thgames.exception.BusinessException; import com.thgames.proxy.config.RedisUtil; @RunWith(SpringRunner.class) @SpringBootTest(classes=GiftApplication.class) @ActiveProfiles("dev") public class GameProxyApplicationTests2 { @Autowired private RedisUtil redisUtil; @Test public void skipTest() throws BusinessException { for(int i=0;i<20;i++) { new Thread() { @Override public void run() { testlock("uid-a", "prefix_a"); } }.start(); } try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } public void testlock(String uid,String prefix) { boolean flag = false; try { if(flag=redisUtil.getLock(uid+prefix)) { Thread.sleep(2000); System.out.println(Thread.currentThread().getName()+" getLock success"); } else { System.out.println(Thread.currentThread().getName()+" not getLock"); } } catch (Exception e) { e.printStackTrace(); } finally { if(flag)redisUtil.removeLock(uid+prefix); } } }
运行结果
Thread-5 not getLock Thread-8 not getLock Thread-4 not getLock Thread-23 not getLock Thread-14 not getLock Thread-18 not getLock Thread-7 not getLock Thread-11 not getLock Thread-15 not getLock Thread-19 not getLock Thread-10 not getLock Thread-6 not getLock Thread-22 not getLock Thread-9 not getLock Thread-17 not getLock Thread-16 not getLock Thread-12 not getLock Thread-20 not getLock Thread-13 not getLock Thread-21 getLock success
原理,redis使用的是单线程模型,setnx命令和getset命令保证原子性,线程安全,低版本setnx没有过期时间参数,为了避免死锁,所以得把过期时间当作value传过去,然后再用getset返回的旧值与当前值比较,相等则获得锁,类似CAS的原理