由于互联网的发展,现在大部分项目都是分布式架构多机部署的,为了防止某些需要单一执行的操作同时被多台服务器执行了,就需要在这些业务点上加上分布式锁,来防止多服务器的并发和重复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也是唯一的,所以上锁的过程其实就是向服务注册中心注册服务,谁成功了,谁就相当于获得锁了。