一、基础知识
1、锁的种类
- 单机版同一个JVM虚拟机内,synchronized 或者 Lock 接口。
- 分布式不同个JVM虚拟机内,单机的线程锁机制不再起作用,资源类在不同的服务器之间共享了。
2、分布式锁需要具备的条件和刚需
- 独占性:OnlyOne,任何时刻只能有且仅有一个线程持有
- 高可用:若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况
- 防死锁:杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底终止跳出方案
- 不乱抢:防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放
- 重入性:同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁
3、分布式锁
多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)。
二、案例v1.1
1、使用场景
多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)。
2、基础代码
@RestController public class DemoController { @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired public static final String KEY = "MY_REDIS"; @GetMapping("/test") public void test() { String result = stringRedisTemplate.opsForValue().get(KEY); // 执行业务代码 // ... } }
三、v1.2
1、v1.1问题
没有加锁,不具备独占性。可以使用synchronized或Lock来实现,建议使用Lock,可以自由设定超时时间,sync只能死等。
2、升级
@RestController public class DemoController { @Autowired private StringRedisTemplate stringRedisTemplate; private final Lock lock = new ReentrantLock(); @Autowired public static final String KEY = "MY_REDIS"; @GetMapping("/test") public void test() { if (lock.tryLock()) { try { // 执行业务代码 // ... }finally { lock.unlock(); } } } }
四、v1.3
1、v1.2问题
只能在单机加锁生效,无法使用在分布式系统上。需要一个让所有进程都能访问到的锁来实现。
2、升级
public class DemoController { @Autowired private StringRedisTemplate stringRedisTemplate; @GetMapping("/test") public void test() { String key = "MY_REDIS"; String value = UUID.randomUUID().toString() + Thread.currentThread().getName(); Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value); if (!flagLock) { System.out.println("抢夺锁失败"); } // 执行业务代码 // ... } }
五、v1.4
1、v1.3问题
只考虑了正常流程,如果出异常,可能无法释放锁,必须要在代码层面finally释放锁。
2、升级
@RestController public class DemoController { @Autowired private StringRedisTemplate stringRedisTemplate; @GetMapping("/test") public void test() { String key = "MY_REDIS"; String value = UUID.randomUUID().toString() + Thread.currentThread().getName(); try { Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value); if (!flagLock) { System.out.println("抢夺锁失败"); } // 执行业务代码 // ... } finally { stringRedisTemplate.delete(key); } } }
六、v1.5
1、v1.4问题
如果代码没走带finally这步,比如部署了微服务Jar包的机器挂了,没办法保证解锁,这个key没有被删除,需要加入一个过期时间限定key。
2、升级
Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value); stringRedisTemplate.expire(key,10L, TimeUnit.SECONDS);
七、v1.6
1、v1.5问题
设置key + 过期时间分开了,必须要合并成一行具备原子性。可能代码只走到设置key的这步就挂了。
2、升级
// 同步设置锁和过期时间
Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value, 10L, TimeUnit.SECONDS);
八、v1.7
1、v1.6问题
可能删除了别人的锁。比如:A线程执行业务代码需要15秒,锁的过期时间只有10秒,这时候B在第11秒的时候进来了。当第15秒时A执行完业务代码后,会直接删除B的锁。
2、升级
@RestController public class DemoController { @Autowired private StringRedisTemplate stringRedisTemplate; @GetMapping("/test") public void test() { String key = "MY_REDIS"; String value = UUID.randomUUID().toString() + Thread.currentThread().getName(); try { // 同步设置锁和过期时间 Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value, 10L, TimeUnit.SECONDS); if (!flagLock) { System.out.println("抢夺锁失败"); } // 执行业务代码 // ... } finally { // 判断是否当前线程加的锁 if (stringRedisTemplate.opsForValue().get(key).equals(value)) { stringRedisTemplate.delete(key); } } } }
九、v1.8
1、v1.7问题
判断加锁与解锁是不是同一个客户端,finally块的判断+del删除操作不是原子性的,Redis调用Lua脚本通过eval命令保证代码执行的原子性。
2、升级
(1)RedisUtils
public class RedisUtils { private static JedisPool jedisPool; static { JedisPoolConfig jedisPoolConfig=new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(20); jedisPoolConfig.setMaxIdle(10); jedisPool=new JedisPool(jedisPoolConfig,"192.168.111.147",6379); } public static Jedis getJedis() throws Exception { if(null!=jedisPool){ return jedisPool.getResource(); } throw new Exception("Jedispool was not init"); } }
(2)升级代码
@RestController public class DemoController { @Autowired private StringRedisTemplate stringRedisTemplate; @GetMapping("/test") public void test() throws Exception { String key = "MY_REDIS"; String value = UUID.randomUUID().toString() + Thread.currentThread().getName(); try { // 同步设置锁和过期时间 Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value, 10L, TimeUnit.SECONDS); if (!flagLock) { System.out.println("抢夺锁失败"); } // 执行业务代码 // ... } finally { Jedis jedis = RedisUtils.getJedis(); String script = "if redis.call('get', KEYS[1]) == ARGV[1] " + "then " + "return redis.call('del', KEYS[1]) " + "else " + " return 0 " + "end"; try { Object result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value)); if ("1".equals(result.toString())) { System.out.println("------del REDIS_LOCK_KEY success"); } else { System.out.println("------del REDIS_LOCK_KEY error"); } } finally { if (null != jedis) { jedis.close(); } } } } }
十、v1.9
1、v1.8问题
只能基于单个Redis节点实现分布式锁,无法使用在多个redis节点中实现。官方推荐RedLock之Redisson,Redisson中的 “看门狗” 会自动实现锁的续期。
2、升级
(1)RedisConfig
@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) { RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(lettuceConnectionFactory); // 设置key序列号方式String redisTemplate.setKeySerializer(new StringRedisSerializer()); // 设置value的序列化方式json redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.afterPropertiesSet(); return redisTemplate; } @Bean public Redisson redisson() { Config config = new Config(); config.useSingleServer().setAddress("redis://192.168.1.108:6379").setDatabase(0); return (Redisson) Redisson.create(config); } }
(2)升级代码
@RestController public class DemoController { @Autowired private Redisson redisson; @GetMapping("/test") public void test() throws Exception { String key = "MY_REDIS"; RLock redissonLock = redisson.getLock(key); redissonLock.lock(); try { // 执行业务代码 // ... } finally { redissonLock.unlock(); } } }
3、完善
高并发下会报异常
升级
@RestController public class DemoController { @Autowired private Redisson redisson; @GetMapping("/test") public void test() throws Exception { String key = "MY_REDIS"; RLock redissonLock = redisson.getLock(key); redissonLock.lock(); try { // 执行业务代码 // ... } finally { if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) { redissonLock.unlock(); } } } }