分布式锁的原理:基于redis的setnx命令,setnx的作用就是设置一个key值,如果在redis中该key值不存在就设置成功,如果存在就会设置失败。在分布式集群环境下,多个服务器的线程同时设置一个key,哪个服务器的线程设置成功,就表示该服务器的线程获得了锁对象,其他线程必须等待。获得锁的线程需要记得,在某个时刻进行锁的释放(删除那个key)。
实现思路:
(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.UUID; /** * 分布式锁的工具类 */ @Component public class RedisLockUtil { @Autowired private RedisTemplate redisTemplate; //redis原始连接对象 private RedisConnection redisConnection; //lua脚本的缓存签名字符串 private String lockSha; private String unlockSha; private ThreadLocal<String> threadLocal = new ThreadLocal<>(); /** * 添加分布式锁的Lua脚本(Lua脚本可以保证命令的原子性操作,因此在需要线程安全的操作时,我们可以考虑Lua脚本) */ private String lockLua = "local key = KEYS[1]\n" + "local value = ARGV[1]\n" + "local time = ARGV[2]\n" + "\n" + "local result = redis.call('setnx', key, value)\n" + "if result == 1 then\n" + " --当前获得了分布式锁\n" + " --设置锁的过期时间\n" + " redis.call('expire', key, time)\t\n" + " return true\t\n" + "end\n" + "\n" + "--没有获得分布式锁\n" + "return false"; //解锁的lua脚本 private String unlockLua = "--要删除的是什么锁\n" + "local key = KEYS[1]\n" + "local uuid = ARGV[1]\n" + "\n" + "--获取锁中的uuid\n" + "local lockUUID = redis.call('get', key)\n" + "\n" + "--判断是不是自己上的锁\n" + "if uuid == lockUUID then\n" + " --是自己上的锁,删除\n" + " redis.call('del', key)\n" + " return true\n" + "end\n" + "\n" + "--不是自己上的锁\n" + "return false"; @PostConstruct public void init(){ //获得原始连接 redisConnection = redisTemplate.getConnectionFactory().getConnection(); //缓存lua脚本 lockSha = redisConnection.scriptLoad(lockLua.getBytes()); unlockSha = redisConnection.scriptLoad(unlockLua.getBytes()); } /** * 加锁的方法 * @return */ public boolean lock(String key, int timeout){ String uuid = UUID.randomUUID().toString(); //线程间的uuid数据隔离 threadLocal.set(uuid); //执行加锁的lua脚本 boolean flag = redisConnection.evalSha(lockSha, ReturnType.BOOLEAN, 1, key.getBytes(), uuid.getBytes(), (timeout + "").getBytes()); return flag; } /** * 解锁的方法 * @return */ public boolean unlock(String key){ //解锁原则:谁加的锁,谁来解锁。 String uuid = threadLocal.get(); //执行解锁的lua boolean flag = redisConnection.evalSha(unlockSha, ReturnType.BOOLEAN, 1, key.getBytes(), uuid.getBytes()); return flag; } }
分布锁的应用(Service层加锁)
import com.qf.stu.student_demo.dao.IStuDao; import com.qf.stu.student_demo.entity.Student; import com.qf.stu.student_demo.service.IStuService; import com.qf.stu.student_demo.until.RedisLockUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Primary; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.util.List; import java.util.concurrent.TimeUnit; @Service public class StuServiceImpl implements IStuService { @Autowired private IStuDao stuDao; @Autowired private RedisTemplate redisTemplate; @Autowired private RedisLockUtil redisLockUtil; /** * 分布式集群架构-分布式锁 */ @Override public List<Student> queryAll(){ //先查询redis是否有缓存该数据 List<Student> stus = (List<Student>) redisTemplate.opsForValue().get("stus"); //判断缓存中是否存在 if (stus == null) { //通过lua脚本获得分布式锁 boolean flag = redisLockUtil.lock("lock", 120); if (flag){ //获得分布式锁,进行缓存重建 stus = stuDao.queryAll();//查询数据库 //重建缓存 redisTemplate.opsForValue().set("stus", stus); //设置过期时间 redisTemplate.expire("stus", 5, TimeUnit.MINUTES); //释放锁 redisLockUtil.unlock("lock"); }else { //未拿到分布式锁,则休眠50毫秒后,再递归调用 queryAll() try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } return this.queryAll(); } } return stus; } /* *//** * 单体架构 * @return *//* @Override public List<Student> queryAll() { //先查询缓存 List<Student> stus = (List<Student>) redisTemplate.opsForValue().get("stus"); //判断缓存中是否存在 if(stus == null){ synchronized (this){ stus = (List<Student>) redisTemplate.opsForValue().get("stus"); if (stus == null){ System.out.println("查询了数据库"); stus = stuDao.queryAll(); } } //重建缓存 redisTemplate.opsForValue().set("stus",stus); //设置过期时间 redisTemplate.expire("stus",5, TimeUnit.MINUTES); } return stus; }*/ }
================ SpringBoot提供的操作缓存服务器的API ================
@Cacheable:标记了当前注解的方法,在执行这个方法前,会先去缓存服务中查询数据,如果有就不会调用目标方法,如果没有再调用目标方法,并且重建缓存 - 主要作用于查询方法
@CachePut:该注解作用和@Cacheable差不多,唯一的区别在于被@CachePut注解标记的方法,一定会被执行。被标记方法的返回值会添加到缓存服务中 - 主要作用于添加的方法
@CacheEvict:根据表达式,删除缓存服务器的某个注解 - 主要用于修改和删除的方法
@Caching:可以帮助开发者在同一个方法上标记多个相同的缓存注解
import com.qf.stu.student_demo.dao.IStuDao; import com.qf.stu.student_demo.entity.Student; import com.qf.stu.student_demo.service.IStuService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Caching; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import java.util.List; @Service @Primary public class StuServiceSpringCacheImpl implements IStuService { @Autowired private IStuDao stuDao; @Override @Cacheable(cacheNames = "stu",key = "'stulist'") public List<Student> queryAll() { System.out.println("查询所有学生的方法"); return stuDao.queryAll(); } @Override @CachePut(cacheNames = "stu",key = "'stuone' + #result.id")//从当前方法的返回值中找到id的属性,作为stuone的key值 @CacheEvict(cacheNames = "stu",key ="'stulist'") public Student insert(Student student) { System.out.println("添加学生到数据库"); stuDao.insert(student); return student; } @Override @Cacheable(cacheNames = "stu",key = "'stuone' + #id")//从参数中获取id public Student queryOne(Integer id) { System.out.println("根据id查询学生信息"); return stuDao.queryOne(id); } @Override @Caching(evict = { @CacheEvict(cacheNames = "stu" ,key = "'stulist'"), @CacheEvict(cacheNames = "stu" ,key = "'stulist' + #id") }) public int deleteById(Integer id) { return stuDao.deleteById(id); } }
注意:使用这些SpringBoot提供的操作缓存服务器的API,启动类上必须加@EnableCaching注解
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; @SpringBootApplication @EnableCaching public class StudentDemoApplication { public static void main(String[] args) { SpringApplication.run(StudentDemoApplication.class, args); } }
application.yml配置
redis:
host: xx.xx.xx.xx
password: root
cache:
type: redis
redis:
time-to-live: 60000