在微服务开发中,经常会因为并发带来一些麻烦的问题。最经典案例的就是秒杀减库存的问题,想要解决这个问题,那么就需要使用分布式并发锁来将资源锁住,等业务逻辑执行完了之后在把锁释放掉。
实现分布式并发锁有多种实现方式:
1、使用数据库做锁
2、使用Redis做锁
3、使用ZooKeeper做锁
可以用来做分布式锁需要满足什么条件可以自行百度,我这里介绍下在SpringBoot中如何使用redis搭配lua脚本实现分布式锁。
为什么redis可以做分布式锁:
1、redis是单线程的,所以不存在并发问题
2、可以使用redis的SETNX命令(set if not exists,即设置key的value值,若key已经存在,不做任何操作)来抢占key这把锁的资源,如:
SETNX a "a"
如果设值成功,则认为抢到了a这把锁,如果设值失败(a的值已经存在),则认为加锁失败。
聚心尚品 金刚砂海绵擦魔力擦厨房清洁用海绵去污垢除锈刷锅神器 5片装¥8.90
3、为了避免死锁,可以针对key设置过期时间。
为什么需要搭配lua脚本:
1、上述的2和3的操作是2条命令,如果2的操作执行完毕了,因为某些不可控的原因3的操作未执行线程就死掉了,这就导致了死锁的发生。
2、为了避免A线程业务处理太久导致对资源a加的锁已经自动过期了,这个时候B线程需要操作资源a,同样对a加锁,刚好A线程业务处理完毕,误解除了B对资源a加的锁,导致B的锁失效。所以我们在加锁的时候可以对加的锁的key设置一个value值当做锁的密码,这样只有加锁的线程可以使用自己知道的密码去解除这把锁,而不会发生误解锁的事情。所以我们需要在解锁的时候先判断锁是不是我们加的,如果是才能进行解锁操作。这样这里又是2个操作
3、因为上述2个原因是非原子性的操作,所以我们需要引入lua脚本,因为redis在执行lua脚本的时候是原子操作。
下面是分布式加锁和解锁的代码,可以直接复制使用:
/** * redis分布式锁工具 * 使用lua脚本实现加锁和解锁操作,保证原子性 * @author liqingcan */ @Component public class RedisLockService { @Autowired private RedisTemplate<String, String> redisTemplate; /** * 加锁的lua脚本 */ private final static RedisScript<Long> LOCK_LUA_SCRIPT = new DefaultRedisScript<>( "if redis.call(\"setnx\", KEYS[1], KEYS[2]) == 1 then return redis.call(\"expire\", KEYS[1], KEYS[3]) else return 0 end" , Long.class ); /** * 加锁失败结果 */ private final static Long LOCK_FAIL = 0L; /** * 解锁的lua脚本 */ private final static RedisScript<Long> UNLOCK_LUA_SCRIPT = new DefaultRedisScript<>( "if redis.call(\"get\",KEYS[1]) == KEYS[2] then return redis.call(\"del\",KEYS[1]) else return -1 end" , Long.class ); /** * 解锁失败结果 */ private final static Long UNLOCK_FAIL = -1L; /** * 加锁方法 * 对key加锁,value为key对应的值,expire是锁自动过期时间防止死锁 * @param key key * @param value value * @param expire 锁自动过期时间(秒) * @return */ public boolean lock(String key, String value, Long expire){ if (key == null || value == null || expire == null) { return false; } List<String> keys = Arrays.asList(key, value, expire.toString()); Long res = redisTemplate.execute(LOCK_LUA_SCRIPT, keys); return !LOCK_FAIL.equals(res); } /** * 解锁方法 * 对key解锁,只有value值等于redis中key对应的值才能解锁,避免误解锁 * @param key * @param value * @return */ public boolean unlock(String key, String value){ if (key == null || value == null) { return false; } List<String> keys = Arrays.asList(key, value); Long res = redisTemplate.execute(UNLOCK_LUA_SCRIPT, keys); return !UNLOCK_FAIL.equals(res); } }
这里有几个点解释下:
1、RedisTemplate需要指定使用RedisTemplate<String, String>,否则会导致执行lua脚本的时候发生异常。
2、加锁失败和解锁失败的标志为啥一个是0一个是-1?
加锁时,设置过期时间的命令expire失败返回的是0,成功返回1,所以判断加锁失败的标记为0
解锁时,删除key的命令del返回的是被删除的key数量,所以如果在解锁的过程中,key已经过期失效,也当做解锁成功,这种情况返回的是0,为了跟这种情况区分开,所以判断解锁失败的标记为-1
完整的demo演示可以参考:https://gitee.com/lqccan/blog-demo demo14中的测试类RedisLockTest