前言
目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。
在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。
Redis 的优点
Redis有很高的性能
Redis命令对此支持较好,实现起来比较方便
这也是选用Redis 做分布式锁的原因
启动 mysql 和 redis
一、先看下目录结构
二、一般的业务代码
@Override
public Result loginByWx(String openId) {
User user = userMapper.findByOpenId(openId);
if (user != null) {
return Result.of(0, "OK", user.getUserId());
}
// 模拟业务执行时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("create user by openId: {}", openId);
user = new User();
user.setOpenId(openId);
userMapper.save(user);
return Result.of(0, "OK", user.getUserId());
}
如果在并发或者短时间内重复请求会出现重复创建用户的问题
三、了解Redis set方法
Jedis.set 方法
public String set(final String key, final String value, final String nxxx, final String expx, final int time)
存储数据到缓存中,并制定过期时间和当Key存在时是否覆盖。
key 锁的名字
value 锁的内容
nxxx的值只能取NX或者XX,如果取NX,则只有当key不存在是才进行set,如果取XX,则只有当key已经存在时才进行set
expx的值只能取EX或者PX,代表数据过期时间的单位,EX代表秒,PX代表毫秒。
time 过期时间,单位是expx所代表的单位。
四、编写分布式锁
package com.itunion.demo.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
@Service
public class RedisLockServiceImpl implements LockService {
private static Logger log = LoggerFactory.getLogger(UserServiceImpl.class);
private static final String LOCK_SUCCESS = "OK";
@Autowired
protected RedisTemplate<String, String> redisTemplate;
@Override
public void unLock(String key) {
redisTemplate.delete(key);
}
public synchronized boolean isLock(String key, int seconds) {
return redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
Jedis conn = (Jedis) connection.getNativeConnection();
String result = conn.set(key, "1", "NX", "EX", seconds);
return result != null && result.equalsIgnoreCase(LOCK_SUCCESS);
}
});
}
@Override
public <T> T lockExecute(String key, LockExecute<T> lockExecute) {
boolean isLock = isLock(key, 15);
final int SLEEP_TIME = 200;
final int RETRY_NUM = 20;
int i;
for (i = 0; i < RETRY_NUM; i++) {
if (isLock) {
break;
}
try {
log.debug("wait redis lock key > {}", key);
Thread.sleep(SLEEP_TIME);
} catch (InterruptedException e) {
log.warn("wait redis error {}", e.getMessage());
}
isLock = isLock(key, 150);
}
if (!isLock) {
log.warn("wait lock time out key > {}", key);
return lockExecute.waitTimeOut();
}
try {
if (i > 0) log.debug("wait lock retry count {}", i);
return lockExecute.execute();
} finally {
unLock(key);
}
}
}
五、在业务上添加锁
public Result loginByWxLock(String openId){
// 更新点赞数量
String lockKey = "lock:loginByWx:" + openId;
return lockService.lockExecute(lockKey, new LockService.LockExecute<Result>() {
@Override
public Result execute() {
return loginByWx(openId);
}
@Override
public Result waitTimeOut() {
return Result.of(-1, "访问太频繁");
}
});
}
六、单元测试
@Test
public void contextLoads() throws InterruptedException {
int size = 100;
CountDownLatch countDownLatch = new CountDownLatch(size);
List<Result> results = new ArrayList<>();
for (int i = 0; i < size; i++) {
new Thread(new Runnable() {
@Override
public void run() {
Random random = new Random();
String openId = "openId:" + random.nextInt(10);
results.add(userService.loginByWxLock(openId));
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
// 统计信息
int loginSuccessNum = 0;
int loginFailedNum = 0;
for (Result result : results) {
if (result.getCode() == 0) {
loginSuccessNum++;
}else{
loginFailedNum++;
}
}
System.out.println("登录成功总数:" + loginSuccessNum);
System.out.println("登录失败总数:" + loginFailedNum);
}
测试结果
Github: https://github.com/qiaohhgz/spring-boot-redis-lock.git
关注我们
注意:本文归作者所有,未经作者允许,不得转载