利用redis中的命令setnx(key, value),key不存在就新增,存在就什么都不做。同时有多个客户端发送setnx命令,只有一个客户端可以成功,返回1(true);其他的客户端返回0(false)。
1.核心代码
- public void deductByRedis() {
- // 加锁setnx
- // 加锁,获取锁失败重试
- while (!this.stringRedisTemplate.opsForValue().setIfAbsent("ljf-lock", "111")){
- try {
- Thread.sleep(40);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- //加锁成功
- try {
- // 先查询库存是否充足
- Stock stock = this.stockMapper.selectById(1L);
- // 再减库存
- if (stock != null && stock.getCount() > 0){
- stock.setCount(stock.getCount() - 1);
- this.stockMapper.updateById(stock);
- }
- else{
- System.out.println("库存不足..........."+new Date());
- }
- } finally {
- // 解锁
- this.stringRedisTemplate.delete("ljf-lock");
- }
- }
2.controller修改调用service的方法
[root@localhost ] cd /root/export/apache-zookeeper-3.7.0-bin/bin
[root@localhost bin]# ./zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /root/export/apache-zookeeper-3.7.0-bin/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
[root@localhost bin]# ./zkCli.sh
[root@localhost ~]# redis-server /myredis/redis.conf
[root@localhost ~]# redis-cli -a 123456 -p 6379
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
1.配置
2.执行
1.初始
2.测试后
1.存在死锁问题:setnx刚刚获取到锁,当前服务器宕机,导致del释放锁无法执行,进而导致锁无法锁无法释放(死锁)
解决:给锁设置过期时间,自动释放锁。
设置过期时间两种方式:
通过expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)
使用set指令设置过期时间:set key value ex 3 nx(既达到setnx的效果,又设置了过期时间)
2.防误删:可能会释放其他服务器的锁。
场景:如果业务逻辑的执行时间是7s。执行流程如下
index1业务逻辑没执行完,3秒后锁被自动释放。
index2获取到锁,执行业务逻辑,3秒后锁被自动释放。
index3获取到锁,执行业务逻辑
index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁,导致index3的业务只执行1s就被别人释放。
最终等于没锁的情况。
解决:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁
问题:删除操作缺乏原子性。
场景:
index1执行删除时,查询到的lock值确实和uuid相等
index1执行删除前,lock刚好过期时间已到,被redis自动释放
index2获取了lock
index1执行删除,此时会把index2的lock删除
解决方案:没有一个命令可以同时做到判断 + 删除,所有只能通过其他方式实现(LUA脚本)
3.使用lua保证删除原子性
4.重入性:redis + Hash
Redis 提供了 Hash (哈希表)这种可以存储键值对数据结构。所以我们可以使用 Redis Hash 存储的锁的重入次数,然后利用 lua 脚本判断逻辑。
5.自动续期
luna脚本:
2.对应方法
加锁:
setnx:独占排他 死锁、不可重入、原子性
. set k v ex 30 nx:独占排他、死锁 不可重入
hash + lua脚本:可重入锁
判断锁是否被占用(exists),如果没有被占用则直接获取锁(hset/hincrby)并设置过期时间(expire)
如果锁被占用,则判断是否当前线程占用的(hexists),如果是则重入(hincrby)并重置过期时间(expire)
否则获取锁失败,将来代码中重试
Timer定时器 + lua脚本:实现锁的自动续期
判断锁是否自己的锁(hexists == 1),如果是自己的锁则执行expire重置过期时间
解锁
hash + lua脚本:可重入
判断当前线程的锁是否存在,不存在则返回nil,将来抛出异常
存在则直接减1(hincrby -1),判断减1后的值是否为0,为0则释放锁(del),并返回1
不为0,则返回0
重试:递归 循环
1.客户端
- @Component
- public class DistributedLockClient {
- @Autowired
- private StringRedisTemplate redisTemplate;
-
- private String uuid;
-
- public DistributedLockClient() {
- this.uuid = UUID.randomUUID().toString();
- }
-
- public DistributedRedisLock getRedisLock(String lockName){
- return new DistributedRedisLock(redisTemplate, lockName, uuid);
- }
- }
2.分布锁
- public class DistributedRedisLock implements Lock {
- private StringRedisTemplate redisTemplate;
-
- private String lockName;
-
- private String uuid;
-
- private long expire = 30;
-
- public DistributedRedisLock(StringRedisTemplate redisTemplate, String lockName, String uuid) {
- this.redisTemplate = redisTemplate;
- this.lockName = lockName;
- this.uuid = uuid + ":" + Thread.currentThread().getId();
- }
-
- @Override
- public void lock() {
- this.tryLock();
- }
-
- @Override
- public void lockInterruptibly() throws InterruptedException {
-
- }
-
- @Override
- public boolean tryLock() {
- try {
- return this.tryLock(-1L, TimeUnit.SECONDS);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- return false;
- }
-
- /**
- * 加锁方法
- * @param time
- * @param unit
- * @return
- * @throws InterruptedException
- */
- @Override
- public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
- if (time != -1){
- this.expire = unit.toSeconds(time);
- }
- String script = "if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
- "then " +
- " redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
- " redis.call('expire', KEYS[1], ARGV[2]) " +
- " return 1 " +
- "else " +
- " return 0 " +
- "end";
- while (!this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire))){
- Thread.sleep(50);
- }
- // 加锁成功,返回之前,开启定时器自动续期
- this.renewExpire();
- return true;
- }
-
- /**
- * 解锁方法
- */
- @Override
- public void unlock() {
- String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 " +
- "then " +
- " return nil " +
- "elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 " +
- "then " +
- " return redis.call('del', KEYS[1]) " +
- "else " +
- " return 0 " +
- "end";
- Long flag = this.redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuid);
- if (flag == null){
- throw new IllegalMonitorStateException("this lock doesn't belong to you!");
- }
- }
-
- @Override
- public Condition newCondition() {
- return null;
- }
-
- // String getId(){
- // return this.uuid + ":" + Thread.currentThread().getId();
- // }
-
- private void renewExpire(){
- String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
- "then " +
- " return redis.call('expire', KEYS[1], ARGV[2]) " +
- "else " +
- " return 0 " +
- "end";
- new Timer().schedule(new TimerTask() {
- @Override
- public void run() {
- if (redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire))) {
- renewExpire();
- }
- }
- }, this.expire * 1000 / 3);
- }
-
- }
3.调用
- @Autowired
- private DistributedLockClient distributedLockClient;
- public void deductByRedisLua() {
- DistributedRedisLock redisLock = this.distributedLockClient.getRedisLock("lua-lock");
- redisLock.lock();
- //加锁成功
- try {
- // 先查询库存是否充足
- Stock stock = this.stockMapper.selectById(1L);
- // 再减库存
- if (stock != null && stock.getCount() > 0){
- stock.setCount(stock.getCount() - 1);
- this.stockMapper.updateById(stock);
- }
- else{
- System.out.println("库存不足..........."+new Date());
- }
- //重入锁
- test();
- } finally {
- // 解锁
- redisLock.unlock();
- }
- }
- public void test(){
- DistributedRedisLock redisLock = this.distributedLockClient.getRedisLock("lua-lock");
- redisLock.lock();;
- System.out.println("可重入锁.....");
- redisLock.unlock();
- }
4.controller
[root@localhost ] cd /root/export/apache-zookeeper-3.7.0-bin/bin
[root@localhost bin]# ./zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /root/export/apache-zookeeper-3.7.0-bin/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
[root@localhost bin]# ./zkCli.sh
[root@localhost ~]# redis-server /myredis/redis.conf
[root@localhost ~]# redis-cli -a 123456 -p 6379
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
1.配置
2.执行
1.初始
2.测试后
查看