redis分布式事务
概述:
场景:多系统(多进程)对同一资源并发修改操作。PS:同一进程的多线程调用是系统内事务管理不属于分布式事务。
思路:通过设置唯一锁,判断是否又其他客户端在使用。redis中可以通过setNx(key)来上锁,get(key)检查是否被上锁。
难点:保证锁的唯一性、原子性、高可用性、容错性和避免死锁。
思考
基于分布式事务产生的的场景进行考虑解决方案
1、在多系统并发操作:选择合适的中间件(跨系统)、数据库(底层)。
2、容错性:就要确保在提供服务方的稳定、容错性,应该提供集群的概念。
3、高效:redis、rocketMQ、zookeeper|mysql。
4、集合事务本质和所掌握的中间件进行思考,技术的出现是为了解决业务难题。
实现方式:
1、redis
2、zookeper:
3、mysql乐观锁
4、消息中间件
redis解决方案
redis命令
SETNX
SETNX key val
当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
expire
expire key timeout
为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
delete
delete key
删除key
实现方式
-
bug示例:
Jedis client = jedisPool.getResource(); //当前锁空闲 if(client.setnx("lockResource",value) == 0){ //设置锁时间 client.expire("",expire); }
存在问题解析:
在对锁的操作不具有原子性,(加锁和时间设置)。当出现进程在设置时间时候崩溃错误问题,锁将永远保存,不主动处理。
-
不推荐示例,将过期+当前时间戳时间设置为value,通过比对来设置锁
public boolean lock(String key, long expire, long acquireTimeout) { Jedis conn = null; String retIdentifier = null; try { // 获取连接 conn = jedisPool.getResource(); long end = System.currentTimeMillis() + acquireTimeout; while (System.currentTimeMillis() < end) { //锁的时间 long lockExpire = System.currentTimeMillis() + expire; if (conn.setnx(key, String.valueOf(lockExpire)) == 1) { return true; } String currentLockExpire = conn.get(key); //锁过期 if (lockExpire < System.currentTimeMillis()) { //获取上一个所的时间并设置新时间 String oldLockExpire = conn.getSet(key, String.valueOf(lockExpire)); if (currentLockExpire.equals(oldLockExpire)) { return true; } } } } catch (Exception e) { e.printStackTrace(); } return false; }
存在问题解析:
- 需要分布式下每个客户端的时间保持一致;
- 锁快过期时,多个客户端同时执行getSet,虽然最终只有一个客户端可以加锁,但该客户端锁的过期时间可能被其他客户端覆盖;
- 不具备拥有者标识,任何客户端都可以解锁。
-
最终解决方案
Jedis conn = jedisPool.getResource(); // SET IF NOT EXIST,而且还是原子的操作成功,返回“OK”,否则返回null conn.set(key, value, "NX", "EX", expireSeconds);
-
解锁
- del:用于删除已存在的键
- pttl:以毫秒为单位返回key的剩余过期时间
bug示例
public boolean unlock(String key){ if(1==conn.redis.del(key)){ return ture; } return false; }
错误分析:
不具备拥有者标识,谁都可以删掉。
进阶修改:增加一个返回value如果一致则删除
public boolean unlockPlus(String key,String localValue){ if(localValue=conn.get(key)){ //存在的问题是:此时key过期了被其他进程刚锁定,就有可能其他进程的锁被删掉。 if(1==conn.redis.del(key)){ return ture; } } return false; }
错误分析:
不具备原则性
最终版本:
public boolean compareAndDel(final String key, final String value) { return execute(new JedisAction<Boolean>() { String lua = " local val = redis.call('get', KEYS[1]) " + " if val == ARGV[1] then " + " redis.call('del', KEYS[1]) " + " return true " + " end"; /** lua 脚本保证一系列操作的原子性 local val = redis.call('get', KEYS[1]) if val == ARGV[1] then redis.call('del', KEYS[1]) return true end */ @Override public Boolean action(Jedis jedis) { conn.evalsha(jedis.scriptLoad(lua), Lists.newArrayList(key),Lists.newArrayList(value)); return true; } }); }
解释:
通过redis里eval命令操作lua代码,这样可以确保在解锁时保持原子性,而不会因为进程的崩溃导致解锁失败
锁到期
那么对应到锁的应用上也是这样,当占有锁的时间快到了但是此时业务未处理完,可以延长锁的过期时间,即锁支持可重入。
节点挂掉
当主节点还没有数据同步就挂掉:当在集群上实现分布式锁的时候,master节点宕机且数据未同步至slave节点时,此时就会出现多个客户端拥有一把锁的情况,违背分布式锁机制互斥性。
解决:
多个主节点冗余,核心思想是同时使用多个Redis Master来冗余,且这些节点是完全独立的,也不需要对这些节点之间的数据进行同步。获取集群中多数master节点上的锁,同时全部获取,否则全部释放。
参考:公众号:IT界农民工