由于使用for update来实现分布式锁在高并发时,会造成数据库压力过大,所以可以使用redis的setNX特性来实现分布式锁。

首先,分布式锁和我们平常讲到的锁原理基本一样,目的就是确保在多个线程并发时,只有一个线程在同一刻操作这个业务或者说方法、变量。
在一个进程中,也就是一个jvm或者说应用中,我们很容易去处理控制,在java.util并发包中已经为我们提供了这些方法去加锁,比如synchronized关键字或者Lock锁,都可以处理。
但是如果在分布式环境下,要保证多个线程同时只有1个能访问某个资源,就需要用到分布式锁。这里我们将介绍用Redis的setnx命令来实现分布式锁。
其实目前通常所说的setnx命令,并非单指redis的setnx key value这条命令,这条命令可能会在后期redis版本中删除。
一般代指redis中对set命令加上nx参数进行使用,set这个命令,目前已经支持这么多参数可选:

  1. SET key value [EX seconds] [PX milliseconds] [NX|XX]

从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改:

  • EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
  • PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
  • NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
  • XX :只在键已经存在时,才对键进行设置操作。


image.png

实战

根据上述原理,编写分布式锁

  1. @Slf4j
  2. public class RedisLock implements AutoCloseable {
  3. private RedisTemplate redisTemplate;
  4. private String key;
  5. private String value;
  6. //单位:秒
  7. private int expireTime;
  8. public RedisLock(RedisTemplate redisTemplate,String key,int expireTime){
  9. this.redisTemplate = redisTemplate;
  10. this.key = key;
  11. this.expireTime=expireTime;
  12. this.value = UUID.randomUUID().toString();
  13. }
  14. /**
  15. * 获取分布式锁
  16. * @return
  17. */
  18. public boolean getLock(){
  19. RedisCallback<Boolean> redisCallback = connection -> {
  20. //设置NX
  21. RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
  22. //设置过期时间
  23. Expiration expiration = Expiration.seconds(expireTime);
  24. //序列化key
  25. byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
  26. //序列化value
  27. byte[] redisValue = redisTemplate.getValueSerializer().serialize(value);
  28. //执行setnx操作
  29. Boolean result = connection.set(redisKey, redisValue, expiration, setOption);
  30. return result;
  31. };
  32. //获取分布式锁
  33. Boolean lock = (Boolean)redisTemplate.execute(redisCallback);
  34. return lock;
  35. }
  36. public boolean unLock() {
  37. String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
  38. " return redis.call(\"del\",KEYS[1])\n" +
  39. "else\n" +
  40. " return 0\n" +
  41. "end";
  42. RedisScript<Boolean> redisScript = RedisScript.of(script,Boolean.class);
  43. List<String> keys = Arrays.asList(key);
  44. Boolean result = (Boolean)redisTemplate.execute(redisScript, keys, value);
  45. log.info("释放锁的结果:"+result);
  46. return result;
  47. }
  48. @Override
  49. public void close() throws Exception {
  50. unLock();
  51. }
  52. }
  1. @RequestMapping("redisLock")
  2. public String redisLock(){
  3. log.info("我进入了方法!");
  4. try (RedisLock redisLock = new RedisLock(redisTemplate,"redisKey",30)){
  5. if (redisLock.getLock()) {
  6. log.info("我进入了锁!!");
  7. Thread.sleep(15000);
  8. }
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. } catch (Exception e) {
  12. e.printStackTrace();
  13. }
  14. log.info("方法执行完成");
  15. return "方法执行完成";
  16. }

通过postman测试一下,最终结果:
image.png
image.png

通过定时任务(spring-task)集群部署校验编写的分布式锁

image.png
说明:哪个服务获取锁,就哪个服务执行任务A,来解决任务A重复执行的问题。

  1. @Service
  2. @Slf4j
  3. public class SchedulerService {
  4. @Autowired
  5. private RedisTemplate redisTemplate;
  6. @Scheduled(cron = "0/5 * * * * ?")
  7. public void sendSms(){
  8. try(RedisLock redisLock = new RedisLock(redisTemplate,"autoSms",30)) {
  9. if (redisLock.getLock()){
  10. log.info("向138xxxxxxxx发送短信!");
  11. }
  12. } catch (Exception e) {
  13. e.printStackTrace();
  14. }
  15. }
  16. }