分布式锁最经典的例子应该是分布式的电商系统里对商品的库存进行加锁了,举个简单的例子,单机时代,我们可能就一台机器,这时候也可能需要锁,为啥呢,多线程啊,所以还是有锁,比如最简单的 synchronize 到了分布式系统,synchronize 已经不够用了。因为不只有一台机器啊,即使 服务器 A 加锁了,服务器 B 可能依旧跑进去了,比如两个人连接到了两台服务器A、B,然后买同一件商品,即使有 synchronize 第一个人完全可以在 A 下单,第二个人在 B 下单,synchronize 只能保证当前进程,当有多个进程时,就废了。
分布式锁就是为了解决这个问题,假设你有100台机器,无论你在那台机器下单,相应商品的库存都会先锁死。你可以这样理解,无论那台服务器,下单前都需要先获得对应商品的锁,没有这个锁就无法下单,这个锁便是分布式锁,它能锁住分布式系统,就这么简单。我今天要说的是 基于 redis 实现分布式锁,可能你会觉得好奇,网上相应文章太多了,我干嘛还写?实话告诉你吧,那些文章早就过期了,不然,我写干嘛。哈哈哈。
基于 redis 的 分布式锁
我先废话下,分布式锁,不是指这个锁是分布式的,而是它可以锁住分布式系统,如果锁是分布式的,我就不懂是否能锁住分布式系统了,没玩过。好了,show you the code
private boolean success(Boolean value) { return value != null && value; } /** * 给 key 加锁 * @param key 要加锁的 key * @return true 如果成功 否则 false */ public boolean lock(String key, int time) { return success(redisTemplate.opsForValue().setIfAbsent(key, "lock", time, TimeUnit.SECONDS)); } /** * 解锁 key * @param key 要解锁的 key */ public void unlock(String key) { if (key == null || key.isEmpty()) return; redisTemplate.opsForValue().getOperations().delete(key); }
没错,就这么简单,传入你的商品 id,当然,你也可以传入 一个前缀,表示是商品,以避免与别的冲突,自行考虑。然后在指定的 秒 之后就会失效,即自动解锁了,你也可以手动解锁,而且手动解锁是必须的,否则商品会一直被锁死,直到超时。
其原理和网上绝大部分使用 redis 实现分布式锁的一样,redis 单线程,其次,如果 key 不存在则会存入并返回成功,否则返回失败,跟网上大部分教程不一样的是,之前版本的 redis 不能在存入数据的时候指定 过期时间,而是需要之后手动设置,这有什么问题呢?简单的说,你可能数据存入成功了,但是设置超时的时候挂了,然后锁死了,这是两步操作不是一步,你可以理解为是两步原子操作,但是现在支持了,自然也不需要那么复杂了。就这么简单啊。
其中 setIfAbsent 是在 spring-data-redis 2.1 版本之后新增的。具体看 spring 文档 而 Redis 自 2.6.12 版本开始支持 set 方法设置过期时间,具体看 redis 文档
如果 redis 版本低于 2.6.12 或者 spring-data-redis 版本低于 2.1,而又不想更新版本,可以使用下面的代码:
private boolean success(Boolean value) { return value != null && value; } public Boolean lock(String key, long timeout) { return redisTemplate.execute((RedisCallback<Boolean>) connection -> { JedisCommands commands = (JedisCommands)connection.getNativeConnection(); String result = commands.set(key, "lock", "NX", "PX", TimeUnit.SECONDS.toMillis(timeout)); return "OK".equals(result); }); } public long unlock(String key) { if (key == null || key.isEmpty()) return 0; return redisTemplate.execute((RedisCallback<Long>) connection -> connection.del(key.getBytes())); }