利用DB实现一个业务锁例子

由于互联网的发展,现在大部分项目都是分布式架构多机部署的,为了防止某些需要单一执行的操作同时被多台服务器执行了,就需要在这些业务点上加上分布式锁,来防止多服务器的并发和重复job。常见的场景比方说给用户发送一些消息,如果同一时刻N台服务器获取到了任务,那么结果将不堪设想,很容易对用户形成了信息轰炸的效果,如果再触发一些限流机制,那直接影响后续的业务进行了。
下面这个例子呢,只是为了更好的理解锁的概念,生产中常用的锁可以使用redis的分布式锁机制。

锁模型SQl

CREATE TABLE `lock` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT ‘自增主键‘,
  `key` varchar(128) NOT NULL COMMENT ‘锁唯一性Key‘,
  `create_time` datetime NOT NULL COMMENT ‘创建时间‘,
  `expire_time` bigint(20) unsigned NOT NULL COMMENT ‘锁过期时间戳‘,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_lock` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

简单的锁模型

@TableName("lock")
@Data
public class LockEntity {
 /**
  * 自增的id,当解锁的时候做删除操作时用作乐观锁条件
  */
 @Version
 private Long id;
 /**
  * 锁的key,唯一
  */
 private String key;
 /**
  * 过期时间
  */
 private Long expireTime;
 /**
  * 创建时间
  */
 private Date createTime;
 /**
  * db的当前时间,用于判断是否过期
  */
 private Long dbTime;
}

提供锁能力的Service

public interface LockService {
    /**
     * 尝试获得锁
     */
    LockEntity tryLock(String key, Long seconds);

    /**
     * 解锁
     */
    Boolean unLock(String key, Long lockId);
}

具体实现ServiceImpl

@Service
public class LockServiceImpl implements LockService {
    @Autowired
    private LockMapper lockMapper;

    @Override
    public LockEntity tryLock(String key, Long seconds) {
        try {
            return lock(key, seconds);
        } catch (Exception e) {
            //获得锁过程中出错了
            return null;
        }
    }

    private LockEntity lock(String key, Long seconds) {
        //每次上锁之前先校验
        LockEntity lockEntity = lockMapper.selectByKey(key);
        /**
         * 如果锁存在了
         * 判断过期时间,如果没有过期,说明正在有线程持有锁
         * 如果已经过期了,说明由于某种原因没有释放锁
         */
        if (lockEntity != null) {
            if (lockEntity.getDbTime() < lockEntity.getExpireTime()) {
                return null;
            } else {
                /**
                 * 如果多个线程删除同一个锁,必须用id作为乐观锁进行操作
                 * 哪个线程删除成功了,就有获得锁的权力
                 */
                if (!removeLock(key, lockEntity.getId())) {
                    return null;
                }
            }
        }
        lockMapper.insert(key, new Date(), seconds);
        return lockMapper.selectByKey(key);
    }

    @Override
    public Boolean unLock(String key, Long lockId) {
        return removeLock(key, lockId);
    }

    private boolean removeLock(String key, Long id) {
        return lockMapper.delete(Wrappers.<LockEntity>lambdaQuery()
                .eq(LockEntity::getKey, key)
                .eq(LockEntity::getId, id)) > 0;
    }

}

其他的一些sql(由于仅仅作为笔记,所以没有按照mybatis规范去实现持久层代码)

@Mapper
public interface LockMapper extends BaseMapper<LockEntity> {

    String columns = "id,key,create_time as createTime,expire_time as expireTime,unix_timestamp(now()) as dbTime ";

    @Select("select " + columns + "form lock where #{key}")
    LockEntity selectByKey(String key);

    @Insert("insert into lock (key,create_time,expire_time) values (#{key,#{createTime},unix_timestamp(now())+#{expireTime})")
    void insert(String key, Date createDate,Long expireTime);
}

这个小例子呢,其实是利用了mysql的唯一主键的排他性,将DB的排他能力运用到了我们的业务服务中。
此外,了解了这样的思路之后,我们是不是也可以用一些其他第三方的服务特性来实现业务锁呢,比如服务注册中心,服务的注册id也是唯一的,所以上锁的过程其实就是向服务注册中心注册服务,谁成功了,谁就相当于获得锁了。

利用DB实现一个业务锁例子

上一篇:mysql-常用命令


下一篇:MYSQL高可用集群架构-MHA架构 (一主双从)